System tray (#70)

added SystemTray Service
This commit is contained in:
kotontrion
2023-09-04 21:57:29 +02:00
committed by GitHub
parent 6b043b37af
commit 665c46b7dc
20 changed files with 2562 additions and 1821 deletions

View File

@@ -4,27 +4,16 @@ import Notifications from 'resource:///com/github/Aylur/ags/service/notification
import Mpris from 'resource:///com/github/Aylur/ags/service/mpris.js';
import Audio from 'resource:///com/github/Aylur/ags/service/audio.js';
import Battery from 'resource:///com/github/Aylur/ags/service/battery.js';
import SystemTray from 'resource:///com/github/Aylur/ags/service/systemtray.js';
import App from 'resource:///com/github/Aylur/ags/app.js';
import {
Box, Button, Stack, Label, Icon, CenterBox, Window, Slider, ProgressBar
} from 'resource:///com/github/Aylur/ags/widget.js';
import Widget from 'resource:///com/github/Aylur/ags/widget.js';
import { exec, execAsync } from 'resource:///com/github/Aylur/ags/utils.js';
// import statements are long, so there is also the global ags object you can import from
// const { Hyprland, Notifications, Mpris, Audio, Battery } = ags.Service;
// const { App } = ags;
// const { exec } = ags.Utils;
// const {
// Box, Button, Stack, Label, Icon, CenterBox, Window, Slider, ProgressBar
// } = ags.Widget;
// every widget is a subclass of Gtk.<widget>
// with a few extra available properties
// for example Box is a subclass of Gtk.Box
const { Box, Button, Stack, Label, Icon, CenterBox, Window, Slider, ProgressBar } = Widget;
// widgets can be only assigned as a child in one container
// so to make a reuseable widget, just make it a function
// then you can use it by calling simply calling it
const Workspaces = () => Box({
className: 'workspaces',
connections: [[Hyprland, box => {
@@ -161,6 +150,20 @@ const BatteryLabel = () => Box({
],
});
const SysTray = () => Box({
connections: [[SystemTray, box => {
box.children = SystemTray.items.map(item => Button({
child: Icon(),
onPrimaryClick: (_, event) => item.activate(event),
onSecondaryClick: (_, event) => item.openMenu(event),
connections: [[item, button => {
button.child.icon = item.icon;
button.tooltipMarkup = item.tooltipMarkup;
}]],
}));
}]],
});
// layout of the bar
const Left = () => Box({
children: [
@@ -182,11 +185,12 @@ const Right = () => Box({
Volume(),
BatteryLabel(),
Clock(),
SysTray(),
],
});
const Bar = ({ monitor } = {}) => Window({
name: `bar${monitor || ''}`, // name has to be unique
name: `bar-${monitor}`, // name has to be unique
className: 'bar',
monitor,
anchor: ['top', 'left', 'right'],

27
flake.lock generated
View File

@@ -1,32 +1,12 @@
{
"nodes": {
"dongsu8142-nur": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1690083492,
"narHash": "sha256-UoIG+sl44U38FTmM2+m4X2Qi5aivjITcUxAkVHouZl4=",
"owner": "dongsu8142",
"repo": "nur",
"rev": "7f5c7067a482e96fe3787e07a54e21041a4d15c0",
"type": "github"
},
"original": {
"owner": "dongsu8142",
"repo": "nur",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1690031011,
"narHash": "sha256-kzK0P4Smt7CL53YCdZCBbt9uBFFhE0iNvCki20etAf4=",
"lastModified": 1693471703,
"narHash": "sha256-0l03ZBL8P1P6z8MaSDS/MvuU8E75rVxe5eE1N6gxeTo=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "12303c652b881435065a98729eb7278313041e49",
"rev": "3e52e76b70d5508f3cec70b882a29199f4d1ee85",
"type": "github"
},
"original": {
@@ -38,7 +18,6 @@
},
"root": {
"inputs": {
"dongsu8142-nur": "dongsu8142-nur",
"nixpkgs": "nixpkgs"
}
}

View File

@@ -1,31 +1,23 @@
{
description = "A customizable and extensible shell";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
dongsu8142-nur = {
url = "github:dongsu8142/nur";
inputs.nixpkgs.follows = "nixpkgs";
};
};
outputs = inputs@{
self,
nixpkgs,
...
}: let
lib = nixpkgs.lib;
genSystems = lib.genAttrs [
outputs = { nixpkgs, self }:
let
genSystems = nixpkgs.lib.genAttrs [
"aarch64-linux"
"x86_64-linux"
];
pkgsFor = genSystems (system:
import nixpkgs {
inherit system;
}
);
in {
pkgs = genSystems (system: import nixpkgs { inherit system; });
in
{
packages = genSystems (system: {
default = pkgsFor.${system}.callPackage ./nix/default.nix { inherit inputs; };
default = pkgs.${system}.callPackage ./nix/ags.nix {
gjs = pkgs.${system}.callPackage ./nix/gjs.nix { };
};
});
};
}

View File

@@ -1,17 +1,15 @@
{
lib,
stdenv,
system,
inputs,
buildNpmPackage,
fetchFromGitLab,
nodePackages,
meson,
pkg-config,
ninja,
gobject-introspection,
gtk3,
libpulseaudio
{ lib
, stdenv
, buildNpmPackage
, fetchFromGitLab
, nodePackages
, meson
, pkg-config
, ninja
, gobject-introspection
, gtk3
, libpulseaudio
, gjs
}:
let
@@ -40,7 +38,7 @@ stdenv.mkDerivation {
dontBuild = true;
npmDepsHash = "sha256-uNdmlQIwXoO8Ls0qjJnwRGqpfiJK1PajAvoiHfJXcxg=";
npmDepsHash = "sha256-4BNbFi/Ltg/8tuicrrMBIdOhteEIs85Zqj9oI/hYbl0=";
installPhase = ''
mkdir $out
@@ -56,7 +54,7 @@ stdenv.mkDerivation {
'';
patches = [
./lib-path.patch
./gvc-path.patch
];
nativeBuildInputs = [
@@ -68,7 +66,7 @@ stdenv.mkDerivation {
buildInputs = [
gobject-introspection
inputs.dongsu8142-nur.packages.${system}.gtk-gjs
gjs
gtk3
libpulseaudio
];

13
nix/fix-paths.patch Normal file
View File

@@ -0,0 +1,13 @@
diff --git a/installed-tests/debugger-test.sh b/installed-tests/debugger-test.sh
index 0d118490..54c5507e 100755
--- a/installed-tests/debugger-test.sh
+++ b/installed-tests/debugger-test.sh
@@ -3,7 +3,7 @@
if test "$GJS_USE_UNINSTALLED_FILES" = "1"; then
gjs="$TOP_BUILDDIR/gjs-console"
else
- gjs=gjs-console
+ gjs=@gjsConsole@
fi
echo 1..1

161
nix/gjs.nix Normal file
View File

@@ -0,0 +1,161 @@
{ fetchurl
, lib
, stdenv
, meson
, mesonEmulatorHook
, ninja
, pkg-config
, gnome
, gtk3
, atk
, gobject-introspection
, spidermonkey_102
, pango
, cairo
, readline
, libsysprof-capture
, glib
, libxml2
, dbus
, gdk-pixbuf
, networkmanager
, harfbuzz
, makeWrapper
, wrapGAppsHook
, which
, xvfb-run
, nixosTests
, upower
, glib-networking
, gtk-layer-shell
, libdbusmenu-gtk3
}:
let
testDeps = [
gtk3 atk pango.out gdk-pixbuf harfbuzz
];
in stdenv.mkDerivation rec {
pname = "gjs";
version = "1.76.2";
outputs = [ "out" "dev" "installedTests" ];
src = fetchurl {
url = "mirror://gnome/sources/gjs/${lib.versions.majorMinor version}/${pname}-${version}.tar.xz";
sha256 = "sha256-99jJ1lPqb9eK/kpQcg4EaqK/wHj9pjXdEwZ90ZnGJdQ=";
};
patches = [
# Hard-code various paths
./fix-paths.patch
# Allow installing installed tests to a separate output.
./installed-tests-path.patch
];
nativeBuildInputs = [
meson
ninja
pkg-config
makeWrapper
wrapGAppsHook
which # for locale detection
libxml2 # for xml-stripblanks
dbus # for dbus-run-session
gobject-introspection
] ++ lib.optionals (!stdenv.buildPlatform.canExecute stdenv.hostPlatform) [
mesonEmulatorHook
];
buildInputs = [
cairo
upower
gnome.gnome-bluetooth
gtk-layer-shell
glib-networking
networkmanager
readline
libsysprof-capture
spidermonkey_102
libdbusmenu-gtk3
];
nativeCheckInputs = [
xvfb-run
] ++ testDeps;
propagatedBuildInputs = [
glib
];
mesonFlags = [
"-Dinstalled_test_prefix=${placeholder "installedTests"}"
] ++ lib.optionals (!stdenv.isLinux || stdenv.hostPlatform.isMusl) [
"-Dprofiler=disabled"
];
doCheck = !stdenv.isDarwin;
postPatch = ''
patchShebangs build/choose-tests-locale.sh
substituteInPlace installed-tests/debugger-test.sh --subst-var-by gjsConsole $out/bin/gjs-console
'' + lib.optionalString stdenv.hostPlatform.isMusl ''
substituteInPlace installed-tests/js/meson.build \
--replace "'Encoding'," "#'Encoding',"
'';
preCheck = ''
# Our gobject-introspection patches make the shared library paths absolute
# in the GIR files. When running tests, the library is not yet installed,
# though, so we need to replace the absolute path with a local one during build.
# We are using a symlink that will be overridden during installation.
mkdir -p $out/lib $installedTests/libexec/installed-tests/gjs
ln -s $PWD/libgjs.so.0 $out/lib/libgjs.so.0
ln -s $PWD/installed-tests/js/libgimarshallingtests.so $installedTests/libexec/installed-tests/gjs/libgimarshallingtests.so
ln -s $PWD/installed-tests/js/libgjstesttools/libgjstesttools.so $installedTests/libexec/installed-tests/gjs/libgjstesttools.so
ln -s $PWD/installed-tests/js/libregress.so $installedTests/libexec/installed-tests/gjs/libregress.so
ln -s $PWD/installed-tests/js/libwarnlib.so $installedTests/libexec/installed-tests/gjs/libwarnlib.so
'';
postInstall = ''
# TODO: make the glib setup hook handle moving the schemas in other outputs.
installedTestsSchemaDatadir="$installedTests/share/gsettings-schemas/${pname}-${version}"
mkdir -p "$installedTestsSchemaDatadir"
mv "$installedTests/share/glib-2.0" "$installedTestsSchemaDatadir"
'';
postFixup = ''
wrapProgram "$installedTests/libexec/installed-tests/gjs/minijasmine" \
--prefix XDG_DATA_DIRS : "$installedTestsSchemaDatadir" \
--prefix GI_TYPELIB_PATH : "${lib.makeSearchPath "lib/girepository-1.0" testDeps}"
'';
checkPhase = ''
runHook preCheck
xvfb-run -s '-screen 0 800x600x24' \
meson test --print-errorlogs
runHook postCheck
'';
separateDebugInfo = stdenv.isLinux;
passthru = {
tests = {
installed-tests = nixosTests.installed-tests.gjs;
};
updateScript = gnome.updateScript {
packageName = "gjs";
versionPolicy = "odd-unstable";
};
};
meta = with lib; {
description = "JavaScript bindings for GNOME";
homepage = "https://gitlab.gnome.org/GNOME/gjs/blob/master/doc/Home.md";
license = licenses.lgpl2Plus;
maintainers = teams.gnome.members;
platforms = platforms.unix;
};
}

View File

@@ -0,0 +1,37 @@
diff --git a/installed-tests/meson.build b/installed-tests/meson.build
index 04c7910f..9647908c 100644
--- a/installed-tests/meson.build
+++ b/installed-tests/meson.build
@@ -1,7 +1,7 @@
### Installed tests ############################################################
-installed_tests_execdir = get_option('prefix') / get_option('libexecdir') / 'installed-tests' / meson.project_name()
-installed_tests_metadir = abs_datadir / 'installed-tests' / meson.project_name()
+installed_tests_execdir = get_option('installed_test_prefix') / 'libexec' / 'installed-tests' / meson.project_name()
+installed_tests_metadir = get_option('installed_test_prefix') / 'share' / 'installed-tests' / meson.project_name()
# Simple shell script tests #
diff --git a/meson.build b/meson.build
index 9ab29475..42ffe07f 100644
--- a/meson.build
+++ b/meson.build
@@ -557,7 +557,7 @@ install_data('installed-tests/extra/lsan.supp',
install_dir: get_option('datadir') / api_name / 'lsan')
if get_option('installed_tests')
- schemadir = abs_datadir / 'glib-2.0' / 'schemas'
+ schemadir = get_option('installed_test_prefix') / 'share' / 'glib-2.0' / 'schemas'
install_data('installed-tests/js/org.gnome.GjsTest.gschema.xml', install_dir: schemadir)
meson.add_install_script('build/compile-gschemas.py', schemadir)
endif
diff --git a/meson_options.txt b/meson_options.txt
index 825ba77a..21f0323c 100644
--- a/meson_options.txt
+++ b/meson_options.txt
@@ -25,3 +25,5 @@ option('skip_gtk_tests', type: 'boolean', value: false,
description: 'Skip tests that need a display connection')
option('verbose_logs', type: 'boolean', value: false,
description: 'Enable extra log messages that may decrease performance (not allowed in release builds)')
+option('installed_test_prefix', type: 'string', value: '',
+ description: 'Prefix for installed tests')

3470
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,22 +1,23 @@
{
"name": "ags",
"version": "1.0.0",
"description": "description",
"main": "src/main.ts",
"repository": "",
"author": "Aylur",
"license": "GPL",
"dependencies": {
"@girs/gvc-1.0": "^1.0.0-3.1.0",
"@girs/nm-1.0": "^1.43.1-3.1.0",
"typescript": "^5.1.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"eslint": "^8.42.0"
},
"scripts": {
"test": "eslint ."
}
"name": "ags",
"version": "1.0.0",
"description": "description",
"main": "src/main.ts",
"repository": "",
"author": "Aylur",
"license": "GPL",
"dependencies": {
"@girs/dbusmenugtk3-0.4": "^0.4.0-3.2.0",
"@girs/gvc-1.0": "^1.0.0-3.1.0",
"@girs/nm-1.0": "^1.43.1-3.1.0",
"typescript": "^5.1.0"
},
"devDependencies": {
"@typescript-eslint/eslint-plugin": "^5.33.0",
"@typescript-eslint/parser": "^5.33.0",
"eslint": "^8.42.0"
},
"scripts": {
"test": "eslint ."
}
}

View File

@@ -36,6 +36,7 @@
<file>service/mpris.js</file>
<file>service/network.js</file>
<file>service/notifications.js</file>
<file>service/systemtray.js</file>
<file>dbus/types.js</file>
<file>dbus/com.github.Aylur.ags.xml</file>
@@ -45,5 +46,7 @@
<file>dbus/org.freedesktop.UPower.Device.xml</file>
<file>dbus/org.mpris.MediaPlayer2.Player.xml</file>
<file>dbus/org.mpris.MediaPlayer2.xml</file>
<file>dbus/org.kde.StatusNotifierWatcher.xml</file>
<file>dbus/org.kde.StatusNotifierItem.xml</file>
</gresource>
</gresources>

View File

@@ -0,0 +1,50 @@
<node>
<interface name="org.kde.StatusNotifierItem">
<property name="Category" type="s" access="read"/>
<property name="Id" type="s" access="read"/>
<property name="Title" type="s" access="read"/>
<property name="Status" type="s" access="read"/>
<property name="WindowId" type="i" access="read"/>
<property name="IconThemePath" type="s" access="read"/>
<property name="ItemIsMenu" type="b" access="read"/>
<property name="Menu" type="o" access="read"/>
<property name="IconName" type="s" access="read"/>
<property name="IconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName"
value="KDbusImageVector"/>
</property>
<property name="AttentionIconName" type="s" access="read"/>
<property name="AttentionIconPixmap" type="a(iiay)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName"
value="KDbusImageVector"/>
</property>
<property name="ToolTip" type="(sa(iiay)ss)" access="read">
<annotation name="org.qtproject.QtDBus.QtTypeName"
value="KDbusToolTipStruct"/>
</property>
<method name="ContextMenu">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Activate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="SecondaryActivate">
<arg name="x" type="i" direction="in"/>
<arg name="y" type="i" direction="in"/>
</method>
<method name="Scroll">
<arg name="delta" type="i" direction="in"/>
<arg name="orientation" type="s" direction="in"/>
</method>
<signal name="NewTitle"/>
<signal name="NewIcon"/>
<signal name="NewAttentionIcon"/>
<signal name="NewOverlayIcon"/>
<signal name="NewToolTip"/>
<signal name="NewStatus">
<arg name="status" type="s"/>
</signal>
</interface>
</node>

View File

@@ -0,0 +1,36 @@
<node>
<interface name="org.kde.StatusNotifierWatcher">
<annotation name="org.gtk.GDBus.C.Name" value="Watcher" />
<method name="RegisterStatusNotifierItem">
<annotation name="org.gtk.GDBus.C.Name" value="RegisterItem" />
<arg name="service" type="s" direction="in"/>
</method>
<method name="RegisterStatusNotifierHost">
<annotation name="org.gtk.GDBus.C.Name" value="RegisterHost" />
<arg name="service" type="s" direction="in"/>
</method>
<property name="RegisteredStatusNotifierItems" type="as" access="read">
<annotation name="org.gtk.GDBus.C.Name" value="RegisteredItems" />
<annotation name="org.qtproject.QtDBus.QtTypeName.Out0"
value="QStringList"/>
</property>
<property name="IsStatusNotifierHostRegistered" type="b" access="read">
<annotation name="org.gtk.GDBus.C.Name" value="IsHostRegistered" />
</property>
<property name="ProtocolVersion" type="i" access="read"/>
<signal name="StatusNotifierItemRegistered">
<annotation name="org.gtk.GDBus.C.Name" value="ItemRegistered" />
<arg type="s" direction="out" name="service" />
</signal>
<signal name="StatusNotifierItemUnregistered">
<annotation name="org.gtk.GDBus.C.Name" value="ItemUnregistered" />
<arg type="s" direction="out" name="service" />
</signal>
<signal name="StatusNotifierHostRegistered">
<annotation name="org.gtk.GDBus.C.Name" value="HostRegistered" />
</signal>
<signal name="StatusNotifierHostUnregistered">
<annotation name="org.gtk.GDBus.C.Name" value="HostUnregistered" />
</signal>
</interface>
</node>

View File

@@ -45,6 +45,27 @@ export interface BatteryProxy extends Gio.DBusProxy {
IsPresent: boolean
}
export interface StatusNotifierItemProxy extends Gio.DBusProxy {
new(...args: unknown[]): StatusNotifierItemProxy;
Category: string;
Id: string;
Title: string;
Status: string;
WindowId: number;
IconThemePath: string;
ItemIsMenu: boolean;
Menu: string;
IconName: string;
IconPixmap: [number, number, Uint8Array][];
AttentionIconName: string;
AttentionIconPixmap: [number, number, Uint8Array][];
ToolTip: [string, [number, number, Uint8Array], string, string];
ContextMenuAsync: (x: number, y: number) => Promise<void>;
ActivateAsync: (x: number, y: number) => Promise<void>;
SecondaryActivateAsync: (x: number, y: number) => Promise<void>;
ScrollAsync: (delta: number, orientation: string) => Promise<void>;
}
export interface AgsProxy extends Gio.DBusProxy {
new(...args: unknown[]): AgsProxy
InspectorRemote: () => void;
@@ -56,3 +77,25 @@ export interface AgsProxy extends Gio.DBusProxy {
busName?: string,
objPath?: string) => void
}
export interface StatusNotifierItemProxy extends Gio.DBusProxy {
new(...args: unknown[]): StatusNotifierItemProxy;
Category: string;
Id: string;
Title: string;
Status: string;
WindowId: number;
IconThemePath: string;
ItemIsMenu: boolean;
Menu: string;
IconName: string;
IconPixmap: [number, number, Uint8Array][];
AttentionIconName: string;
AttentionIconPixmap: [number, number, Uint8Array][];
ToolTip: [string, [number, number, Uint8Array], string, string];
ContextMenuAsync: (x: number, y: number) => Promise<void>;
ActivateAsync: (x: number, y: number) => Promise<void>;
SecondaryActivateAsync: (x: number, y: number) => Promise<void>;
ScrollAsync: (delta: number, orientation: string) => Promise<void>;
}

View File

@@ -8,6 +8,7 @@ import Hyprland from './service/hyprland.js';
import Mpris from './service/mpris.js';
import Network from './service/network.js';
import Notifications from './service/notifications.js';
import SystemTray from './service/systemtray.js';
export {
Applications,
@@ -18,6 +19,7 @@ export {
Mpris,
Network,
Notifications,
SystemTray,
Service,
};
@@ -30,5 +32,6 @@ Service.Mpris = Mpris;
Service.Network = Network;
Service.Notifications = Notifications;
Service.Service = Service;
Service.SystemTray = SystemTray;
export default Service;

267
src/service/systemtray.ts Normal file
View File

@@ -0,0 +1,267 @@
import Gio from 'gi://Gio';
import GLib from 'gi://GLib';
import Gdk from 'gi://Gdk?version=3.0';
import Gtk from 'gi://Gtk?version=3.0';
import GdkPixbuf from 'gi://GdkPixbuf';
import DbusmenuGtk3 from 'gi://DbusmenuGtk3';
import Service from './service.js';
import { StatusNotifierItemProxy } from '../dbus/types.js';
import { bulkConnect, loadInterfaceXML } from '../utils.js';
import Widget from '../widget.js';
const StatusNotifierWatcherIFace = loadInterfaceXML('org.kde.StatusNotifierWatcher');
const StatusNotifierItemIFace = loadInterfaceXML('org.kde.StatusNotifierItem');
const StatusNotifierItemProxy =
Gio.DBusProxy.makeProxyWrapper(StatusNotifierItemIFace) as StatusNotifierItemProxy;
export class TrayItem extends Service {
static {
Service.register(this, {
'removed': ['string'],
'ready': [],
});
}
private _proxy: StatusNotifierItemProxy;
private _busName: string;
menu?: DbusmenuGtk3.Menu;
constructor(busName: string, objectPath: string) {
super();
this._busName = busName;
this._proxy = new StatusNotifierItemProxy(
Gio.DBus.session,
busName,
objectPath,
this._itemProxyAcquired.bind(this),
null,
Gio.DBusProxyFlags.NONE);
}
activate(event: Gdk.Event) {
this._proxy.ActivateAsync(event.get_root_coords()[1], event.get_root_coords()[2]);
}
secondaryActivate(event: Gdk.Event) {
this._proxy.SecondaryActivateAsync(event.get_root_coords()[1], event.get_root_coords()[2]);
}
scroll(event: Gdk.EventScroll) {
const direction =
(event.direction == 0 || event.direction == 1) ? 'vertical' : 'horizontal';
const delta =
(event.direction == 0 || event.direction == 1) ? event.delta_y : event.delta_x;
this._proxy.ScrollAsync(delta, direction);
}
openMenu(event: Gdk.Event) {
this.menu // DbusmenuGtk3 imports the gdk type from @girs
? (this.menu as unknown as Gtk.Menu).popup_at_pointer(event)
: this._proxy.ContextMenuAsync(event.get_root_coords()[1], event.get_root_coords()[2]);
}
get category() { return this._proxy.Category; }
get id() { return this._proxy.Id; }
get title() { return this._proxy.Title; }
get status() { return this._proxy.Status; }
get windowId() { return this._proxy.WindowId; }
get isMenu() { return this._proxy.ItemIsMenu; }
get tooltipMarkup() {
if (!this._proxy.ToolTip)
return '';
let tooltipMarkup = this._proxy.ToolTip[2];
if (this._proxy.ToolTip[3] !== '')
tooltipMarkup += '\n' + this._proxy.ToolTip[3];
return tooltipMarkup;
}
get icon() {
let icon;
if (this.status === 'NeedsAttention') {
icon = this._proxy.AttentionIconName
? this._proxy.AttentionIconName
: this._getPixbuf(this._proxy.AttentionIconPixmap);
}
else {
icon = this._proxy.IconName
? this._proxy.IconName
: this._getPixbuf(this._proxy.IconPixmap);
}
return icon || 'image-missing';
}
private _itemProxyAcquired(proxy: StatusNotifierItemProxy) {
if (proxy.Menu) {
const menu = Widget({
// @ts-expect-error
type: DbusmenuGtk3.Menu,
dbus_name: proxy.g_name_owner,
dbus_object: proxy.Menu,
});
this.menu = (menu as unknown) as DbusmenuGtk3.Menu;
}
bulkConnect(proxy, [
['notify::g-name-owner', () => {
if (!proxy.g_name_owner)
this.emit('removed', this._busName);
}],
['g-signal', () => {
this._refreshAllProperties();
this.emit('changed');
}],
['g-properties-changed', () => this.emit('changed')],
]);
['Title', 'Icon', 'AttentionIcon', 'OverlayIcon', 'ToolTip', 'Status']
.forEach(prop => proxy.connectSignal(`New${prop}`, () => {
this.emit('changed');
}));
this.emit('ready');
}
private _refreshAllProperties() {
const variant = this._proxy.g_connection.call_sync(
this._proxy.g_name,
this._proxy.g_object_path,
'org.freedesktop.DBus.Properties',
'GetAll',
GLib.Variant.new('(s)', [this._proxy.g_interface_name]),
GLib.VariantType.new('(a{sv})'),
Gio.DBusCallFlags.NONE, -1,
null,
) as GLib.Variant<'(a{sv})'>;
const [properties] = variant.deep_unpack();
Object.entries(properties).map(([propertyName, value]) => {
this._proxy.set_cached_property(propertyName, value);
});
}
private _getPixbuf(pixMapArray: [number, number, Uint8Array][]) {
if (!pixMapArray)
return;
const pixMap = pixMapArray.sort((a, b) => a[0] - b[0]).pop();
if (!pixMap)
return;
const array = Uint8Array.from(pixMap[2]);
for (let i = 0; i < 4 * pixMap[0] * pixMap[1]; i += 4) {
const alpha = array[i];
array[i] = array[i + 1];
array[i + 1] = array[i + 2];
array[i + 2] = array[i + 3];
array[i + 3] = alpha;
}
return GdkPixbuf.Pixbuf.new_from_bytes(
array,
GdkPixbuf.Colorspace.RGB,
true,
8,
pixMap[0],
pixMap[1],
pixMap[0] * 4,
);
}
}
class SystemTrayService extends Service {
static {
Service.register(this, {
'added': ['string'],
'removed': ['string'],
});
}
private _dbus!: Gio.DBusExportedObject;
private _items: Map<string, TrayItem>;
get IsStatusNotifierHostRegistered() { return true; }
get ProtocolVersion() { return 0; }
get RegisteredStatusNotifierItems() { return Array.from(this._items.keys()); }
get items() { return this._items; }
constructor() {
super();
this._items = new Map();
this._register();
}
private _register() {
Gio.bus_own_name(
Gio.BusType.SESSION,
'org.kde.StatusNotifierWatcher',
Gio.BusNameOwnerFlags.NONE,
(connection: Gio.DBusConnection) => {
this._dbus = Gio.DBusExportedObject
.wrapJSObject(StatusNotifierWatcherIFace as string, this);
this._dbus.export(connection, '/StatusNotifierWatcher');
},
null,
() => {
print('Another system tray is already running');
},
);
}
RegisterStatusNotifierItemAsync(serviceName: string[], invocation: Gio.DBusMethodInvocation) {
let busName: string, objectPath: string;
const [service] = serviceName;
if (service.startsWith('/')) {
objectPath = service;
busName = invocation.get_sender();
} else {
busName = service;
objectPath = '/StatusNotifierItem';
}
invocation.return_value(null);
const item = new TrayItem(busName, objectPath);
item.connect('ready', () => {
this._items.set(busName, item);
this.emit('added', busName);
this.emit('changed');
this._dbus.emit_signal(
'StatusNotifierItemRegistered',
new GLib.Variant('(s)', [busName + objectPath]),
);
});
item.connect('removed', () => {
this._items.delete(busName);
this.emit('removed', busName);
this.emit('changed');
this._dbus.emit_signal(
'StatusNotifierItemUnregistered',
new GLib.Variant('(s)', [busName]),
);
});
}
getItem(name: string) {
return this._items.get(name);
}
}
export default class SystemTray {
static _instance: SystemTrayService;
static get instance() {
Service.ensureInstance(SystemTray, SystemTrayService);
return SystemTray._instance;
}
static get items() { return Array.from(SystemTray.instance.items.values()); }
static getItem(name: string) { return SystemTray._instance.getItem(name); }
}

View File

@@ -6,66 +6,56 @@ import { Context } from 'gi-types/cairo1';
export default class AgsIcon extends Gtk.Image {
static {
GObject.registerClass({
GTypeName: 'AgsIcon',
Properties: {
'size': GObject.ParamSpec.int(
'size', 'Size', 'Size',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
0, 1024, 0,
),
'icon': GObject.ParamSpec.string(
'icon', 'Icon', 'Icon',
GObject.ParamFlags.READWRITE | GObject.ParamFlags.CONSTRUCT,
'',
),
},
}, this);
GObject.registerClass({ GTypeName: 'AgsIcon' }, this);
}
constructor(params: object | string) {
constructor(params: object | string | GdkPixbuf.Pixbuf) {
const {
icon = '',
size = 0,
} = params as { icon: string, size: number };
super(typeof params === 'string' ? { icon: params } : params);
...rest
} = params as { icon: string | GdkPixbuf.Pixbuf, size: number };
super(typeof params === 'string' || params instanceof GdkPixbuf.Pixbuf ? {} : rest);
// set correct size after construct
if (typeof params === 'object') {
this.size = size;
this.icon = icon;
}
this.size = size;
this.icon = typeof params === 'string' || params instanceof GdkPixbuf.Pixbuf
? params : icon;
}
_size = 0;
_previousSize = 0;
get size() { return this._size || this._previousSize || 13; }
set size(size: number) {
size ||= 0;
this._size = size;
this.queue_draw();
}
_file = false;
_icon = '';
_type!: 'file' | 'named' | 'pixbuf';
_icon: string | GdkPixbuf.Pixbuf = '';
get icon() { return this._icon; }
set icon(icon: string) {
set icon(icon: string | GdkPixbuf.Pixbuf) {
if (!icon || this._icon === icon)
return;
this._icon = icon;
if (GLib.file_test(icon, GLib.FileTest.EXISTS)) {
this._file = true;
if (typeof icon === 'string') {
if (GLib.file_test(icon, GLib.FileTest.EXISTS)) {
this._type = 'file';
this.set_from_pixbuf(
GdkPixbuf.Pixbuf.new_from_file_at_size(icon, this.size, this.size));
} else {
this._type = 'named';
this.icon_name = icon;
this.pixel_size = this.size;
}
}
else if (icon instanceof GdkPixbuf.Pixbuf) {
this._type = 'pixbuf';
this.set_from_pixbuf(
GdkPixbuf.Pixbuf.new_from_file_at_size(
icon, this.size, this.size,
),
);
icon.scale_simple(this.size, this.size, GdkPixbuf.InterpType.BILINEAR));
}
else {
this._file = false;
this.icon_name = icon;
this.pixel_size = this.size;
logError(new Error(`expected Pixbuf or string for icon, but got ${typeof icon}`));
}
}
@@ -81,12 +71,20 @@ export default class AgsIcon extends Gtk.Image {
this._previousSize = size;
if (this._file) {
this.set_from_pixbuf(
GdkPixbuf.Pixbuf.new_from_file_at_size(this.icon, size, size),
);
} else {
this.pixel_size = size;
switch (this._type) {
case 'file':
this.set_from_pixbuf(
GdkPixbuf.Pixbuf.new_from_file_at_size(this.icon as string, size, size));
break;
case 'pixbuf':
this.set_from_pixbuf((this.icon as GdkPixbuf.Pixbuf).scale_simple(
this.size, this.size, GdkPixbuf.InterpType.BILINEAR));
break;
case 'named':
this.set_pixel_size(size);
break;
default:
break;
}
return super.vfunc_draw(cr);

View File

@@ -67,7 +67,8 @@ export class AgsMenuItem extends Gtk.MenuItem {
onSelect = '',
onDeselect = '',
...rest
}: { [key: string]: Command }) {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}: { [key: string]: any }) {
super(rest);
this.onActivate = onActivate;

View File

@@ -126,20 +126,16 @@ export default class AgsWindow extends Gtk.Window {
switch (margin.length) {
case 1:
margins = [
['TOP', 0], ['RIGHT', 0], ['BOTTOM', 0], ['LEFT', 0]];
margins = [['TOP', 0], ['RIGHT', 0], ['BOTTOM', 0], ['LEFT', 0]];
break;
case 2:
margins = [
['TOP', 0], ['RIGHT', 1], ['BOTTOM', 0], ['LEFT', 1]];
margins = [['TOP', 0], ['RIGHT', 1], ['BOTTOM', 0], ['LEFT', 1]];
break;
case 3:
margins = [
['TOP', 0], ['RIGHT', 1], ['BOTTOM', 2], ['LEFT', 1]];
margins = [['TOP', 0], ['RIGHT', 1], ['BOTTOM', 2], ['LEFT', 1]];
break;
case 4:
margins = [
['TOP', 0], ['RIGHT', 1], ['BOTTOM', 2], ['LEFT', 3]];
margins = [['TOP', 0], ['RIGHT', 1], ['BOTTOM', 2], ['LEFT', 3]];
break;
default:
break;

39
types/ambient.d.ts vendored
View File

@@ -4,41 +4,41 @@ declare function log(msg: string, subsitutions?: any[]): void;
declare function logError(err: Error, msg?: string): void;
declare const pkg: {
version: string;
name: string;
version: string;
name: string;
};
declare const imports: {
config: any;
gi: any;
searchPath: string[];
config: any;
gi: any;
searchPath: string[];
}
declare module console {
export function error(obj: object, others?: object[]): void;
export function error(msg: string, subsitutions?: any[]): void;
export function error(obj: object, others?: object[]): void;
export function error(msg: string, subsitutions?: any[]): void;
}
declare interface String {
format(...replacements: string[]): string;
format(...replacements: number[]): string;
format(...replacements: string[]): string;
format(...replacements: number[]): string;
}
declare interface Number {
toFixed(digits: number): number;
toFixed(digits: number): number;
}
declare class TextDecoder {
constructor(label?: string, options?: TextDecoderOptions);
decode(input?: BufferSource, options?: TextDecodeOptions): string;
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
constructor(label?: string, options?: TextDecoderOptions);
decode(input?: BufferSource, options?: TextDecodeOptions): string;
readonly encoding: string;
readonly fatal: boolean;
readonly ignoreBOM: boolean;
}
declare class TextEncoder {
constructor();
encode(input?: string): Uint8Array;
constructor();
encode(input?: string): Uint8Array;
}
declare module 'gi://Gvc' {
@@ -50,3 +50,8 @@ declare module 'gi://NM' {
import NM10 from '@girs/nm-1.0';
export default NM10;
}
declare module 'gi://DbusmenuGtk3' {
import Dbusmenu from '@girs/dbusmenugtk3-0.4';
export default Dbusmenu;
}