├── .eslintrc.yml
├── LICENSES.md
├── Makefile
├── README.md
├── docs
├── hyprland.md
├── niri.md
├── sample-config
│ ├── extensions
│ │ └── rice
│ │ │ └── extension.js
│ ├── settings.json
│ └── style.css
└── sway.md
├── gws
├── gws-apps
├── gws-console
├── gws-prefs
├── gws-search
├── lint
├── eslintrc-gjs.yml
└── eslintrc-shell.yml
├── prefs
├── main.js
├── style.css
└── ui
│ ├── apps-grid.ui
│ ├── dock.ui
│ ├── general.ui
│ ├── icons
│ └── hicolor
│ │ └── scalable
│ │ └── actions
│ │ ├── add-window-symbolic.svg
│ │ ├── applications-symbolic.svg
│ │ ├── block-symbolic.svg
│ │ ├── bottom-panel-symbolic.svg
│ │ ├── dash-symbolic.svg
│ │ ├── dock-to-bottom-symbolic.svg
│ │ ├── extension-symbolic.svg
│ │ ├── frame-symbolic.svg
│ │ ├── general-symbolic.svg
│ │ ├── heart-filled-symbolic.svg
│ │ ├── other-symbolic.svg
│ │ ├── overview-symbolic.svg
│ │ ├── pageview-symbolic.svg
│ │ ├── pulse-symbolic.svg
│ │ ├── remove-window-symbolic.svg
│ │ ├── reset-symbolic.svg
│ │ ├── search-symbolic.svg
│ │ ├── select-mode-symbolic.svg
│ │ ├── select-window-symbolic.svg
│ │ └── toolbar-symbolic.svg
│ ├── menu.ui
│ ├── others.ui
│ ├── panel-row.ui
│ ├── panel.ui
│ ├── popups.ui
│ ├── search.ui
│ ├── services.ui
│ ├── tweaks.ui
│ └── window.ui
├── schemas
├── com.github.icedman.gws.gschema.xml
└── gschemas.compiled
├── screenshots
├── screenshot-2024-12-11-01.png
└── screenshot-2024-12-26-01.png
├── settings.json
├── src
├── app.js
├── appsGrid.js
├── compositors
│ ├── dwl.js
│ ├── hyprland.js
│ ├── niri.js
│ ├── sway.js
│ └── wmInterface.js
├── dock.js
├── extensions
│ ├── bar-items
│ │ ├── audio.js
│ │ ├── clock.js
│ │ ├── extension.js
│ │ ├── network.js
│ │ ├── power.js
│ │ ├── shutdown.js
│ │ ├── stats.js
│ │ ├── style.css
│ │ └── ui
│ │ │ ├── brightness.ui
│ │ │ ├── calendar.ui
│ │ │ ├── network.ui
│ │ │ ├── power.ui
│ │ │ └── volume.ui
│ ├── console
│ │ ├── extension.js
│ │ └── style.css
│ └── dock-items
│ │ └── extension.js
├── lib
│ ├── appInfo.js
│ ├── background.js
│ ├── collisions.js
│ ├── devices.js
│ ├── dock.js
│ ├── dockItem.js
│ ├── dot.js
│ ├── drawing.js
│ ├── environment.js
│ ├── extensionInterface.js
│ ├── factory.js
│ ├── fileUtils.js
│ ├── gnomeShellMonkeyPatches.js
│ ├── iconInfo.js
│ ├── ipc.js
│ ├── misc.js
│ ├── popupMenu.js
│ ├── signalTracker.js
│ ├── signals.js
│ └── timer.js
├── main.js
├── panel.js
├── popups.js
├── search.js
├── services
│ ├── bluetooth.js
│ ├── brightness.js
│ ├── dbus.js
│ ├── fuzzy-app-search
│ │ ├── README.md
│ │ ├── fileUtils.js
│ │ ├── indexUtils.js
│ │ ├── metadata.js
│ │ ├── scorer.js
│ │ ├── search.js
│ │ └── tokenizer.js
│ ├── inhibitor.js
│ ├── login1.js
│ ├── monitors.js
│ ├── mounts.js
│ ├── network.js
│ ├── power.js
│ ├── powerProfiles.js
│ ├── remoteSearch.js
│ ├── style.js
│ ├── systemActions.js
│ ├── systemApps.js
│ ├── systemStats.js
│ ├── trash.js
│ └── volume.js
├── style.css
├── ui
│ ├── apps.ui
│ ├── icons
│ │ └── hicolor
│ │ │ └── scalable
│ │ │ ├── places
│ │ │ ├── archlinux-symbolic.svg
│ │ │ ├── debian-symbolic.svg
│ │ │ ├── fedora-symbolic.svg
│ │ │ ├── kalilinux-symbolic.svg
│ │ │ ├── linuxmint-symbolic.svg
│ │ │ ├── manjaro-symbolic.svg
│ │ │ ├── ubuntu-symbolic.svg
│ │ │ └── zorin-symbolic.svg
│ │ │ └── status
│ │ │ ├── caffeine-off-symbolic.svg
│ │ │ ├── caffeine-on-symbolic.svg
│ │ │ ├── cpu-alt-symbolic.svg
│ │ │ ├── cpu-symbolic.svg
│ │ │ ├── hard-disk-symbolic.svg
│ │ │ ├── hard-drive-2-symbolic.svg
│ │ │ ├── hard-drive-symbolic.svg
│ │ │ ├── memory-symbolic.svg
│ │ │ ├── storage-symbolic.svg
│ │ │ ├── triangle-down-symbolic.svg
│ │ │ ├── triangle-left-symbolic.svg
│ │ │ ├── triangle-right-symbolic.svg
│ │ │ └── triangle-up-symbolic.svg
│ ├── result-row.ui
│ └── search.ui
├── user-extensions
├── wallpaper.js
└── windowManager.js
└── tests
├── bluetooth.js
├── clipboard.js
├── dbus
├── dbus.c
├── ff.js
├── grid.js
├── icons.js
├── search.js
├── search.sh
├── style.css
├── system.js
├── test.sh
├── tests.js
└── tests.md
/.eslintrc.yml:
--------------------------------------------------------------------------------
1 | extends:
2 | - ./lint/eslintrc-gjs.yml
3 | - ./lint/eslintrc-shell.yml
4 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | all: build install lint
2 |
3 | .PHONY: build install
4 |
5 | build:
6 | glib-compile-schemas --strict --targetdir=schemas/ schemas
7 |
8 | install:
9 | mkdir -p ~/.local/share/glib-2.0/schemas
10 | cp ./schemas/*.compiled ~/.local/share/glib-2.0/schemas
11 |
12 | lint:
13 | eslint ./
14 |
15 | xml-lint:
16 | find . -name "*.ui" -type f -exec xmllint --output '{}' --format '{}' \;
17 |
18 | pretty: xml-lint
19 | prettier --single-quote --write "**/*.js"
20 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
GJS Wayland Shell (GWS)
4 |
5 |
6 | A Gnome-based shell over wayland - with macOS-like dash Animation
7 |
8 |
9 |
10 |
11 |
12 | [](https://www.buymeacoffee.com/icedman)
13 |
14 | 
15 |
16 | # Requirements
17 | 1. wayland window manager/compositor
18 | * niri
19 | * sway
20 | * hyprland
21 |
22 | need patching for DBus access to windows list and events
23 | * labwc (https://github.com/icedman/labwc/tree/dbus-patch)
24 | * dwl (https://codeberg.org/icedman/dwl/src/branch/dbus-patch)
25 |
26 | 2. gtk4-layer-shell-devel
27 | Fedora:
28 | ```sudo dnf install gtk4-layer-shell-devel```
29 |
30 | Arch:
31 | ```sudo dnf install gtk4-layer-shell```
32 |
33 | 3. gnome-shell
34 | ```sudo dnf install gnome-shell```
35 |
36 | 4. udiskie (optiona) for volume mount monitor
37 | ```sudo dnf install udiskie```
38 |
39 | # Installation
40 |
41 | ```git clone https://github.com/icedman/gjs-wayland-shell```
42 |
43 | # Run
44 |
45 | Run from terminal or from niri, sway, hyprland autostart config
46 |
47 | ```sh
48 | $ gws
49 | ```
50 |
51 | # Features
52 |
53 | * Dash
54 | * Top Panel
55 | * Apps Grid
56 | * Search Light
57 | * animated icons
58 | * running apps indicator
59 | * dynamic trash icons
60 | * mount icons
61 |
62 | # Features in development
63 | * autohide (partly done)
64 |
65 | # Search Light
66 |
67 | Map the search app to your hotkey with your window manager. Or run the app from terminal.
68 |
69 | ```sh
70 | $ gws-search
71 | ```
72 |
73 | # Apps Grid
74 |
75 | Map the apps grid to your hotkey with your window manager. Or run the app from terminal.
76 |
77 | ```sh
78 | $ gws-apps
79 | ```
80 |
81 | Functions the same way as Gnome Shell's search, using DBus querying SearchProviders.
82 |
83 | # Panel Items
84 |
85 | * logo
86 | * clock
87 | * inhibitor
88 | * network
89 | * power
90 | * volume
91 | * mic
92 | * brightness
93 |
94 | # Dock Items
95 |
96 | * favorite apps
97 | * running apps
98 | * trash
99 | * separator
100 | * volumes mounted
101 |
102 | # Config directory
103 |
104 | # Customize CSS
105 |
106 | Copy ```gws``` from in ```docs/sample-config``` to the ```~/.config``` directory
107 |
108 | Edit custom css at ```~/.config/gws/style.css```
109 |
110 | sample css:
111 |
112 | ```css
113 |
114 | #Dock { /* the dock */ }
115 | #Bar { /* the topbar */ }
116 |
117 | #Dock #container,
118 | #Bar #container { /* set the background color and borders */ }
119 |
120 | /* customize parts of the dock or bar */
121 | #Dock #container #lead,
122 | #Dock #container #center,
123 | #Dock #container #trail {
124 | }
125 |
126 | ```
127 |
128 | # Customize Settings
129 |
130 | Settings are handled via gnome dconf. A preferences app is provided for safer edits.
131 |
132 | ```sh
133 | $ gws-prefs
134 | ```
135 |
136 | Alternatively, dconf settings may be overriden by edit a settings file at ```~/.config/gws/settings.json```
137 |
138 | sample config:
139 |
140 | ```json
141 | {
142 | "favorite-apps": [
143 | "kitty.desktop",
144 | "org.gnome.Nautilus.desktop",
145 | "google-chrome.desktop",
146 | "org.mozilla.firefox.desktop",
147 | "org.gnome.Calendar.desktop",
148 | "org.gnome.clocks.desktop",
149 | "org.gnome.Software.desktop",
150 | "org.gnome.TextEditor.desktop"
151 | ],
152 | "baritems-lead-items": ["logo"],
153 | "baritems-center-items": ["clock"],
154 | "baritems-trail-items": ["network", "power", "volume", "mic"]
155 | }
156 | ```
157 |
158 | # Extension/Rice
159 |
160 | The sample extension at the ```docs``` folder. (Requires coding - and studying the gws code)
161 |
162 | # Why is gnome-shell required?
163 |
164 | * This shell re-uses some of gnome-shell's dbus interfaces
165 | * This shell re-uses a lot of gnome-shell's code
166 | * gnome-shell need not be running
167 |
168 | # Debugging
169 |
170 | To show a console like gnome-shell's looking glass the browse console:
171 |
172 | ```sh DEBUG_CONSOLE=1 gws```
173 |
174 | Query the Main object
175 |
176 | ```js
177 | JSON.stringify(Object.keys(Main));
178 | ```
179 |
180 | Query the Dock
181 |
182 | ```js
183 | JSON.stringify(Main.dock, null, 4);
184 | ```
185 |
186 | Query the Panel
187 |
188 | ```js
189 | JSON.stringify(Main.panel, null, 4);
190 | ```
191 |
192 | # More screen shots
193 |
194 | 
--------------------------------------------------------------------------------
/docs/hyprland.md:
--------------------------------------------------------------------------------
1 | # Hyrpland
2 |
3 | ### Windows Updated
4 | ```json
5 | {
6 | "event": "windows-update",
7 | "windows": [
8 | {
9 | "address": "0x55e0c6e75980",
10 | "mapped": true,
11 | "hidden": false,
12 | "at": [
13 | 22,
14 | 46
15 | ],
16 | "size": [
17 | 1556,
18 | 863
19 | ],
20 | "workspace": {
21 | "id": 1,
22 | "name": "1"
23 | },
24 | "floating": false,
25 | "pseudo": false,
26 | "monitor": 0,
27 | "class": "kitty",
28 | "title": "./tests/test.sh",
29 | "initialClass": "kitty",
30 | "initialTitle": "kitty",
31 | "pid": 716768,
32 | "xwayland": false,
33 | "pinned": false,
34 | "fullscreen": 0,
35 | "fullscreenClient": 0,
36 | "grouped": [],
37 | "tags": [],
38 | "swallowing": "0x0",
39 | "focusHistoryID": 0,
40 | "id": "55e0c6e75980"
41 | },
42 | {
43 | "address": "0x55e0c6e557f0",
44 | "mapped": true,
45 | "hidden": false,
46 | "at": [
47 | 22,
48 | 46
49 | ],
50 | "size": [
51 | 1556,
52 | 863
53 | ],
54 | "workspace": {
55 | "id": 2,
56 | "name": "2"
57 | },
58 | "floating": false,
59 | "pseudo": false,
60 | "monitor": 0,
61 | "class": "sublime_text",
62 | "title": "~/Developer/gnome/gjs-wayland-shell/docs/hyprland.md (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
63 | "initialClass": "sublime_text",
64 | "initialTitle": "~/Developer/gnome/gjs-wayland-shell/docs/sway.md (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
65 | "pid": 717410,
66 | "xwayland": false,
67 | "pinned": false,
68 | "fullscreen": 0,
69 | "fullscreenClient": 0,
70 | "grouped": [],
71 | "tags": [],
72 | "swallowing": "0x0",
73 | "focusHistoryID": 1,
74 | "id": "55e0c6e557f0"
75 | }
76 | ],
77 | "raw": [
78 | {
79 | "address": "0x55e0c6e75980",
80 | "mapped": true,
81 | "hidden": false,
82 | "at": [
83 | 22,
84 | 46
85 | ],
86 | "size": [
87 | 1556,
88 | 863
89 | ],
90 | "workspace": {
91 | "id": 1,
92 | "name": "1"
93 | },
94 | "floating": false,
95 | "pseudo": false,
96 | "monitor": 0,
97 | "class": "kitty",
98 | "title": "./tests/test.sh",
99 | "initialClass": "kitty",
100 | "initialTitle": "kitty",
101 | "pid": 716768,
102 | "xwayland": false,
103 | "pinned": false,
104 | "fullscreen": 0,
105 | "fullscreenClient": 0,
106 | "grouped": [],
107 | "tags": [],
108 | "swallowing": "0x0",
109 | "focusHistoryID": 0,
110 | "id": "55e0c6e75980"
111 | },
112 | {
113 | "address": "0x55e0c6e557f0",
114 | "mapped": true,
115 | "hidden": false,
116 | "at": [
117 | 22,
118 | 46
119 | ],
120 | "size": [
121 | 1556,
122 | 863
123 | ],
124 | "workspace": {
125 | "id": 2,
126 | "name": "2"
127 | },
128 | "floating": false,
129 | "pseudo": false,
130 | "monitor": 0,
131 | "class": "sublime_text",
132 | "title": "~/Developer/gnome/gjs-wayland-shell/docs/hyprland.md (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
133 | "initialClass": "sublime_text",
134 | "initialTitle": "~/Developer/gnome/gjs-wayland-shell/docs/sway.md (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
135 | "pid": 717410,
136 | "xwayland": false,
137 | "pinned": false,
138 | "fullscreen": 0,
139 | "fullscreenClient": 0,
140 | "grouped": [],
141 | "tags": [],
142 | "swallowing": "0x0",
143 | "focusHistoryID": 1,
144 | "id": "55e0c6e557f0"
145 | }
146 | ]
147 | }
148 | ```
149 |
150 | ### Window Opened
151 | ```json
152 | {
153 | "event": "window-opened",
154 | "window": {
155 | "id": "564cfb99c4f0"
156 | },
157 | "raw": [
158 | "openwindow",
159 | "564cfb99c4f0",
160 | "1",
161 | "kitty",
162 | "kitty"
163 | ]
164 | }
165 | ```
166 |
167 | ### Window Closed
168 | ```json
169 | {
170 | "event": "window-closed",
171 | "window": {
172 | "id": "564cfb99c4f0"
173 | },
174 | "raw": [
175 | "closewindow",
176 | "564cfb99c4f0"
177 | ]
178 | }
179 | ```
180 |
181 | ### Window Focused
182 | ```json
183 | {
184 | "event": "window-focused",
185 | "window": {
186 | "id": "564cfb777a10"
187 | },
188 | "raw": [
189 | "activewindowv2",
190 | "564cfb777a10"
191 | ]
192 | }
193 | ```
194 |
195 | ### Workspace
196 | ```json
197 | {
198 | "event": "unhandled",
199 | "raw": "workspace>>1"
200 | }
201 | ```
202 |
203 | ```json
204 | {
205 | "event": "unhandled",
206 | "raw": "workspacev2>>1,1"
207 | }
208 | ```
--------------------------------------------------------------------------------
/docs/niri.md:
--------------------------------------------------------------------------------
1 | # Niri
2 |
3 | ### Windows Updated
4 | ```json
5 | {
6 | "event": "windows-update",
7 | "windows": [
8 | {
9 | "id": 1,
10 | "title": "./tests/test.sh",
11 | "app_id": "kitty",
12 | "pid": 700901,
13 | "workspace_id": 1,
14 | "is_focused": true
15 | },
16 | {
17 | "id": 2,
18 | "title": "~/Developer/gnome/gjs-wayland-shell/src/compositors/niri.js (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
19 | "app_id": "sublime_text",
20 | "pid": 701529,
21 | "workspace_id": 1,
22 | "is_focused": false
23 | }
24 | ],
25 | "raw": {
26 | "Ok": {
27 | "Windows": [
28 | {
29 | "id": 1,
30 | "title": "./tests/test.sh",
31 | "app_id": "kitty",
32 | "pid": 700901,
33 | "workspace_id": 1,
34 | "is_focused": true
35 | },
36 | {
37 | "id": 2,
38 | "title": "~/Developer/gnome/gjs-wayland-shell/src/compositors/niri.js (gjs-wayland-shell) - Sublime Text (UNREGISTERED)",
39 | "app_id": "sublime_text",
40 | "pid": 701529,
41 | "workspace_id": 1,
42 | "is_focused": false
43 | }
44 | ]
45 | }
46 | }
47 | }
48 | ```
49 |
50 | ### Window Opened
51 | ```json
52 | {
53 | "event": "window-opened",
54 | "window": {
55 | "id": 3,
56 | "title": "iceman@fedora:~/Developer/gnome/gjs-wayland-shell",
57 | "app_id": "kitty",
58 | "pid": 702023,
59 | "workspace_id": 1,
60 | "is_focused": true
61 | },
62 | "raw": {
63 | "WindowOpenedOrChanged": {
64 | "window": {
65 | "id": 3,
66 | "title": "iceman@fedora:~/Developer/gnome/gjs-wayland-shell",
67 | "app_id": "kitty",
68 | "pid": 702023,
69 | "workspace_id": 1,
70 | "is_focused": true
71 | }
72 | }
73 | }
74 | }
75 | ```
76 |
77 | ### Window Closed
78 | ```json
79 | {
80 | "event": "window-closed",
81 | "window": {
82 | "id": 6,
83 | "raw": {
84 | "WindowClosed": {
85 | "id": 6
86 | }
87 | }
88 | }
89 | }
90 | ```
91 |
92 | ### Window Focused
93 | ```json
94 | {
95 | "event": "window-focused",
96 | "window": {
97 | "id": 3
98 | },
99 | "raw": {
100 | "WindowFocusChanged": {
101 | "id": 3
102 | }
103 | }
104 | }
105 | ```
106 |
107 | ### Workspace
108 | ```json
109 | {
110 | "event": "success",
111 | "raw": {
112 | "WorkspaceActivated": {
113 | "id": 2,
114 | "focused": true
115 | }
116 | }
117 | }
118 | ```
--------------------------------------------------------------------------------
/docs/sample-config/extensions/rice/extension.js:
--------------------------------------------------------------------------------
1 | import Gtk from 'gi://Gtk';
2 | import GObject from 'gi://GObject';
3 |
4 | const Extension = Main.imports.Extension;
5 |
6 | const Rice = GObject.registerClass(
7 | {},
8 | class Rice extends Extension {
9 | enable() {
10 | super.enable();
11 |
12 | // override apps at ~/.config/gws/settings.json
13 | /*
14 | {
15 | "favorite_apps": [
16 | "kitty.desktop",
17 | "org.gnome.Nautilus.desktop",
18 | "google-chrome.desktop",
19 | "org.mozilla.firefox.desktop",
20 | "org.gnome.Calendar.desktop",
21 | "org.gnome.clocks.desktop",
22 | "org.gnome.Software.desktop",
23 | "org.gnome.TextEditor.desktop"
24 | ]
25 | }
26 | */
27 | // Main.extensions['dock-items'].favorite_apps = Main.customSettings?.favorite_apps ?? favorite_apps;
28 | // Main.extensions['dock-items'].reattachDockItems();
29 | }
30 |
31 | _enable() {
32 | super.enable();
33 |
34 | this.dock = new Main.imports.Dock.DockPanel({
35 | name: 'Dock',
36 | customSettings: {
37 | 'dock-location': 1,
38 | 'edge-distance': 0,
39 | },
40 | });
41 | {
42 | this.dockItem = new Main.imports.Dock.DockItem({
43 | app: 'kitty.desktop',
44 | });
45 | // this.dock.trail.append(this.dockItem);
46 | this.dock.center.append(this.dockItem);
47 | Main.extensions['dock-items'].createTrashItem(this.dock.center);
48 | }
49 | this.dock.present();
50 |
51 | {
52 | // let center = new Gtk.Box();
53 | // let dockItem = new Main.imports.Dock.DockItem({app:'kitty.desktop'});
54 | // center.append(dockItem);
55 | // let panelItem = new Main.imports.Dock.PanelItem();
56 | // panelItem.set_label('hellow');
57 | // center.append(panelItem);
58 | // Main.dock.lead.append(center);
59 | }
60 |
61 | // Main.dock.window.leadSpacer.visible = false;
62 |
63 | {
64 | this.dockItem = new Main.imports.Dock.DockItem({
65 | app: 'kitty.desktop',
66 | });
67 | Main.dock.trail.append(this.dockItem);
68 | Main.dock.trail.add_css_class('icons-container');
69 | }
70 | // for(let i=0;i<8;i++)
71 | {
72 | Main.extensions['dock-items'].createRunningApps(Main.dock.lead);
73 | this.dockItem = new Main.imports.Dock.DockItem({
74 | app: 'kitty.desktop',
75 | });
76 | Main.dock.lead.append(this.dockItem);
77 | Main.dock.lead.add_css_class('icons-container');
78 | }
79 |
80 | Main.dock.window.queue_resize();
81 | }
82 | disable() {
83 | super.disable();
84 | }
85 | },
86 | );
87 |
88 | export default Rice;
89 |
--------------------------------------------------------------------------------
/docs/sample-config/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "favorite-apps": [
3 | "kitty.desktop",
4 | "org.gnome.Nautilus.desktop",
5 | "google-chrome.desktop",
6 | "org.mozilla.firefox.desktop",
7 | "com.spotify.Client.desktop",
8 | "org.gnome.Calendar.desktop",
9 | "org.gnome.clocks.desktop",
10 | "org.gnome.Software.desktop",
11 | "org.gnome.TextEditor.desktop"
12 | ],
13 | "dock-items-lead": [
14 |
15 | ],
16 | "panel-items-lead": [
17 | {
18 | "id": "logo",
19 | "widget": "logo",
20 | "showOSName": false,
21 | "showShortOSName": true,
22 | "hideIcon": false
23 | },
24 | "search"
25 | ],
26 | "panel-items": [
27 | {
28 | "id": "clock-date",
29 | "icon": "",
30 | "widget": "clock",
31 | "format": " %a, %b %d, %Y",
32 | "-format": " %a, %b %d, %Y %H%:%M %p ",
33 | "interval": 60000
34 | },
35 | {
36 | "id": "clock-time",
37 | "icon": "",
38 | "widget": "clock",
39 | "format": " %H%:%M %p ",
40 | "interval": 750
41 | }
42 | ],
43 | "panel-items-trail": [
44 | {
45 | "id": "inhibitor",
46 | "icons": [
47 | "caffeine-off",
48 | "caffeine-on"
49 | ]
50 | },
51 | {
52 | "id": "brightness",
53 | "format": "%P%",
54 | "icons": [ "display-brightness-symbolic" ]
55 | },
56 | {
57 | "id": "arrow-inhibitor-network",
58 | "-widget": "icon-label",
59 | "icon": "triangle-left"
60 | },
61 | "network",
62 | "--bluetooth",
63 | {
64 | "id": "arrow-network-power",
65 | "-widget": "icon-label",
66 | "icon": "triangle-left"
67 | },
68 | {
69 | "id": "power",
70 | "width": 80,
71 | "format": "%P%",
72 | "formatAlt": "%P%",
73 | "formatAltToEmpty": "%P% %H:%M battery remaining",
74 | "formatAltToFull": "%P% %H:%M left charging time"
75 | },
76 | {
77 | "id": "volume",
78 | "format": "%P%"
79 | },
80 | "mic",
81 | {
82 | "id": "disk-stats-root",
83 | "widget": "disk-stats",
84 | "format": "%Mounted %Used/%Size (%Use%)",
85 | "on": "/"
86 | },
87 | {
88 | "id": "memory-stats",
89 | "format": "%usagePercent% %swapUsagePercent% ",
90 | "width": 84
91 | },
92 | {
93 | "id": "cpu-stats",
94 | "width": 84
95 | },
96 | {
97 | "id": "shutdown",
98 | "icon": "system-shutdown-symbolic"
99 | }
100 | ]
101 | }
102 |
--------------------------------------------------------------------------------
/docs/sample-config/style.css:
--------------------------------------------------------------------------------
1 | #Dock button image {
2 | filter: sepia(50%);
3 | /* filter: grayscale(90%); */
4 | /* filter: contrast(120%);*/
5 | /* transform: scale(1.5);*/
6 | }
7 |
8 | #Dock #container {
9 | border: 2px solid red;
10 | }
11 | #Dock:hover #container {
12 | transform: translateY(0px);
13 | }
14 |
15 | #Dock #container {
16 | border-left: 0px;
17 | border-right: 0px;
18 | border-bottom: 0px;
19 | }
20 |
21 | #Bar #container {
22 | border-bottom: 2px solid yellow;
23 | }
24 |
25 | #Search {
26 | filter: blur(2px);
27 | /* filter: sepia(50%);*/
28 | /* filter: grayscale(90%);*/
29 | /* filter: contrast(120%);*/
30 | }
31 |
--------------------------------------------------------------------------------
/gws:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd "$(dirname "$0")/src"
4 | mkdir -p ~/.local/share/glib-2.0/schemas
5 | cp ../schemas/*.compiled ~/.local/share/glib-2.0/schemas
6 | export GI_TYPELIB_PATH=/usr/lib64/gnome-shell
7 | export LD_LIBRARY_PATH=/usr/lib64/gnome-shell
8 | export LD_PRELOAD=/usr/lib64/libgtk4-layer-shell.so
9 | gjs -m ./main.js
--------------------------------------------------------------------------------
/gws-apps:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | gdbus call --session --dest=com.github.icedman.gws.controller --object-path=/com/github/icedman/gws/controller --method=com.github.icedman.gws.controller.show_apps
--------------------------------------------------------------------------------
/gws-console:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | gdbus call --session --dest=com.github.icedman.gws.controller --object-path=/com/github/icedman/gws/controller --method=com.github.icedman.gws.controller.show_console
--------------------------------------------------------------------------------
/gws-prefs:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd "$(dirname "$0")/prefs"
4 | gjs -m ./main.js
--------------------------------------------------------------------------------
/gws-search:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | gdbus call --session --dest=com.github.icedman.gws.controller --object-path=/com/github/icedman/gws/controller --method=com.github.icedman.gws.controller.show_search
--------------------------------------------------------------------------------
/lint/eslintrc-shell.yml:
--------------------------------------------------------------------------------
1 | rules:
2 | camelcase:
3 | - error
4 | - properties: never
5 | allow: [^vfunc_, ^on_]
6 | object-curly-spacing:
7 | - error
8 | - always
9 | prefer-arrow-callback: error
10 | globals:
11 | global: readonly
12 |
--------------------------------------------------------------------------------
/prefs/style.css:
--------------------------------------------------------------------------------
1 | .dark * {
2 | border-color: rgba(0,0,0,0.25);
3 | color: rgba(175,175,175,1);
4 | }
5 |
6 | .light * {
7 | border-color: rgba(125,125,125,0.5);
8 | color: rgba(50,50,50,1);
9 | }
10 |
11 | preferencesgroup {
12 | margin: 10px;
13 | }
14 |
15 | preferencesgroup .header {
16 | padding: 12px;
17 | }
18 |
19 | preferencesgroup .dim-label {
20 | padding: 0px 12px 12px 12px;
21 | }
22 |
23 | .panel-row image * {
24 | /* fill: white;*/
25 | }
26 | .panel-row image {
27 | margin-top: 4px;
28 | margin-bottom: 4px;
29 | margin-left: 8px;
30 | margin-right: 8px;
31 | }
32 |
33 | .panel-row {
34 | padding: 4px;
35 | /* padding-left: 0px;*/
36 | /* padding-right: 0px;*/
37 | border-radius: 6px;
38 | }
39 | .panel-row.active .panel-box,
40 | .panel-row:hover .panel-box {
41 | background: rgba(100, 100, 100, 0.5);
42 | }
43 |
44 | /*.header {*/
45 | /* border: 2px solid red;*/
46 | /*}*/
47 |
48 | /*.adwviewswitcherbutton {
49 | border-width: 1px;
50 | border-radius: 0px;
51 | border-right: 0px;
52 | }
53 |
54 | .adwviewswitcher .adwviewswitcherbutton:last-child {
55 | border-right: 1px;
56 | }
57 | */
58 |
59 | /*
60 | .adwviewswitcher > * {
61 | border-right: 0px;
62 | }
63 |
64 | .adwviewswitcher > :first-child {
65 | border-radius: 8px 0 0 8px;
66 | }
67 |
68 | .adwviewswitcher > :last-child {
69 | border-radius: 0 8px 8px 0;
70 | }
71 |
72 | .dark .adwviewswitcher > :last-child {
73 | border-right: 1px solid rgba(0,0,0,0.25);
74 | }
75 |
76 | .light .adwviewswitcher > :last-child {
77 | border-right: 1px solid rgba(125,125,125,0.5);
78 | }*/
79 |
80 | /*
81 | .adwheaderbar .gtktogglebutton {
82 | background: rgba(0,0,0,0);
83 | border-color: rgba(0,0,0,0);
84 | }
85 | .dark .adwheaderbar .gtktogglebutton:hover {
86 | background: rgba(0,0,0,0.25);
87 | }
88 | .light .adwheaderbar .gtktogglebutton:hover {
89 | background: rgba(125,125,125,0.5);
90 | }
91 | */
92 |
93 | .adwviewswitcher > * {
94 | border: 0px;
95 | }
96 |
97 | .adwviewswitcherbutton {
98 | padding: 0px;
99 | }
100 |
101 | .adwviewswitcherbutton .gtkimage,
102 | .adwviewswitcherbutton .gtklabel {
103 | padding: 4px;
104 | }
105 |
106 | .adwheaderbar .gtktogglebutton {
107 | border: 0px;
108 | }
109 | /*
110 | .header_bar * {
111 | border: 0px;
112 | background: transparent;
113 | }
114 | */
115 | /*theme_view_bg_color*/
116 | /*theme_sidebar_bg_color*/
117 |
118 | .sidebar-pane .background {
119 | background-color: @theme_sidebar_bg_color;
120 | }
121 |
122 | .sidebar-pane button {
123 | border: 0px;
124 | }
125 | .sidebar-pane button {
126 | background: transparent;
127 | }
128 |
129 | .content-pane .view {
130 | background-color: shade(@theme_base_color, 0.6); /* 0.8 makes it darker */
131 | }
132 |
133 | headerbar {
134 | background: transparent;
135 | border: 0px;
136 | }
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/add-window-symbolic.svg:
--------------------------------------------------------------------------------
1 |
5 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/applications-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/block-symbolic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
45 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/dash-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
45 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/dock-to-bottom-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/extension-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/frame-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/general-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
65 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/heart-filled-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
41 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/other-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/overview-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
57 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/pageview-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/pulse-symbolic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/remove-window-symbolic.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/reset-symbolic.svg:
--------------------------------------------------------------------------------
1 |
8 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/search-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/select-mode-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/select-window-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
--------------------------------------------------------------------------------
/prefs/ui/icons/hicolor/scalable/actions/toolbar-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/prefs/ui/menu.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
36 |
40 |
41 |
42 |
--------------------------------------------------------------------------------
/prefs/ui/panel-row.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
29 |
30 |
--------------------------------------------------------------------------------
/prefs/ui/services.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | appearance
5 | Services
6 | dash-symbolic
7 |
8 |
9 | Services
10 |
11 |
12 |
13 |
14 | These services may take system resources. Enable only ones that are needed. Requires starting the Shell.
15 |
16 |
17 |
18 |
19 | System Stats
20 | Monitor system stats. Used by disk, cpu, and memory Panel indicators.
21 | service-system-stats
22 |
23 |
24 | center
25 |
26 |
27 |
28 |
29 |
30 |
31 | Trash
32 | Monitor trash for dynamic trash icon. Used by the Dash.
33 | service-trash
34 |
35 |
36 | center
37 |
38 |
39 |
40 |
41 |
42 |
43 | Mounted Volumes
44 | Monitor volumes for mount and unmount. Used by the Dash.
45 | service-mounts
46 |
47 |
48 | center
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
--------------------------------------------------------------------------------
/prefs/ui/window.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | 980
7 | 640
8 | False
9 |
12 |
13 |
14 | max-width: 550sp
15 | True
16 |
17 |
18 |
19 |
20 | 0.2
21 | 240
22 |
23 |
24 | Sidebar
25 |
26 |
27 | vertical
28 |
29 |
52 |
53 |
54 |
55 | false
56 | Placeholder Sidebar
57 |
58 |
59 |
60 |
61 | vertical
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 | Content
71 |
72 |
73 | true
74 | true
75 |
76 |
77 | Placehoder Content
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
--------------------------------------------------------------------------------
/schemas/gschemas.compiled:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icedman/gjs-wayland-shell/cedee2ea0c30b3d847b9c558b9cb0295406a777b/schemas/gschemas.compiled
--------------------------------------------------------------------------------
/screenshots/screenshot-2024-12-11-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icedman/gjs-wayland-shell/cedee2ea0c30b3d847b9c558b9cb0295406a777b/screenshots/screenshot-2024-12-11-01.png
--------------------------------------------------------------------------------
/screenshots/screenshot-2024-12-26-01.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icedman/gjs-wayland-shell/cedee2ea0c30b3d847b9c558b9cb0295406a777b/screenshots/screenshot-2024-12-26-01.png
--------------------------------------------------------------------------------
/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "dock-items-lead" : [],
3 | "favorite-apps" : [
4 | "kitty.desktop",
5 | "org.gnome.Nautilus.desktop",
6 | "google-chrome.desktop",
7 | "org.mozilla.firefox.desktop",
8 | "com.spotify.Client.desktop",
9 | "org.gnome.Calendar.desktop",
10 | "org.gnome.clocks.desktop",
11 | "org.gnome.Software.desktop",
12 | "org.gnome.TextEditor.desktop"
13 | ],
14 | "panel-items" : [],
15 | "panel-items-lead" : [
16 | {
17 | "hideIcon" : false,
18 | "id" : "logo",
19 | "showOSName" : true,
20 | "showShortOSName" : false,
21 | "widget" : "logo"
22 | },
23 | "search"
24 | ],
25 | "panel-items-trail" : [
26 | {
27 | "_icons" : [
28 | "caffeine-off",
29 | "caffeine-on"
30 | ],
31 | "id" : "inhibitor"
32 | },
33 | {
34 | "format" : "%P%",
35 | "id" : "brightness"
36 | },
37 | {
38 | "-widget" : "icon-label",
39 | "icon" : "triangle-left",
40 | "id" : "arrow-inhibitor-network"
41 | },
42 | "network",
43 | "--bluetooth",
44 | {
45 | "-widget" : "icon-label",
46 | "icon" : "triangle-left",
47 | "id" : "arrow-network-power"
48 | },
49 | {
50 | "format" : "%P%",
51 | "formatAlt" : "%P%",
52 | "formatAltToEmpty" : "%P% %H:%M battery remaining",
53 | "formatAltToFull" : "%P% %H:%M left charging time",
54 | "id" : "power",
55 | "width" : 80
56 | },
57 | {
58 | "format" : "%P%",
59 | "id" : "volume"
60 | },
61 | "mic",
62 | {
63 | "format" : "%Mounted %Used/%Size (%Use%)",
64 | "id" : "disk-stats-root",
65 | "on" : "/",
66 | "widget" : "disk-stats"
67 | },
68 | {
69 | "format" : "%usagePercent% %swapUsagePercent% ",
70 | "id" : "memory-stats",
71 | "width" : 84
72 | },
73 | {
74 | "id" : "cpu-stats",
75 | "width" : 84
76 | },
77 | {
78 | "-format" : "%Y-%m-%d %H:%M %p",
79 | "-interval" : 750,
80 | "format" : " %a, %b %d, %Y %H%:%M %p ",
81 | "icon" : "",
82 | "id" : "clock",
83 | "widget" : "clock"
84 | }
85 | ]
86 | }
87 | {
88 | "dock-items-lead" : [],
89 | "favorite-apps" : [
90 | "kitty.desktop",
91 | "org.gnome.Nautilus.desktop",
92 | "google-chrome.desktop",
93 | "org.mozilla.firefox.desktop",
94 | "com.spotify.Client.desktop",
95 | "org.gnome.Calendar.desktop",
96 | "org.gnome.clocks.desktop",
97 | "org.gnome.Software.desktop",
98 | "org.gnome.TextEditor.desktop"
99 | ],
100 | "panel-items" : [],
101 | "panel-items-lead" : [
102 | {
103 | "hideIcon" : false,
104 | "id" : "logo",
105 | "showOSName" : true,
106 | "showShortOSName" : false,
107 | "widget" : "logo"
108 | },
109 | "search"
110 | ],
111 | "panel-items-trail" : [
112 | {
113 | "_icons" : [
114 | "caffeine-off",
115 | "caffeine-on"
116 | ],
117 | "id" : "inhibitor"
118 | },
119 | {
120 | "format" : "%P%",
121 | "id" : "brightness"
122 | },
123 | {
124 | "-widget" : "icon-label",
125 | "icon" : "triangle-left",
126 | "id" : "arrow-inhibitor-network"
127 | },
128 | "network",
129 | "--bluetooth",
130 | {
131 | "-widget" : "icon-label",
132 | "icon" : "triangle-left",
133 | "id" : "arrow-network-power"
134 | },
135 | {
136 | "format" : "%P%",
137 | "formatAlt" : "%P%",
138 | "formatAltToEmpty" : "%P% %H:%M battery remaining",
139 | "formatAltToFull" : "%P% %H:%M left charging time",
140 | "id" : "power",
141 | "width" : 80
142 | },
143 | {
144 | "format" : "%P%",
145 | "id" : "volume"
146 | },
147 | "mic",
148 | {
149 | "format" : "%Mounted %Used/%Size (%Use%)",
150 | "id" : "disk-stats-root",
151 | "on" : "/",
152 | "widget" : "disk-stats"
153 | },
154 | {
155 | "format" : "%usagePercent% %swapUsagePercent% ",
156 | "id" : "memory-stats",
157 | "width" : 84
158 | },
159 | {
160 | "id" : "cpu-stats",
161 | "width" : 84
162 | },
163 | {
164 | "-format" : "%Y-%m-%d %H:%M %p",
165 | "-interval" : 750,
166 | "format" : " %a, %b %d, %Y %H%:%M %p ",
167 | "icon" : "",
168 | "id" : "clock",
169 | "widget" : "clock"
170 | }
171 | ]
172 | }
173 |
--------------------------------------------------------------------------------
/src/compositors/hyprland.js:
--------------------------------------------------------------------------------
1 | import Gio from "gi://Gio";
2 | import GLib from "gi://GLib";
3 | import GObject from "gi://GObject";
4 | import { WindowManagerInterface } from "./wmInterface.js";
5 |
6 | import {
7 | connectToSocket,
8 | connectToHyprSocket,
9 | disconnectSocket,
10 | sendMessage,
11 | receiveMessage,
12 | } from "../lib/ipc.js";
13 |
14 | const HyprShell = GObject.registerClass(
15 | class HyprShell extends WindowManagerInterface {
16 | _init() {
17 | super._init();
18 | this.name = "HYPR";
19 | }
20 |
21 | isAvailable() {
22 | try {
23 | let [success, output] = GLib.spawn_sync(
24 | null, // Working directory
25 | ["pgrep", "-x", "Hyprland"], // Command to check process
26 | null, // Environment
27 | GLib.SpawnFlags.SEARCH_PATH,
28 | null, // Child setup
29 | );
30 | return success && output.length > 0;
31 | } catch (e) {
32 | return false;
33 | }
34 | }
35 |
36 | connect() {
37 | return connectToHyprSocket();
38 | }
39 |
40 | async listen() {
41 | let connection = connectToHyprSocket(2);
42 | if (!connection) {
43 | return;
44 | }
45 | super.listen(connection, null);
46 | }
47 |
48 | parseMessage(msg) {
49 | let eventMap = {
50 | activewindowv2: "window-focused",
51 | openwindow: "window-opened",
52 | closewindow: "window-closed",
53 | };
54 |
55 | let res = [];
56 | let lines = msg.trim().split("\n");
57 | lines.forEach((l) => {
58 | let line = l.replace(">>", ",").split(",");
59 | let event = eventMap[line[0]];
60 | if (event) {
61 | res.push({
62 | event: event,
63 | window: {
64 | id: line[1],
65 | },
66 | raw: l,
67 | });
68 | } else {
69 | res.push({
70 | event: "unhandled",
71 | window: {},
72 | raw: l,
73 | });
74 | }
75 | });
76 |
77 | return res;
78 | }
79 |
80 | normalizeWindow(w) {
81 | /*
82 | {
83 | id: XXX, // unique identifier,
84 | app_id: 'kitty', // without .desktop suffix,
85 | title: 'title', // optional
86 | class: 'windowClass', // optional
87 | }
88 | */
89 | // hyperland
90 | if (w["address"]) {
91 | w["id"] = w["address"].replace("0x", "");
92 | }
93 | if (!w["app_id"] && w["class"]) {
94 | w["app_id"] = w["class"];
95 | }
96 | return super.normalizeWindow(w);
97 | }
98 |
99 | async getWindows() {
100 | let connection = this.connect();
101 | if (!connection) {
102 | return;
103 | }
104 | let message = "[j]/clients";
105 | await sendMessage(connection, message);
106 | let response = await receiveMessage(connection);
107 | this.disconnect(connection);
108 |
109 | let obj = JSON.parse(response);
110 | this.windows = obj;
111 | this.normalizeWindows();
112 | obj = {
113 | event: "windows-update",
114 | windows: this.windows,
115 | raw: obj,
116 | };
117 | this.onWindowsUpdated(obj);
118 | return Promise.resolve(obj);
119 | }
120 |
121 | async focusWindow(window) {
122 | let connection = this.connect();
123 | if (!connection) {
124 | return;
125 | }
126 | let message = `[j]/dispatch focuswindow address:${window["address"]}`;
127 | await sendMessage(connection, message);
128 | let response = await receiveMessage(connection);
129 | this.disconnect(connection);
130 |
131 | let obj = {
132 | event: response == "ok" ? "success" : "fail",
133 | };
134 | return Promise.resolve(obj);
135 | }
136 |
137 | async closeWindow(window) {
138 | let connection = this.connect();
139 | if (!connection) {
140 | return;
141 | }
142 | let message = `[j]/dispatch closewindow address:${window["address"]}`;
143 | await sendMessage(connection, message);
144 | let response = await receiveMessage(connection);
145 | this.disconnect(connection);
146 |
147 | let obj = {
148 | event: response == "ok" ? "success" : "fail",
149 | };
150 | return Promise.resolve(obj);
151 | }
152 |
153 | async spawn(cmd, arg = "") {
154 | cmd = cmd.replace("%U", arg);
155 | cmd = cmd.replace("%u", arg);
156 |
157 | let connection = this.connect();
158 | if (!connection) {
159 | return;
160 | }
161 | let message = `[j]/dispatch exec ${cmd}`;
162 | let response = await sendMessage(connection, message);
163 | this.disconnect(connection);
164 |
165 | let obj = {
166 | event: response == "ok" ? "success" : "fail",
167 | };
168 | return Promise.resolve(obj);
169 | }
170 |
171 | async exit() {
172 | let connection = this.connect();
173 | if (!connection) {
174 | return;
175 | }
176 | let message = `[j]/dispatch exit`;
177 | let response = await sendMessage(connection, message);
178 | this.disconnect(connection);
179 |
180 | return Promise.resolve(true);
181 | }
182 | },
183 | );
184 |
185 | export default HyprShell;
186 |
--------------------------------------------------------------------------------
/src/dock.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import Gsk from "gi://Gsk";
4 | import GLib from "gi://GLib";
5 | import Gio from "gi://Gio";
6 | import GObject from "gi://GObject";
7 | import LayerShell from "gi://Gtk4LayerShell";
8 | import { PopupMenu } from "./lib/popupMenu.js";
9 | import { Dot } from "./lib/dot.js";
10 | import { Extension } from "./lib/extensionInterface.js";
11 | import { getAppInfo, getAppInfoFromFile } from "./lib/appInfo.js";
12 | import { pointInRectangle, distanceToRectangle } from "./lib/collisions.js";
13 | import { pointerInWindow, getModifierStates } from "./lib/devices.js";
14 |
15 | import { DockPanel } from "./lib/dock.js";
16 | import { DockItem, DockAppItem } from "./lib/dockItem.js";
17 |
18 | const Dock = GObject.registerClass(
19 | class Dock extends Extension {
20 | _init(params) {
21 | this.name = params?.name ?? "Dock";
22 | delete params?.name;
23 | super._init({
24 | ...(params ?? {}),
25 | });
26 | }
27 |
28 | enable() {
29 | this.window = new DockPanel({
30 | title: this.name,
31 | name: this.name,
32 | hexpand: true,
33 | vexpand: true,
34 | default_width: 20,
35 | default_height: 20,
36 | });
37 |
38 | this.container = this.window.container;
39 | this.lead = this.window.lead;
40 | this.trail = this.window.trail;
41 | this.center = this.window.center;
42 |
43 | super.enable();
44 | }
45 |
46 | disable() {
47 | this.window._endAnimation();
48 | this.window.destroy();
49 | this.window = null;
50 | super.disable();
51 | }
52 |
53 | create_dock(params) {
54 | return new Dock(params);
55 | }
56 |
57 | create_dockitem_from_appinfo(app) {
58 | let appInfo = getAppInfo(app);
59 | if (!appInfo) return;
60 | let btn = new DockAppItem({
61 | app: appInfo,
62 | });
63 | return btn;
64 | }
65 | },
66 | );
67 |
68 | export default Dock;
69 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/audio.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 |
4 | export function createVolumeIndicator(config) {
5 | let volume = Main.panel.create_panelitem(config);
6 |
7 | let menu = volume.menu;
8 | menu.has_arrow = true;
9 |
10 | let builder = new Gtk.Builder();
11 | builder.add_from_file(`${this.path}/ui/volume.ui`);
12 |
13 | let widget = builder.get_object("volume-widget");
14 | let w = builder.get_object("volume");
15 | let l = builder.get_object("volume-label");
16 | let t = builder.get_object("volume-toggle");
17 | l.set_size_request(40, -1);
18 | widget.parent.remove(widget);
19 | menu.child.append(widget);
20 | volume.menu = menu;
21 | // volume.append(menu);
22 |
23 | volume.on_click = () => {
24 | menu.popup();
25 | };
26 |
27 | volume.first_change = false;
28 |
29 | let setVolume = () => {
30 | volume._debounceVolume = Main.loTimer.debounce(
31 | Main.loTimer,
32 | () => {
33 | let value = w.get_value();
34 | Main.volume._stream.volume = Main.volume.state.max_volume * value;
35 | },
36 | 5,
37 | volume._debounceVolume,
38 | );
39 | };
40 |
41 | w.connect("value-changed", (w) => {
42 | setVolume();
43 | // if (!volume.first_change || ) {
44 | // volume.first_change = true;
45 | // return;
46 | // }
47 | // menu.popup();
48 | });
49 | t.connect("clicked", (t) => {
50 | let stream = Main.volume._stream;
51 | let control = Main.volume._control;
52 | if (!stream || !control) return;
53 | const { isMuted } = stream;
54 | if (isMuted && stream.volume === 0) {
55 | stream.volume = 0.4 * control.get_vol_max_norm();
56 | stream.push_volume();
57 | }
58 | stream.change_is_muted(!isMuted);
59 | });
60 |
61 | Main.volume.connectObject(
62 | "volume-update",
63 | () => {
64 | let state = Main.volume.state;
65 | volume.set_label(``);
66 | volume.set_icon(state.icon);
67 |
68 | t.set_icon_name(state.icon);
69 | w.set_value(state.level / 100);
70 | l.set_label(`${Math.floor(state.level)}%`);
71 | if (state.is_muted) {
72 | l.set_label(`0%`);
73 | }
74 | w.set_sensitive(!state.is_muted);
75 | },
76 | this,
77 | );
78 | volume.connect("destroy", () => {
79 | if (volume._debounceVolume) {
80 | Main.loTimer.cancel(volume._debounceVolume);
81 | volume._debounceVolume = null;
82 | }
83 | Main.volume.disconnectObject(volume);
84 | });
85 |
86 | Main.volume.sync();
87 | return volume;
88 | }
89 |
90 | export function createMicIndicator(config) {
91 | let mic = Main.panel.create_panelitem(config);
92 | mic.onMicUpdate = (w, s) => {};
93 |
94 | Main.mic.connectObject(
95 | "mic-update",
96 | () => {
97 | let state = Main.mic.state;
98 | mic.set_label(``);
99 | if (config.icons) {
100 | mic.set_icon(config.icons[state.icon_index] ?? state.icon);
101 | } else {
102 | mic.set_icon(state.icon);
103 | }
104 | mic.onMicUpdate(mic, state);
105 | },
106 | this,
107 | );
108 | mic.connect("destroy", () => {
109 | Main.mic.disconnectObject(mic);
110 | });
111 |
112 | Main.mic.sync();
113 |
114 | mic.on_click = () => {
115 | Main.mic._stream["is-muted"] = !Main.mic._stream["is-muted"];
116 | };
117 | return mic;
118 | }
119 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/network.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 |
4 | export function createNetworkIndicator(config) {
5 | let network = Main.panel.create_panelitem(config);
6 | Main.network.connectObject(
7 | "network-update",
8 | () => {
9 | let state = Main.network.state;
10 | network.set_label(``);
11 | network.set_icon(state.icon);
12 | // network.visible = state.visible;
13 | },
14 | network,
15 | );
16 | Main.network.sync();
17 |
18 | let menu = network.menu;
19 | menu.has_arrow = true;
20 |
21 | let builder = new Gtk.Builder();
22 | builder.add_from_file(`${this.path}/ui/network.ui`);
23 |
24 | let widget = builder.get_object("network-widget");
25 | let i = builder.get_object("network-icon");
26 | let l = builder.get_object("network-label");
27 | l.set_size_request(40, -1);
28 | widget.parent?.remove(widget);
29 | menu.child.append(widget);
30 | network.menu = menu;
31 | // network.append(menu);
32 |
33 | network.on_click = (count, btn) => {
34 | let state = Main.network.state;
35 | let source = Main.network.indicator._primaryIndicatorBinding.source;
36 | if (btn == 3 && state.address) {
37 | i.set_label(state.address);
38 | menu.popup();
39 | return;
40 | }
41 | if (source) {
42 | i.set_label(`${source.title} ${state.id ?? source.subtitle}`);
43 | menu.popup();
44 | return;
45 | }
46 | };
47 |
48 | network.connect("destroy", () => {
49 | Main.network.disconnectObject(network);
50 | });
51 |
52 | return network;
53 | }
54 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/shutdown.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 |
4 | export function createShutdownIcon(config) {
5 | let shutdown = Main.panel.create_panelitem(config);
6 | let menu = shutdown.menu;
7 | menu.setItems([
8 | {
9 | name: "Suspend",
10 | exec: "systemctl suspend",
11 | },
12 | // {
13 | // name: "Logout",
14 | // exec: "loginctl terminate-session $(< /proc/self/sessionid)",
15 | // },
16 | {
17 | name: "Restart",
18 | exec: "shutdown -r now",
19 | },
20 | {
21 | name: "Shutdown",
22 | exec: "shutdown -h now",
23 | },
24 | ]);
25 | menu.has_arrow = true;
26 |
27 | // widget.parent.remove(widget);
28 | // menu.child.append(widget);
29 |
30 | shutdown.on_click = (count, btn) => {
31 | if (btn == 3) {
32 | return;
33 | }
34 | menu.popup();
35 | };
36 |
37 | return shutdown;
38 | }
39 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/stats.js:
--------------------------------------------------------------------------------
1 | export function createCpuStats(config) {
2 | if (!Main.settings.get_boolean("service-system-stats")) return null;
3 | let cpuStats = Main.panel.create_panelitem(config);
4 | let statsService = Main.stats;
5 |
6 | statsService.connectObject(
7 | "stats-cpu-update",
8 | () => {
9 | let state = statsService.state.cpu ?? [];
10 | cpuStats.set_icon(config.icon ?? "cpu-alt-symbolic");
11 | if (state[0]) {
12 | let usage = Math.round(state[0]["usage"] * 100) / 100;
13 | cpuStats.set_label(`${usage}%`);
14 | }
15 | },
16 | cpuStats,
17 | );
18 | statsService.sync();
19 |
20 | cpuStats.on_click = () => {};
21 |
22 | cpuStats.connect("destroy", () => {
23 | statsService.disconnectObject(cpuStats);
24 | });
25 | return cpuStats;
26 | }
27 |
28 | function formatMemory(state, fmt = "%usage%", config) {
29 | let res = fmt;
30 | Object.keys(state).forEach((k) => {
31 | res = res.replace(`%${k}`, state[k]);
32 | });
33 | return res;
34 | }
35 |
36 | export function createMemoryStats(config) {
37 | if (!Main.settings.get_boolean("service-system-stats")) return null;
38 | let memoryStats = Main.panel.create_panelitem(config);
39 | let statsService = Main.stats;
40 |
41 | statsService.connectObject(
42 | "stats-memory-update",
43 | () => {
44 | let state = statsService.state.memory ?? {};
45 | if (state["total"]) {
46 | memoryStats.set_icon(config.icon ?? "memory-symbolic");
47 | memoryStats.set_label(formatMemory(state, config.format, config));
48 | }
49 | },
50 | memoryStats,
51 | );
52 | statsService.sync();
53 |
54 | memoryStats.on_click = () => {};
55 |
56 | memoryStats.connect("destroy", () => {
57 | statsService.disconnectObject(memoryStats);
58 | });
59 | return memoryStats;
60 | }
61 |
62 | function formatDisk(state, fmt = "%Filesystem %Used/%Size %Use%", config) {
63 | let res = fmt;
64 | Object.keys(state).forEach((k) => {
65 | res = res.replace(`%${k}`, state[k]);
66 | });
67 | return res;
68 | }
69 |
70 | export function createDiskStats(config) {
71 | if (!Main.settings.get_boolean("service-system-stats")) return null;
72 | let diskStats = Main.panel.create_panelitem(config);
73 | let statsService = Main.stats;
74 |
75 | statsService.connectObject(
76 | "stats-disk-update",
77 | () => {
78 | let disk = statsService.state.disk ?? {};
79 | let state = disk[config.on ?? "/"];
80 | if (!state) return;
81 |
82 | // [
83 | // "Filesystem",
84 | // "Size",
85 | // "Used",
86 | // "Avail",
87 | // "Use%",
88 | // "Mounted"
89 | // ]
90 |
91 | if (state["Mounted"]) {
92 | diskStats.set_icon(config.icon ?? "hard-disk-symbolic");
93 | diskStats.set_label(formatDisk(state, config.format, config));
94 | }
95 | },
96 | diskStats,
97 | );
98 | statsService.sync();
99 |
100 | diskStats.on_click = () => {
101 | if (config.formatAlt) {
102 | }
103 | };
104 |
105 | diskStats.connect("destroy", () => {
106 | statsService.disconnectObject(diskStats);
107 | });
108 | return diskStats;
109 | }
110 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/style.css:
--------------------------------------------------------------------------------
1 | .calendar-widget {
2 | margin: 6px;
3 | background: transparent;
4 | border: 0px;
5 | font-size: 12pt;
6 | }
--------------------------------------------------------------------------------
/src/extensions/bar-items/ui/brightness.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | false
9 | display-brightness-symbolic
10 |
11 |
12 |
13 |
14 | center
15 | 140px
16 | false
17 | right
18 | horizontal
19 | 2
20 | brightness-adjust
21 |
22 |
23 |
24 |
25 | false
26 |
27 |
28 |
29 |
30 |
31 |
32 | 0.0
33 | 1.0
34 | 0.01
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/ui/calendar.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/ui/network.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | true
9 | false
10 | audio-volume-high-symbolic
11 |
12 |
13 |
14 |
15 | false
16 | center
17 | 140px
18 | 140px
19 | false
20 | right
21 | horizontal
22 | 2
23 | network-adjust
24 |
25 |
26 |
27 |
28 | false
29 | false
30 |
31 |
32 |
33 |
34 |
35 |
36 | 0.0
37 | 1.0
38 | 0.01
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/ui/power.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | true
9 | false
10 | Button
11 |
12 |
13 |
14 |
15 |
16 | false
17 | center
18 | 140px
19 | 140px
20 | false
21 | right
22 | horizontal
23 | 2
24 | power-adjust
25 |
26 |
27 |
28 |
29 | false
30 | false
31 |
32 |
33 |
34 |
35 |
36 |
37 | 0.0
38 | 1.0
39 | 0.01
40 |
41 |
42 |
--------------------------------------------------------------------------------
/src/extensions/bar-items/ui/volume.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | false
9 | audio-volume-high-symbolic
10 |
11 |
12 |
13 |
14 | center
15 | 140px
16 | false
17 | right
18 | horizontal
19 | 2
20 | volume-adjust
21 |
22 |
23 |
24 |
25 | false
26 |
27 |
28 |
29 |
30 |
31 |
32 | 0.0
33 | 1.0
34 | 0.01
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/extensions/console/style.css:
--------------------------------------------------------------------------------
1 | #Console #Output {
2 | color: #202020;
3 | padding: 8px;
4 | }
--------------------------------------------------------------------------------
/src/lib/appInfo.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 |
7 | const appRegistry = {};
8 |
9 | function getAppInfo(app) {
10 | if (!app) {
11 | return {};
12 | }
13 | let appInfo = app;
14 |
15 | if (appInfo && typeof appInfo === "string") {
16 | if (appRegistry[appInfo]) {
17 | return appRegistry[appInfo];
18 | }
19 | let desktopAppInfo = Gio.DesktopAppInfo.new(app);
20 | if (desktopAppInfo) {
21 | let id = desktopAppInfo.get_id();
22 | let icon_name = desktopAppInfo.get_string("Icon");
23 | let name = desktopAppInfo.get_string("Name");
24 | let exec = (desktopAppInfo.get_string("Exec") ?? "").trim();
25 | appInfo = {
26 | id,
27 | name,
28 | icon_name,
29 | exec,
30 | };
31 | } else {
32 | appInfo = {
33 | id: appInfo,
34 | };
35 | }
36 | }
37 |
38 | if (appRegistry[appInfo.id]) {
39 | return appRegistry[appInfo.id];
40 | }
41 |
42 | if (appInfo && !appInfo.menu) {
43 | appInfo.menu = getAppInfoMenu(appInfo);
44 | }
45 |
46 | appInfo["should_show"] = appInfo["should_show"] ?? (() => true);
47 | appInfo["display_name"] = appInfo["display_name"] ?? appInfo["name"];
48 | appInfo["description"] = appInfo["description"] ?? appInfo["name"];
49 | appInfo["title"] = appInfo["title"] ?? appInfo["description"];
50 | appInfo["keywords"] = appInfo["keywords"] ?? appInfo["name"];
51 | appInfo["exec"] = appInfo["exec"] ?? appInfo["id"];
52 | Object.keys(appInfo).forEach((k) => {
53 | if (typeof appInfo[k] != "function") {
54 | appInfo[`get_${k}`] = () => {
55 | return appInfo[k];
56 | };
57 | }
58 | });
59 | appInfo["get_executable"] = appInfo["get_exec"];
60 |
61 | appRegistry[appInfo.id] = appInfo;
62 | return appInfo;
63 | }
64 |
65 | function getAppInfoFromFile(file) {
66 | let desktopAppInfo = Gio.DesktopAppInfo.new_from_filename(file);
67 | return getAppInfo(desktopAppInfo);
68 | }
69 |
70 | function getAppInfoMenu(appInfo) {
71 | let items = [
72 | // {
73 | // id: appInfo.id,
74 | // action: 'open',
75 | // name: 'Open Window',
76 | // exec: appInfo.exec,
77 | // },
78 | ];
79 |
80 | let desktopAppInfo = Gio.DesktopAppInfo.new(appInfo.id ?? "xxx");
81 | if (!desktopAppInfo) {
82 | return items;
83 | }
84 |
85 | let content = null;
86 |
87 | try {
88 | content = GLib.file_get_contents(desktopAppInfo.filename)[1];
89 | } catch (err) {
90 | return null;
91 | }
92 |
93 | let lines = String.fromCharCode.apply(null, content).split("\n");
94 |
95 | // console.log(lines);
96 | desktopAppInfo.list_actions().forEach((action) => {
97 | let name = desktopAppInfo.get_action_name(action);
98 | let nextExec = false;
99 | let exec = null;
100 | for (let i = 0; i < lines.length; i++) {
101 | let line = lines[i].trim();
102 | if (line.includes(`${action}]`)) {
103 | nextExec = true;
104 | }
105 | if (nextExec && line.startsWith("Exec")) {
106 | exec = line
107 | .replace("Exec=", "")
108 | .replace("%U", "")
109 | .replace("%u", "")
110 | .trim();
111 | break;
112 | }
113 | }
114 |
115 | items.push({ action, name, exec });
116 | });
117 |
118 | return items;
119 | }
120 |
121 | export { getAppInfo, getAppInfoFromFile };
122 |
--------------------------------------------------------------------------------
/src/lib/background.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import Gsk from "gi://Gsk";
4 | import GLib from "gi://GLib";
5 | import Gio from "gi://Gio";
6 | import GObject from "gi://GObject";
7 | import LayerShell from "gi://Gtk4LayerShell";
8 | import { PopupMenu } from "./popupMenu.js";
9 | import { Dot } from "./dot.js";
10 | import { Extension } from "./extensionInterface.js";
11 | import { getAppInfo, getAppInfoFromFile } from "./appInfo.js";
12 | import { pointInRectangle, distanceToRectangle } from "./collisions.js";
13 | import { pointerInWindow, getModifierStates } from "./devices.js";
14 |
15 | export const Background = GObject.registerClass(
16 | class Background extends Gtk.Window {
17 | _init(params) {
18 | this.customSettings = params.customSettings ?? {};
19 | delete params.customSettings;
20 |
21 | super._init({
22 | name: params.name,
23 | title: params.name,
24 | hexpand: true,
25 | vexpand: true,
26 | default_width: 20,
27 | default_height: 20,
28 | ...params,
29 | });
30 |
31 | this.style = Main.style;
32 | this.decorated = false;
33 |
34 | LayerShell.init_for_window(this);
35 | LayerShell.set_layer(this, LayerShell.Layer.BACKGROUND);
36 | LayerShell.set_keyboard_mode(this, LayerShell.KeyboardMode.ON_DEMAND);
37 | }
38 |
39 | destroy() {
40 | super.destroy();
41 | }
42 |
43 | vfunc_size_allocate(width, height, z) {
44 | super.vfunc_size_allocate(width, height, z);
45 | }
46 | },
47 | );
48 |
49 | export default { Background };
50 |
--------------------------------------------------------------------------------
/src/lib/collisions.js:
--------------------------------------------------------------------------------
1 | function pointInRectangle(point, rect) {
2 | return (
3 | point.x >= rect.x &&
4 | point.x <= rect.x + rect.width &&
5 | point.y >= rect.y &&
6 | point.y <= rect.y + rect.height
7 | );
8 | }
9 |
10 | function distanceToRectangle(point, rect) {
11 | let rx = rect.x + rect.width / 2;
12 | let ry = rect.y + rect.height / 2;
13 | let dx = point.x - rx;
14 | let dy = point.y - ry;
15 | return Math.sqrt(dx * dx + dy * dy);
16 | }
17 |
18 | export { pointInRectangle, distanceToRectangle };
19 |
--------------------------------------------------------------------------------
/src/lib/devices.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 |
3 | function pointerInWindow(w) {
4 | let res = [false, 0, 0];
5 | const surface = w.get_surface();
6 | const display = surface?.get_display();
7 | const seat = display?.get_default_seat();
8 | const pointer = seat?.get_pointer();
9 | const device_position = surface?.get_device_position(pointer) ?? res;
10 | return device_position;
11 | }
12 |
13 | function getModifierStates(w) {
14 | try {
15 | const surface = w?.get_surface();
16 | const display = surface?.get_display() ?? Gdk.Display.get_default();
17 | const seat = display?.get_default_seat();
18 | const keyboard = seat?.get_keyboard();
19 | // console.log(seat);
20 | // console.log(keyboard);
21 | // console.log(keyboard?.modifier_state);
22 |
23 | return {
24 | ctrl:
25 | (keyboard?.get_modifier_state() & Gdk.ModifierType.CONTROL_MASK) > 0,
26 | shift: (keyboard?.get_modifier_state() & Gdk.ModifierType.SHIFT_MASK) > 0,
27 | alt: (keyboard?.get_modifier_state() & Gdk.ModifierType.ALT_MASK) > 0,
28 | meta: (keyboard?.get_modifier_state() & Gdk.ModifierType.META_MASK) > 0,
29 | hyper: (keyboard?.get_modifier_state() & Gdk.ModifierType.HYPER_MASK) > 0,
30 | };
31 | } catch (err) {
32 | console.log(err);
33 | return {};
34 | }
35 | }
36 |
37 | export { pointerInWindow, getModifierStates };
38 |
--------------------------------------------------------------------------------
/src/lib/environment.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GObject from "gi://GObject";
4 | import Gio from "gi://Gio";
5 | import * as SignalTracker from "./signalTracker.js";
6 |
7 | Math.clamp = function (x, lower, upper) {
8 | return Math.min(Math.max(x, lower), upper);
9 | };
10 |
11 | const Format = imports.format;
12 |
13 | String.prototype.format = Format.format;
14 |
15 | GObject.Object.prototype.connectObject = function (...args) {
16 | SignalTracker.connectObject(this, ...args);
17 | };
18 | GObject.Object.prototype.connect_object = function (...args) {
19 | SignalTracker.connectObject(this, ...args);
20 | };
21 | GObject.Object.prototype.disconnectObject = function (...args) {
22 | SignalTracker.disconnectObject(this, ...args);
23 | };
24 | GObject.Object.prototype.disconnect_object = function (...args) {
25 | SignalTracker.disconnectObject(this, ...args);
26 | };
27 |
28 | Gio.Settings.prototype.getSetting = function (k) {
29 | try {
30 | let settings = this;
31 | return settings.get_value(k).deepUnpack();
32 | // let value = settings.get_value(k);
33 | // let valueType = value.get_type_string();
34 | // switch (valueType) {
35 | // case 's':
36 | // return settings.get_string(k);
37 | // break;
38 | // case 'i':
39 | // return value.get_int32();
40 | // break;
41 | // case 'd':
42 | // return value.get_double();
43 | // break;
44 | // case 'b':
45 | // return value.get_boolean();
46 | // break;
47 | // case 'as':
48 | // return value.deepUnpack();
49 | // break;
50 | // case '(dddd)':
51 | // return value.deepUnpack();
52 | // default:
53 | // console.log(valueType);
54 | // break;
55 | // }
56 | } catch (err) {
57 | console.log(err);
58 | }
59 | return null;
60 | };
61 |
62 | GObject.Object.prototype.load_settings = function (
63 | settings,
64 | settingsMap,
65 | prefix,
66 | ) {
67 | settings = settings ?? this.settings;
68 | if (settings) {
69 | settings.disconnectObject(this);
70 | }
71 | settingsMap = settingsMap ?? this.settingsMap ?? {};
72 | prefix = prefix ?? this.settingsPrefix ?? this.name?.toLowerCase() ?? "";
73 | Object.keys(settingsMap).forEach((k) => {
74 | let _key = k;
75 | if (prefix != "") {
76 | _key = _key.replace(`${prefix}-`, "");
77 | }
78 | _key = _key.replaceAll("-", "_").toUpperCase();
79 | try {
80 | this[_key] = settings.getSetting(k);
81 | } catch (err) {
82 | return;
83 | }
84 | if (Main.userSettings && Main.userSettings[k]) {
85 | this[_key] = Main.userSettings[k];
86 | }
87 | if (this.customSettings && this.customSettings[k]) {
88 | this[_key] = this.customSettings[k];
89 | }
90 | try {
91 | settings.connectObject(
92 | `changed::${k}`,
93 | () => {
94 | this[_key] = settings.getSetting(k);
95 | if (Main.userSettings && Main.userSettings[k]) {
96 | this[_key] = Main.userSettings[k];
97 | }
98 | if (this.customSettings && this.customSettings[k]) {
99 | this[_key] = this.customSettings[k];
100 | }
101 | settingsMap[k]();
102 | },
103 | this,
104 | );
105 | } catch (err) {
106 | // key not in schema
107 | }
108 | });
109 | };
110 |
--------------------------------------------------------------------------------
/src/lib/extensionInterface.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 |
7 | const Extension = GObject.registerClass(
8 | {
9 | Properties: {
10 | enabled: GObject.ParamSpec.boolean(
11 | "enabled",
12 | null,
13 | null,
14 | GObject.ParamFlags.READWRITE,
15 | "",
16 | ),
17 | },
18 | Signals: {
19 | update: { param_types: [GObject.TYPE_OBJECT] },
20 | },
21 | },
22 | class Extension extends GObject.Object {
23 | _init(params) {
24 | super._init(params);
25 | }
26 |
27 | async enable() {
28 | this.enabled = true;
29 | }
30 |
31 | disable() {
32 | this.enabled = false;
33 | }
34 | },
35 | );
36 |
37 | export { Extension };
38 |
--------------------------------------------------------------------------------
/src/lib/factory.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 |
7 | const providerRegistry = {};
8 |
9 | const Factory = GObject.registerClass(
10 | {
11 | Signals: {
12 | "registry-update": {},
13 | },
14 | },
15 | class Factory extends GObject.Object {
16 | _init(params = {}) {
17 | super._init(params);
18 | this.providers = {};
19 | }
20 |
21 | registerProvider(id, createFunction, owner) {
22 | this.providers[id] = {
23 | id: id,
24 | create: createFunction,
25 | owner: owner,
26 | };
27 | this.emit("registry-update");
28 | }
29 |
30 | unregisterProvider(owner) {
31 | Object.keys(this.providers).forEach((k) => {
32 | if (this.providers[id].owner == owner) {
33 | delete this.providers[id];
34 | }
35 | });
36 | }
37 |
38 | create(id, config = {}) {
39 | if (this.providers[id]) {
40 | let item = this.providers[id].create(config);
41 | if (item) {
42 | item.id = config.id ?? id;
43 | // console.log(config);
44 | return item;
45 | }
46 | }
47 | return null;
48 | }
49 | },
50 | );
51 |
52 | export default Factory;
53 |
--------------------------------------------------------------------------------
/src/lib/fileUtils.js:
--------------------------------------------------------------------------------
1 | import GLib from "gi://GLib";
2 | import Gio from "gi://Gio";
3 |
4 | // export {loadInterfaceXML} from './dbusUtils.js';
5 |
6 | /**
7 | * @typedef {object} SubdirInfo
8 | * @property {Gio.File} dir the file object for the subdir
9 | * @property {Gio.FileInfo} info the file descriptor for the subdir
10 | */
11 |
12 | /**
13 | * @param {string} subdir the subdirectory to search within the data directories
14 | * @param {boolean} includeUserDir whether the user's data directory should also be searched in addition
15 | * to the system data directories
16 | * @returns {Generator} a generator which yields file info for subdirectories named
17 | * `subdir` within data directories
18 | */
19 | export function* collectFromDatadirs(subdir, includeUserDir) {
20 | let dataDirs = GLib.get_system_data_dirs();
21 | if (includeUserDir) dataDirs.unshift(GLib.get_user_data_dir());
22 |
23 | for (let i = 0; i < dataDirs.length; i++) {
24 | let path = GLib.build_filenamev([dataDirs[i], "gnome-shell", subdir]);
25 | let dir = Gio.File.new_for_path(path);
26 |
27 | let fileEnum;
28 | try {
29 | fileEnum = dir.enumerate_children(
30 | "standard::name,standard::type",
31 | Gio.FileQueryInfoFlags.NONE,
32 | null,
33 | );
34 | } catch (e) {
35 | fileEnum = null;
36 | }
37 | if (fileEnum != null) {
38 | let info;
39 | while ((info = fileEnum.next_file(null)))
40 | yield { dir: fileEnum.get_child(info), info };
41 | }
42 | }
43 | }
44 |
45 | /**
46 | * @param {string} subdir the subdirectory to search within the data directories
47 | * @param {boolean} includeUserDir whether the user's data directory should also be searched in addition
48 | * to the system data directories
49 | * @returns {Generator} a generator which yields file info for subdirectories named
50 | * `subdir` within data directories
51 | */
52 | export function* collectFromDatadirs2(subdir, includeUserDir) {
53 | let dataDirs = GLib.get_system_data_dirs();
54 | if (includeUserDir) dataDirs.unshift(GLib.get_user_data_dir());
55 |
56 | for (let i = 0; i < dataDirs.length; i++) {
57 | let path = GLib.build_filenamev([dataDirs[i], subdir]);
58 | let dir = Gio.File.new_for_path(path);
59 |
60 | let fileEnum;
61 | try {
62 | fileEnum = dir.enumerate_children(
63 | "standard::name,standard::type",
64 | Gio.FileQueryInfoFlags.NONE,
65 | null,
66 | );
67 | } catch (e) {
68 | fileEnum = null;
69 | }
70 | if (fileEnum != null) {
71 | let info;
72 | while ((info = fileEnum.next_file(null)))
73 | yield { dir: fileEnum.get_child(info), info };
74 | }
75 | }
76 | }
77 |
78 | /**
79 | * @param {Gio.File} dir
80 | * @param {boolean} deleteParent
81 | */
82 | export function recursivelyDeleteDir(dir, deleteParent) {
83 | let children = dir.enumerate_children(
84 | "standard::name,standard::type",
85 | Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
86 | null,
87 | );
88 |
89 | let info;
90 | while ((info = children.next_file(null)) != null) {
91 | let type = info.get_file_type();
92 | let child = dir.get_child(info.get_name());
93 | if (type === Gio.FileType.REGULAR) child.delete(null);
94 | else if (type === Gio.FileType.DIRECTORY) recursivelyDeleteDir(child, true);
95 | }
96 |
97 | if (deleteParent) dir.delete(null);
98 | }
99 |
100 | /**
101 | * @param {Gio.File} srcDir
102 | * @param {Gio.File} destDir
103 | */
104 | export function recursivelyMoveDir(srcDir, destDir) {
105 | let children = srcDir.enumerate_children(
106 | "standard::name,standard::type",
107 | Gio.FileQueryInfoFlags.NOFOLLOW_SYMLINKS,
108 | null,
109 | );
110 |
111 | if (!destDir.query_exists(null)) destDir.make_directory_with_parents(null);
112 |
113 | let info;
114 | while ((info = children.next_file(null)) != null) {
115 | let type = info.get_file_type();
116 | let srcChild = srcDir.get_child(info.get_name());
117 | let destChild = destDir.get_child(info.get_name());
118 | if (type === Gio.FileType.REGULAR)
119 | srcChild.move(destChild, Gio.FileCopyFlags.NONE, null, null);
120 | else if (type === Gio.FileType.DIRECTORY)
121 | recursivelyMoveDir(srcChild, destChild);
122 | }
123 | }
124 |
--------------------------------------------------------------------------------
/src/lib/iconInfo.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 |
4 | function getIconInfo(iconName) {
5 | let iconTheme = Gtk.IconTheme.get_for_display(Gdk.Display.get_default());
6 | let size = 48; // Icon size in pixels
7 | let scale = 1; // Scale factor (e.g., 1 for standard resolution, 2 for HiDPI)
8 | let direction = Gtk.TextDirection.LTR; // Left-to-right or right-to-left
9 | let state = Gtk.IconLookupFlags.FORCE_SVG; // Example: force SVG or other flags
10 | let flags;
11 |
12 | // Look up the icon
13 | let iconInfo = iconTheme.lookup_icon(
14 | iconName,
15 | null,
16 | size,
17 | scale,
18 | direction,
19 | flags,
20 | );
21 |
22 | if (iconInfo) {
23 | // Get the file path of the icon
24 | let iconPath = iconInfo.get_file().get_path();
25 | print(`Icon path: ${iconPath} ${iconInfo.is_symbolic}`);
26 | } else {
27 | print("Icon not found in the current theme.");
28 | }
29 |
30 | return iconInfo;
31 | }
32 |
33 | export { getIconInfo };
34 |
--------------------------------------------------------------------------------
/src/lib/misc.js:
--------------------------------------------------------------------------------
1 | /**
2 | * lowerBound:
3 | *
4 | * @template T, [K=T]
5 | * @param {readonly T[]} array an array or array-like object, already sorted
6 | * according to `cmp`
7 | * @param {K} val the value to add
8 | * @param {(a: T, val: K) => number} cmp a comparator (or undefined to compare as numbers)
9 | * @returns {number}
10 | *
11 | * Returns the position of the first element that is not
12 | * lower than `val`, according to `cmp`.
13 | * That is, returns the first position at which it
14 | * is possible to insert val without violating the
15 | * order.
16 | *
17 | * This is quite like an ordinary binary search, except
18 | * that it doesn't stop at first element comparing equal.
19 | */
20 | function lowerBound(array, val, cmp) {
21 | let min, max, mid, v;
22 | cmp ||= (a, b) => a - b;
23 |
24 | if (array.length === 0) return 0;
25 |
26 | min = 0;
27 | max = array.length;
28 | while (min < max - 1) {
29 | mid = Math.floor((min + max) / 2);
30 | v = cmp(array[mid], val);
31 |
32 | if (v < 0) min = mid + 1;
33 | else max = mid;
34 | }
35 |
36 | return min === max || cmp(array[min], val) < 0 ? max : min;
37 | }
38 |
39 | /**
40 | * insertSorted:
41 | *
42 | * @template T, [K=T]
43 | * @param {T[]} array an array sorted according to `cmp`
44 | * @param {K} val a value to insert
45 | * @param {(a: T, val: K) => number} cmp the sorting function
46 | * @returns {number}
47 | *
48 | * Inserts `val` into `array`, preserving the
49 | * sorting invariants.
50 | *
51 | * Returns the position at which it was inserted
52 | */
53 | export function insertSorted(array, val, cmp) {
54 | let pos = lowerBound(array, val, cmp);
55 | array.splice(pos, 0, val);
56 |
57 | return pos;
58 | }
59 |
--------------------------------------------------------------------------------
/src/lib/popupMenu.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { getAppInfo, getAppInfoFromFile } from "./appInfo.js";
7 |
8 | // todo .. use model
9 |
10 | export const PopupMenu = GObject.registerClass(
11 | class PopupMenu extends Gtk.Popover {
12 | _init(params = {}) {
13 | let appInfo = getAppInfo(params.app);
14 | let items = params.items ?? appInfo?.menu ?? [];
15 | delete params.app;
16 | delete params.items;
17 |
18 | super._init({
19 | name: "Menu",
20 | has_arrow: false,
21 | position: 2,
22 | ...params,
23 | });
24 |
25 | let box = new Gtk.Box({
26 | orientation: Gtk.Orientation.VERTICAL,
27 | });
28 | this.box = box;
29 | this.set_child(box);
30 | this.setItems(items);
31 | }
32 |
33 | setItems(items) {
34 | let box = this.box;
35 |
36 | let children = [];
37 | let n = box.get_first_child();
38 | while (n) {
39 | children.push(n);
40 | n = n.get_next_sibling();
41 | }
42 | children.forEach((c) => {
43 | box.remove(c);
44 | });
45 |
46 | items.forEach((item) => {
47 | let button = new Gtk.Box({
48 | name: "MenuItem",
49 | orientation: Gtk.Orientation.HORIZONTAL,
50 | hexpand: true,
51 | });
52 |
53 | // make this configurable
54 | let evt = new Gtk.GestureClick();
55 | // evt.set_button(3);
56 | evt.connect("pressed", async (actor, count) => {
57 | if (item.script) {
58 | item.script();
59 | this.popdown();
60 | return;
61 | }
62 |
63 | if (item.action == "focus" && item.window) {
64 | Main.shell.focusWindow(item.window);
65 | this.popdown();
66 | return;
67 | }
68 | if (item.action == "open") {
69 | Main.shell.focusOrSpawn(item.id, item.exec);
70 | this.popdown();
71 | return;
72 | }
73 | Main.shell.spawn(item.exec);
74 | this.popdown();
75 | });
76 | button.add_controller(evt);
77 |
78 | // button.icon = new Gtk.Image();
79 | // button.icon.add_css_class("icon");
80 | // button.icon.set_visible(false);
81 | // button.append(button.icon);
82 |
83 | button.label = new Gtk.Label();
84 | button.label.add_css_class("label");
85 | button.label.hexpand = true;
86 | button.label.halign = Gtk.Align.START;
87 | // button.label.set_visible(false);
88 | button.append(button.label);
89 |
90 | button.label.set_label(item.name);
91 |
92 | box.append(button);
93 | });
94 | }
95 | },
96 | );
97 |
--------------------------------------------------------------------------------
/src/lib/signals.js:
--------------------------------------------------------------------------------
1 | import * as SignalTracker from "./signalTracker.js";
2 |
3 | const Signals = imports.signals;
4 |
5 | export class EventEmitter {
6 | connectObject(...args) {
7 | return SignalTracker.connectObject(this, ...args);
8 | }
9 |
10 | disconnectObject(...args) {
11 | return SignalTracker.disconnectObject(this, ...args);
12 | }
13 |
14 | connect_object(...args) {
15 | return this.connectObject(...args);
16 | }
17 |
18 | disconnect_object(...args) {
19 | return this.disconnectObject(...args);
20 | }
21 | }
22 |
23 | Signals.addSignalMethods(EventEmitter.prototype);
24 |
--------------------------------------------------------------------------------
/src/main.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import Gio from "gi://Gio";
4 | import GLib from "gi://GLib";
5 | import GObject from "gi://GObject";
6 | import LayerShell from "gi://Gtk4LayerShell";
7 | import { DockPanel } from "./lib/dock.js";
8 | import { DockItem, DockAppItem, PanelItem } from "./lib/dockItem.js";
9 |
10 | import App from "./app.js";
11 | import Dock from "./dock.js";
12 | import Panel from "./panel.js";
13 | import Search from "./search.js";
14 | import AppsGrid from "./appsGrid.js";
15 | import Popups from "./popups.js";
16 | import Wallpaper from "./wallpaper.js";
17 | import WindowManagerService from "./windowManager.js";
18 |
19 | import Power from "./services/power.js";
20 | import PowerProfiles from "./services/powerProfiles.js";
21 | import Brightness from "./services/brightness.js";
22 | import Monitors from "./services/monitors.js";
23 | import Inhibitor from "./services/inhibitor.js";
24 | import Login1 from "./services/login1.js";
25 | import Network from "./services/network.js";
26 | import Bluetooth from "./services/bluetooth.js";
27 | import Mounts from "./services/mounts.js";
28 | import { Volume, Mic } from "./services/volume.js";
29 | import Trash from "./services/trash.js";
30 | import Style from "./services/style.js";
31 | import DBus from "./services/dbus.js";
32 | import SystemApps from "./services/systemApps.js";
33 | import SystemActions from "./services/systemActions.js";
34 | import SystemStats from "./services/systemStats.js";
35 | import Timer from "./lib/timer.js";
36 | import Factory from "./lib/factory.js";
37 |
38 | import { Extension } from "./lib/extensionInterface.js";
39 |
40 | import "./lib/environment.js";
41 |
42 | // Initialize Gtk before you start calling anything from the import
43 | Gtk.init();
44 |
45 | globalThis.Main = {
46 | app: new App(),
47 |
48 | // timers
49 | timer: new Timer("loop timer"),
50 | loTimer: new Timer("lo-res timer"),
51 | hiTimer: new Timer("hi-res timer"),
52 |
53 | // ui
54 | factory: new Factory(),
55 | wallpaper: new Wallpaper({ name: "Wallpaper" }),
56 | dock: new Dock({ name: "Dock" }),
57 | panel: new Panel({ name: "Panel" }),
58 | search: new Search({ name: "Search" }),
59 | appsGrid: new AppsGrid({ name: "Apps-Grid" }),
60 | popups: new Popups({ name: "Popups" }),
61 |
62 | // services
63 | shell: new WindowManagerService(),
64 | apps: new SystemApps(),
65 | monitors: new Monitors(),
66 | dbus: new DBus(),
67 |
68 | // indicators
69 | power: new Power(),
70 | powerProfiles: new PowerProfiles(),
71 | mounts: new Mounts(),
72 | brightness: new Brightness(),
73 | network: new Network(),
74 | bluetooth: new Bluetooth(),
75 | volume: new Volume(),
76 | mic: new Mic(),
77 | trash: new Trash(),
78 | inhibitor: new Inhibitor(),
79 | login1: new Login1(),
80 |
81 | // misc
82 | actions: new SystemActions(),
83 |
84 | // stats
85 | stats: new SystemStats(),
86 |
87 | style: new Style({
88 | initialStyles: [
89 | { name: "app", path: "./style.css" },
90 | // {
91 | // name: 'user',
92 | // path: `${GLib.getenv('HOME')}/.config/gws/style.css`,
93 | // },
94 | ],
95 | }),
96 |
97 | // extensions
98 | extensions: {},
99 |
100 | // settings
101 | settings: new Gio.Settings({ schema: "com.github.icedman.gws" }),
102 | userSettings: {},
103 |
104 | // imports
105 | imports: {
106 | Extension,
107 | LayerShell,
108 | Dock: {
109 | DockItem,
110 | DockAppItem,
111 | PanelItem,
112 | DockPanel,
113 | },
114 | },
115 | };
116 |
117 | function demo() {
118 | {
119 | let d = new DockItem({ iconSize: 48 });
120 | d.set_label("hello");
121 | d.set_icon("user-trash");
122 | Main.dock.lead.append(d);
123 | }
124 |
125 | {
126 | let d = new DockAppItem({
127 | app: "org.gnome.Calculator.desktop",
128 | iconSize: 48,
129 | });
130 | d.set_icon_size(48);
131 | Main.dock.lead.append(d);
132 | }
133 | }
134 |
135 | // Main.app.connect('ready', () => {
136 | // demo();
137 | // });
138 |
139 | Main.app.init();
140 | Main.app.run([]);
141 |
--------------------------------------------------------------------------------
/src/panel.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import LayerShell from "gi://Gtk4LayerShell";
7 | import { Extension } from "./lib/extensionInterface.js";
8 |
9 | import { DockPanel } from "./lib/dock.js";
10 | import { DockItem, DockAppItem, PanelItem } from "./lib/dockItem.js";
11 | import { getIconInfo } from "./lib/iconInfo.js";
12 |
13 | const Panel = GObject.registerClass(
14 | class Panel extends Extension {
15 | _init(params) {
16 | this.name = params?.name ?? "Panel";
17 | delete params?.name;
18 |
19 | super._init({
20 | ...params,
21 | });
22 | }
23 |
24 | enable() {
25 | this.window = new DockPanel({
26 | title: this.name,
27 | name: this.name,
28 | hexpand: true,
29 | vexpand: true,
30 | default_width: 20,
31 | default_height: 20,
32 | });
33 |
34 | this.container = this.window.container;
35 | this.lead = this.window.lead;
36 | this.trail = this.window.trail;
37 | this.center = this.window.center;
38 |
39 | this.window.present();
40 | this.window.update_layout();
41 |
42 | super.enable();
43 | }
44 |
45 | disable() {
46 | this.window.destroy();
47 | this.window = null;
48 | super.disable();
49 | }
50 |
51 | create_panel(params) {
52 | let p = new Panel(params);
53 | return p;
54 | }
55 |
56 | create_panelitem(config = {}) {
57 | let item = new PanelItem();
58 | if (config.id) {
59 | item.add_css_class(config.id);
60 | }
61 | if (config.widget && config.id != config.widget) {
62 | item.add_css_class(config.widget);
63 | }
64 |
65 | if (config.width || config.height) {
66 | item.set_size_request(config.width ?? -1, config.height ?? -1);
67 | }
68 | item.set_icon(config.icon);
69 | item.set_label(config.label);
70 | return item;
71 | }
72 | },
73 | );
74 |
75 | export default Panel;
76 |
--------------------------------------------------------------------------------
/src/popups.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import LayerShell from "gi://Gtk4LayerShell";
7 | import { Extension } from "./lib/extensionInterface.js";
8 |
9 | const Popups = GObject.registerClass(
10 | class Popups extends Extension {
11 | _init(params) {
12 | this.name = params?.name ?? "popups";
13 | delete params?.name;
14 |
15 | super._init({
16 | ...params,
17 | });
18 | }
19 |
20 | enable() {
21 | this.style = Main.style;
22 | let _updateStyle = this.update_style.bind(this);
23 |
24 | let prefix = "popup";
25 | this.stylePrefix = prefix;
26 | this.settingsPrefix = prefix;
27 | this.settings = Main.settings;
28 | this.settingsMap = {
29 | [`${prefix}-padding`]: _updateStyle,
30 | [`${prefix}-border-radius`]: _updateStyle,
31 | [`${prefix}-border-color`]: _updateStyle,
32 | [`${prefix}-border-thickness`]: _updateStyle,
33 | [`${prefix}-foreground-color`]: _updateStyle,
34 | [`${prefix}-background-color`]: _updateStyle,
35 | };
36 |
37 | this.load_settings();
38 | this.update_style();
39 |
40 | super.enable();
41 | }
42 |
43 | async update_style() {
44 | let rads = [0, 8, 16, 20, 24, 28, 32, 36, 40, 42];
45 |
46 | let styles = [];
47 |
48 | let padding = this.PADDING ?? 0;
49 | let border = this.BORDER_THICKNESS ?? 0;
50 | let borderRadius = rads[Math.floor(this.BORDER_RADIUS)] ?? 0;
51 | let borderColor = this.style.rgba(this.BORDER_COLOR);
52 | let foregroundColor = this.style.rgba(this.FOREGROUND_COLOR);
53 | let backgroundColor = this.style.rgba(this.BACKGROUND_COLOR);
54 | let windowName = "Menu";
55 |
56 | // color
57 | {
58 | let ss = [];
59 | if (foregroundColor[3] > 0) {
60 | ss.push(`color: rgba(${foregroundColor.join(",")});`);
61 | }
62 | styles.push(`#${windowName} * { ${ss.join(" ")} }`);
63 | }
64 |
65 | {
66 | let ss = [];
67 | let pad = Math.floor(padding * 20);
68 | ss.push(`padding: ${pad}px;`);
69 | ss.push(`border: ${border}px solid rgba(${borderColor.join(",")});`);
70 | if (backgroundColor[3] > 0) {
71 | ss.push(`background: rgba(${backgroundColor.join(",")});`);
72 | }
73 | styles.push(`#${windowName} contents { ${ss.join(" ")} }`);
74 | styles.push(`#${windowName} arrow { ${ss.join(" ")} }`);
75 | }
76 |
77 | // border radius
78 | {
79 | let ss = [];
80 | ss.push(`border-radius: ${borderRadius}px;`);
81 | styles.push(`#${windowName} contents { ${ss.join(" ")} }`);
82 | }
83 | {
84 | let ss = [];
85 | ss.push(`border-radius: ${Math.floor(borderRadius * 0.6)}px;`);
86 | styles.push(`#${windowName} button { ${ss.join(" ")} }`);
87 | }
88 |
89 | // shadow
90 | if (this.ICON_SHADOW) {
91 | styles.push(
92 | `#${windowName} button { -gtk-icon-shadow: rgba(0, 0, 0, 0.6) 0 6px 6px; }`,
93 | );
94 | }
95 |
96 | // buttons in general
97 | {
98 | styles.push(`#${windowName} button { outline: none; }`);
99 | }
100 |
101 | try {
102 | console.log(styles);
103 | this.style.buildCss(`${this.stylePrefix}-style`, styles);
104 | } catch (err) {
105 | console.log(err);
106 | }
107 | }
108 |
109 | disable() {
110 | this.window.destroy();
111 | this.window = null;
112 | super.disable();
113 | }
114 | },
115 | );
116 |
117 | export default Popups;
118 |
--------------------------------------------------------------------------------
/src/services/brightness.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 |
8 | const BUS_NAME = "org.gnome.SettingsDaemon.Power";
9 | const OBJECT_PATH = "/org/gnome/SettingsDaemon/Power";
10 |
11 | const BrightnessInterface = `
12 |
13 |
14 |
15 |
16 |
17 | `;
18 |
19 | function isGsdPowerRunning() {
20 | try {
21 | let [success, output] = GLib.spawn_sync(
22 | null, // Working directory
23 | ["pgrep", "-x", "gsd-power"], // Command to check process
24 | null, // Environment
25 | GLib.SpawnFlags.SEARCH_PATH,
26 | null, // Child setup
27 | );
28 | return success && output.length > 0;
29 | } catch (e) {
30 | logError(e, "Error checking gsd-power status");
31 | return false;
32 | }
33 | }
34 |
35 | function loadGsdPower() {
36 | if (isGsdPowerRunning()) return true;
37 | let pp = [
38 | "/usr/libexec/gsd-power",
39 | "/usr/lib64/gsd-power",
40 | "/usr/lib/gsd-power",
41 | ];
42 | for (let i = 0; i < pp.length; i++) {
43 | let exec = pp[i];
44 | let fn = Gio.File.new_for_path(exec);
45 | if (!fn.query_exists(null)) continue;
46 | try {
47 | GLib.spawn_command_line_async(exec);
48 | print("gsd-power started successfully.");
49 | break;
50 | } catch (e) {
51 | // logError(e, 'Failed to start gsd-power');
52 | }
53 | }
54 | return false;
55 | }
56 |
57 | const Brightness = GObject.registerClass(
58 | {
59 | Signals: {
60 | "brightness-update": { param_types: [GObject.TYPE_OBJECT] },
61 | },
62 | },
63 | class Brightness extends Extension {
64 | _init(params) {
65 | super._init(params);
66 | this.isGsdPowerRunning = isGsdPowerRunning;
67 | this.loadGsdPower = loadGsdPower;
68 | }
69 |
70 | async enable() {
71 | super.enable();
72 | this.state = {
73 | icon: "display-brightness-symbolic",
74 | icon_index: 0,
75 | };
76 |
77 | const BrightnessProxy =
78 | Gio.DBusProxy.makeProxyWrapper(BrightnessInterface);
79 | this._proxy = new BrightnessProxy(
80 | Gio.DBus.session,
81 | BUS_NAME,
82 | OBJECT_PATH,
83 | (proxy, error) => {
84 | if (error) console.error(error.message);
85 | else this._proxy.connect("g-properties-changed", () => this.sync());
86 | this.sync();
87 | },
88 | );
89 |
90 | this.retry = 0;
91 | }
92 |
93 | disable() {
94 | super.disable();
95 | this._proxy = null;
96 | if (this.gsdSeq) {
97 | Main.loTimer.cancel(this.gsdSeq);
98 | this.gsdSeq = null;
99 | }
100 | }
101 |
102 | sync() {
103 | if (!this.enabled) return;
104 | console.log("syncing...");
105 | const brightness = this._proxy.Brightness;
106 | const visible = Number.isInteger(brightness) && brightness >= 0;
107 |
108 | this.state = {
109 | brightness,
110 | visible,
111 | icon: "display-brightness-symbolic",
112 | };
113 |
114 | this.emit("brightness-update", this);
115 | }
116 | },
117 | );
118 |
119 | export default Brightness;
120 |
--------------------------------------------------------------------------------
/src/services/dbus.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 |
8 | const BUS_NAME = "com.github.icedman.gws.controller";
9 | const OBJECT_PATH = "/com/github/icedman/gws/controller";
10 |
11 | const DBusServiceInterface = `
12 |
13 |
14 |
15 |
16 |
17 |
18 | `;
19 |
20 | const DBus = GObject.registerClass(
21 | {
22 | Signals: {
23 | "request-apps": {},
24 | "request-search": {},
25 | "request-console": {},
26 | },
27 | },
28 | class DBus extends Extension {
29 | _init(params) {
30 | super._init(params);
31 | }
32 |
33 | async enable() {
34 | try {
35 | this.dbus = Gio.DBusExportedObject.wrapJSObject(
36 | DBusServiceInterface,
37 | this,
38 | );
39 |
40 | let flags = Gio.BusNameOwnerFlags.NONE; // ALLOW_REPLACEMENT;
41 | Gio.DBus.own_name(
42 | Gio.BusType.SESSION,
43 | BUS_NAME,
44 | flags,
45 | () => {
46 | this.dbus.export(Gio.DBus.session, OBJECT_PATH);
47 | },
48 | null,
49 | () => {
50 | this.dbus.unexport();
51 | this.dbus = null;
52 | },
53 | );
54 | } catch (err) {
55 | console.log(err);
56 | }
57 | super.enable();
58 | }
59 |
60 | disable() {
61 | if (this.dbus) {
62 | this.dbus.unexport();
63 | this.dbus = null;
64 | }
65 | super.disable();
66 | }
67 |
68 | show_apps() {
69 | this.emit("request-apps");
70 | }
71 |
72 | show_search() {
73 | this.emit("request-search");
74 | }
75 |
76 | show_console() {
77 | this.emit("request-console");
78 | }
79 | },
80 | );
81 |
82 | export default DBus;
83 |
84 | // gdbus call --session --dest=com.github.icedman.gws.controller \
85 | // --object-path=/com/github/icedman/gws/controller \
86 | // --method=com.github.icedman.gws.controller.show_search
87 |
--------------------------------------------------------------------------------
/src/services/fuzzy-app-search/README.md:
--------------------------------------------------------------------------------
1 | Based on the Gnome Extension:
2 |
3 |
GNOME Fuzzy App Search
4 | ==================
5 |
6 | [Fuzzy](https://en.wikipedia.org/wiki/Approximate_string_matching) application search results for [Gnome Search](https://developer.gnome.org/SearchProvider/). Forked from [gnome-fuzzy-search](https://github.com/fffilo/gnome-fuzzy-search).
7 |
8 | ## Install
9 |
10 | ### Install from extensions.gnome.org
11 |
12 | Go to [the GNOME Extensions page of this extension](https://extensions.gnome.org/extension/3956/gnome-fuzzy-app-search/) and click on the switch to install.
13 |
14 | ### Install from AUR
15 |
16 | You can install GNOME Fuzzy App Search from the AUR package [`gnome-fuzzy-app-search-git`](https://aur.archlinux.org/packages/gnome-fuzzy-app-search-git).
17 |
18 | ### Install from source
19 |
20 | - Download and unpack [the highest release](https://gitlab.com/Czarlie/gnome-fuzzy-app-search/-/releases) from [the Gitlab repo](https://gitlab.com/Czarlie/gnome-fuzzy-app-search) or `git clone https://gitlab.com/Czarlie/gnome-fuzzy-app-search`
21 | - Run `make install` inside the `gnome-fuzzy-app-search` root directory
22 | - On X11, you can press `Alt`+`F2`, enter `r` and press `Enter` to reload extensions (and everything else, too). On Wayland, or if you choose not to reload, the extension will be loaded on your next login.
23 |
24 | ## How It Works
25 |
26 | The search query is split up into [n-grams](https://en.wikipedia.org/wiki/N-gram#n-grams_for_approximate_matching) and matched against an index of the applications you have on your system. A previous version used Levenshtein distances, but this was limited to application titles and didn't scale well (and had bugs).
27 |
28 | 
29 |
30 | ## Where is this project headed?
31 |
32 | Although [the extension this is forked from](https://github.com/fffilo/gnome-fuzzy-search) did have [plans](https://github.com/fffilo/gnome-fuzzy-search/issues/1#issuecomment-445189640) to extend more search providers, other providers currently aren't being actively developed by me, @Czarlie, but I am not entirely ruling it out.
33 |
--------------------------------------------------------------------------------
/src/services/fuzzy-app-search/fileUtils.js:
--------------------------------------------------------------------------------
1 | import GLib from "gi://GLib";
2 | import Gio from "gi://Gio";
3 |
4 | /**
5 | * Return a Promise that resolves with the contents of a file or a default
6 | * value if the file doesn't exist, or another error occurs.
7 | *
8 | * This promise never rejects, but instead returns the given defaultValue,
9 | * which is `undefined` if not passed.
10 | *
11 | * @param {string} path - The path of the file to read
12 | * @param {string} defaultValue - The value to return if an error occurs
13 | *
14 | * @return {Promise.} A Promise that resolves to the file's contents (or the default)
15 | */
16 | export const readFileOr = (path, defaultValue) => {
17 | const file = Gio.File.new_for_path(path);
18 |
19 | return new Promise((resolve) => {
20 | file.load_contents_async(null, (_src, res) => {
21 | try {
22 | const contentBytes = file.load_contents_finish(res)[1];
23 | resolve(new TextDecoder().decode(contentBytes));
24 | } catch (e) {
25 | resolve(defaultValue);
26 | }
27 | });
28 | });
29 | };
30 |
31 | /**
32 | * Write a string to a file, overwriting or creating where necessary, return an (empty) Promise.
33 | *
34 | * Does not create parent folder, rejects on error.
35 | *
36 | * @param {string} path - The path of the file to write
37 | * @param {string} string - The string to write into the file
38 | *
39 | * @return {Promise} A Promise that resolves once the file has been written.
40 | */
41 | export const writeToFile = (path, string) => {
42 | const file = Gio.File.new_for_path(path);
43 |
44 | return new Promise((resolve, reject) => {
45 | file.replace_contents_bytes_async(
46 | new GLib.Bytes(string),
47 | null,
48 | false,
49 | Gio.FileCreateFlags.REPLACE_DESTINATION,
50 | null,
51 | (_src, res) => {
52 | try {
53 | file.replace_contents_finish(res);
54 | } catch (e) {
55 | reject(e);
56 | }
57 | resolve();
58 | },
59 | );
60 | });
61 | };
62 |
63 | /**
64 | * List the files in a specified directory, in a Promise.
65 | *
66 | * If an error occurs, this promise rejects.
67 | *
68 | * @param {string} - The path of the directory
69 | *
70 | * @return {Promise.} - A Promise resolving to an array of the
71 | * filenames inside the directory.
72 | */
73 | export const listDirectory = (path) => {
74 | const files = new Set();
75 |
76 | const dirFile = Gio.File.new_for_path(path);
77 |
78 | return new Promise((resolve, reject) => {
79 | dirFile.enumerate_children_async(
80 | Gio.FILE_ATTRIBUTE_STANDARD_NAME,
81 | Gio.FileQueryInfoFlags.NONE,
82 | 0, // priority 0
83 | null,
84 | (_src, res) => {
85 | try {
86 | resolve(dirFile.enumerate_children_finish(res));
87 | } catch (e) {
88 | reject(e);
89 | }
90 | },
91 | );
92 | }).then((enumerator) => {
93 | let fileInfo;
94 | while ((fileInfo = enumerator.next_file(null))) {
95 | const name = fileInfo.get_attribute_as_string(
96 | Gio.FILE_ATTRIBUTE_STANDARD_NAME,
97 | );
98 | files.add(name);
99 | }
100 |
101 | return files;
102 | });
103 | };
104 |
--------------------------------------------------------------------------------
/src/services/fuzzy-app-search/metadata.js:
--------------------------------------------------------------------------------
1 | export const metadata = {
2 | uuid: "gnome-fuzzy-app-search@gnome-shell-extensions.Czarlie.gitlab.com",
3 | version: "5.0.15",
4 | };
5 | export default metadata;
6 |
--------------------------------------------------------------------------------
/src/services/fuzzy-app-search/scorer.js:
--------------------------------------------------------------------------------
1 | /**
2 | * A key that all objects that are indexable with an {@linkcode Index} have,
3 | * as well as a tokenizer and a weight
4 | *
5 | * @typedef {Object} KeyWeight
6 | * @property {string} [key] - The key itself. May be something like "name" or "id".
7 | * Either key or keys has to be specified, not both
8 | * @property {string} [keys] - An Array of keys to consider, like "name" and "nameEN".
9 | * Either key or keys has to be specified, not both
10 | * @property {number} weight - The weight that each token of the key's content will have.
11 | * For instance, the key "name" might have weight 8,
12 | * "keywords" weight 3 and "description" weight 1
13 | * @property {tokenizer} tokenizer - A function that returns an array of all tokens that
14 | * a value of this key contains. See {@linkcode tokenizer}
15 | */
16 |
17 | /**
18 | * Tokenize the keys of an object and assing each token a score.
19 | *
20 | * Technically, you could use this to *not* tokenize anything and
21 | * return other things mapped to scores, but I don't see the use in that.
22 | *
23 | * If you have consistent object keys and a tokenizer function for each of them,
24 | * you can use {@linkcode getTokenizedScorer} to easily get a scorer.
25 | *
26 | * @callback scorer
27 | * @param {Object} value - the object whose keys' tokens to score.
28 | * @return {Object.} An object containing all tokens found in the
29 | * passed object, each mapped to a score that
30 | * indicates how prominent that token is in the object
31 | */
32 |
33 | /**
34 | * Get a scoring function based on the passed weights for each key to be scored.
35 | *
36 | * @param {KeyWeight[]} keyWeights - An array of keys, their content type
37 | * and weights. See {@linkcode KeyWeight}
38 | * @return {scorer} A scoring function that returns tokens mapped to scores
39 | */
40 | export const getTokenizedScorer = (keyWeights) => {
41 | return (indexEntry) => {
42 | const tokenScores = {};
43 |
44 | keyWeights.forEach((keyWeight) => {
45 | const keyTokenWeights = {};
46 |
47 | if (keyWeight && "key" in keyWeight) {
48 | for (const { token, weight } of keyWeight.tokenizer(
49 | indexEntry[keyWeight.key],
50 | )) {
51 | keyTokenWeights[token] = weight;
52 | }
53 | } else {
54 | keyWeight.keys.forEach((key) => {
55 | for (const { token, weight } of keyWeight.tokenizer(
56 | indexEntry[key],
57 | )) {
58 | keyTokenWeights[token] = weight;
59 | }
60 | });
61 | }
62 |
63 | Object.keys(keyTokenWeights).forEach((token) => {
64 | if (tokenScores[token] === undefined) tokenScores[token] = 0;
65 |
66 | tokenScores[token] = Math.max(
67 | tokenScores[token],
68 | keyTokenWeights[token] * keyWeight.weight * token.length,
69 | );
70 | });
71 | });
72 |
73 | return tokenScores;
74 | };
75 | };
76 |
--------------------------------------------------------------------------------
/src/services/inhibitor.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 |
8 | const BUS_NAME = "org.freedesktop.ScreenSaver";
9 | const OBJECT_PATH = "/org/freedesktop/ScreenSaver";
10 |
11 | const GTK_APPLICATION_INHIBIT_IDLE = 8;
12 |
13 | const InhibitorInterface = `
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | `;
27 |
28 | const Inhibitor = GObject.registerClass(
29 | {
30 | Signals: {
31 | "inhibitor-update": { param_types: [GObject.TYPE_OBJECT] },
32 | },
33 | },
34 | class Inhibitor extends Extension {
35 | _init(params) {
36 | super._init(params);
37 | }
38 |
39 | async enable() {
40 | super.enable();
41 | this._icons = ["caffeine-cup-empty", "caffeine-cup-full"];
42 | this._cookie = null;
43 | this.state = {
44 | active: false,
45 | cookie: null,
46 | icon: this.get_icon(),
47 | icon_index: this.get_icon_index(),
48 | };
49 |
50 | // const InhibitorProxy = Gio.DBusProxy.makeProxyWrapper(InhibitorInterface);
51 | // this._proxy = new InhibitorProxy(
52 | // Gio.DBus.session,
53 | // BUS_NAME,
54 | // OBJECT_PATH,
55 | // (proxy, error) => {
56 | // if (error) console.error(error.message);
57 | // // console.log(
58 | // else
59 | // this._proxy.connect('g-properties-changed', () => this.sync()),
60 | // // );
61 | // this.sync();
62 | // },
63 | // );
64 | }
65 |
66 | disable() {
67 | this.uninhibit();
68 | this._proxy = null;
69 | super.disable();
70 | }
71 |
72 | get_icon_index() {
73 | return this._cookie == null ? 0 : 1;
74 | }
75 |
76 | get_icon() {
77 | return this._icons[this.get_icon_index()];
78 | }
79 |
80 | sync() {
81 | this.state = {
82 | active: this._cookie != null,
83 | cookie: this._cookie,
84 | icon: this.get_icon(),
85 | icon_index: this.get_icon_index(),
86 | };
87 |
88 | // todo ... check for other inhibitors
89 |
90 | this.emit("inhibitor-update", this);
91 | }
92 |
93 | // Inhibit the screensaver
94 | inhibit(window, reason = "by user request") {
95 | if (this._cookie) return;
96 | if (!window) {
97 | if (Main.panel.window && Main.panel.window.visible) {
98 | window = Main.panel.window;
99 | }
100 | if (!window && Main.dock.window && Main.dock.window.visible) {
101 | window = Main.dock.window;
102 | }
103 | }
104 | this._cookie = Main.app.inhibit(
105 | window,
106 | GTK_APPLICATION_INHIBIT_IDLE,
107 | reason,
108 | );
109 | console.log("Inhibited with cookie:", this._cookie);
110 |
111 | // this._proxy.call(
112 | // 'Inhibit',
113 | // new GLib.Variant('(ss)', [applicationName, reason]),
114 | // Gio.DBusCallFlags.NONE,
115 | // -1, // Timeout (-1 for default)
116 | // null, // No cancellable
117 | // (proxy, result) => {
118 | // try {
119 | // const response = proxy.call_finish(result);
120 | // this._cookie = response.deep_unpack(); // This is the inhibition cookie (uint32)
121 | // this.sync();
122 | // console.log('Inhibited with cookie:', this._cookie);
123 | // } catch (e) {
124 | // console.error('Inhibit failed:', e.message);
125 | // }
126 | // },
127 | // );
128 |
129 | this.sync();
130 | return this._cookie;
131 | }
132 |
133 | uninhibit() {
134 | if (!this._cookie) return;
135 | Main.app.uninhibit(this._cookie);
136 | this._cookie = null;
137 | this.sync();
138 |
139 | // this._proxy.call(
140 | // 'UnInhibit',
141 | // new GLib.Variant('(u)', [this._cookie]), // (u) is the type for uint32
142 | // Gio.DBusCallFlags.NONE,
143 | // -1, // Timeout (-1 for default)
144 | // null, // No cancellable
145 | // (proxy, result) => {
146 | // try {
147 | // // Call completed successfully, you can handle any return value if necessary
148 | // proxy.call_finish(result);
149 | // this._cookie = null;
150 | // this.sync();
151 | // console.log('Successfully uninhibited');
152 | // } catch (e) {
153 | // console.error('UnInhibit failed:', e.message);
154 | // }
155 | // },
156 | // );
157 | }
158 |
159 | toggle() {
160 | if (this._cookie) {
161 | this.uninhibit();
162 | } else {
163 | this.inhibit();
164 | }
165 | }
166 | },
167 | );
168 |
169 | export default Inhibitor;
170 |
--------------------------------------------------------------------------------
/src/services/login1.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 |
8 | const BUS_NAME = "org.freedesktop.login1";
9 | const OBJECT_PATH = "/org/freedesktop/login1";
10 | const ACTIVE_ICON = "caffeine-on";
11 | const INACTIVE_ICON = "caffeine-off";
12 |
13 | const Login1Interface = `
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 | `;
48 |
49 | const Login1 = GObject.registerClass(
50 | {
51 | Signals: {
52 | "login1-update": { param_types: [GObject.TYPE_OBJECT] },
53 | },
54 | },
55 | class Login1 extends Extension {
56 | _init(params) {
57 | super._init(params);
58 | }
59 |
60 | async enable() {
61 | super.enable();
62 |
63 | const Login1Proxy = Gio.DBusProxy.makeProxyWrapper(Login1Interface);
64 | try {
65 | this._proxy = new Login1Proxy(
66 | Gio.DBus.system,
67 | BUS_NAME,
68 | OBJECT_PATH,
69 | (proxy, error) => {
70 | if (error) console.error(error.message);
71 | else this._proxy.connect("g-properties-changed", () => this.sync());
72 | this.sync();
73 | },
74 | );
75 |
76 | this._proxy.connectSignal("PrepareForSleep", () => {
77 | console.log("PrepareForSleep");
78 | // Main.inhibitor.uninhibit();
79 | });
80 | } catch (err) {
81 | console.log(err);
82 | }
83 | }
84 |
85 | disable() {
86 | super.disable();
87 | }
88 |
89 | sync() {
90 | this.emit("login1-update", this);
91 | }
92 | },
93 | );
94 |
95 | export default Login1;
96 |
--------------------------------------------------------------------------------
/src/services/monitors.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 | import { pointInRectangle } from "../lib/collisions.js";
8 |
9 | const Monitors = GObject.registerClass(
10 | {
11 | Signals: {
12 | "monitors-update": { param_types: [GObject.TYPE_OBJECT] },
13 | },
14 | },
15 | class Monitors extends Extension {
16 | _init(params) {
17 | super._init(params);
18 | }
19 |
20 | async enable() {
21 | super.enable();
22 | this.state = {
23 | count: 0,
24 | };
25 |
26 | this.monitors = Gdk.Display.get_default().get_monitors();
27 | this.monitors.connectObject("items-changed", this.sync.bind(this), this);
28 | this.sync();
29 |
30 | // debug
31 | // setInterval(() => {
32 | // let m = this.getMonitorAtPointer();
33 | // if (m)
34 | // console.log(m.connector);
35 | // }, 500);
36 | }
37 |
38 | disable() {
39 | super.disable();
40 |
41 | if (this.monitors) {
42 | this.monitors.disconnectObject(this);
43 | this.monitors = null;
44 | }
45 | }
46 |
47 | getMonitor(id) {
48 | if (!id) {
49 | return this.getPrimaryMonitor();
50 | }
51 | for (let i = 0; i < this.state.count; i++) {
52 | let m = this.monitors.get_item(i);
53 | if (m.connector == id) {
54 | return m;
55 | }
56 | }
57 | return null;
58 | }
59 |
60 | getPrimaryMonitor() {
61 | return this.monitors.get_item(0);
62 | }
63 |
64 | // don't work ... unless probably a background/wallpaper window is attached
65 | getMonitorAtPosition(x, y) {
66 | for (let i = 0; i < this.state.count; i++) {
67 | let m = this.monitors.get_item(i);
68 | if (!m) break;
69 | let g = m.get_geometry();
70 | if (pointInRectangle({ x, y }, g)) {
71 | return m;
72 | }
73 | }
74 | return null;
75 | }
76 |
77 | getMonitorAtPointer(dev) {
78 | dev = dev ?? Gdk.Display.get_default()?.get_default_seat()?.get_pointer();
79 | if (!dev) return;
80 | let surface = dev.get_surface_at_position();
81 | console.log(surface);
82 | return this.getMonitorAtPosition(surface[1], surface[2]);
83 | }
84 |
85 | sync() {
86 | this.state = {
87 | count: this.monitors.get_n_items(),
88 | };
89 | this.emit("monitors-update", this);
90 | }
91 | },
92 | );
93 |
94 | export default Monitors;
95 |
96 | // m = Gdk.Display.get_default().get_monitors()
97 | // m.get_n_items()
98 | // m.get_item(0)
99 | // ls.set_monitor(Main.dock.window, m1)
100 |
--------------------------------------------------------------------------------
/src/services/mounts.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 | import { getAppInfo } from "../lib/appInfo.js";
8 |
9 | const MOUNT_PREFIX = "gws-mount-";
10 |
11 | const Mounts = GObject.registerClass(
12 | {
13 | Signals: {
14 | "mounts-update": { param_types: [GObject.TYPE_OBJECT] },
15 | },
16 | },
17 | class Mounts extends Extension {
18 | _init(params) {
19 | super._init(params);
20 | }
21 |
22 | async enable() {
23 | if (!Main.settings.get_boolean("service-mounts")) {
24 | return;
25 | }
26 |
27 | super.enable();
28 | this.state = {
29 | mounts: {},
30 | mount_ids: [],
31 | };
32 |
33 | this._volumeMonitor = Gio.VolumeMonitor.get();
34 | this._volumeMonitor.connectObject(
35 | "mount-added",
36 | this._onMountAdded.bind(this),
37 | "mount-removed",
38 | this._onMountRemoved.bind(this),
39 | this,
40 | );
41 |
42 | if (Main?.timer) {
43 | Main.timer.runOnce(() => {
44 | this.checkMounts();
45 | }, 1000);
46 | }
47 | }
48 |
49 | disable() {
50 | super.disable();
51 | if (this._volumeMonitor) {
52 | this._volumeMonitor.disconnectObject(this);
53 | this._volumeMonitor = null;
54 | }
55 | }
56 |
57 | _appName(basename) {
58 | return `${MOUNT_PREFIX}${basename}`;
59 | }
60 |
61 | _setupMountIcon(mount) {
62 | let basename = mount.get_default_location().get_basename();
63 | if (basename.startsWith("/")) {
64 | // why does this happen?? issue #125
65 | return;
66 | }
67 | let label = mount.get_name();
68 | let appname = this._appName(basename);
69 | let fullpath = mount.get_default_location().get_path();
70 | let icon = mount.get_icon().names[0] || "drive-harddisk-solidstate";
71 | let mount_exec = 'echo "not implemented"';
72 | let unmount_exec = `umount ${fullpath}`;
73 | let mount_id = `${appname}.desktop`;
74 |
75 | let execOpen = "nautilus --select";
76 | // let execOpen = 'xdg-open';
77 |
78 | // this registers the mount.desktop
79 | getAppInfo({
80 | id: mount_id,
81 | exec: `${execOpen} ${fullpath}`,
82 | name: label,
83 | icon_name: icon,
84 | menu: [
85 | {
86 | action: "open",
87 | name: "Open Window",
88 | exec: `${execOpen} ${fullpath}`,
89 | },
90 | {
91 | action: "unmount",
92 | name: "Unmount",
93 | exec: unmount_exec,
94 | },
95 | ],
96 | });
97 |
98 | this.state.mounts[mount_id] = mount;
99 | }
100 |
101 | _onMountAdded(monitor, mount) {
102 | // console.log('add');
103 | // console.log(mount);
104 | // this.last_mounted = mount;
105 | // let basename = mount.get_default_location().get_basename();
106 | // this._setupMountIcon(mount);
107 | // // remove mount_ids << add mount_ids
108 | // this.emit('mounts-update', this);
109 | this.sync();
110 | }
111 |
112 | _onMountRemoved(monitor, mount) {
113 | // console.log('remove');
114 | // console.log(mount);
115 | // let basename = mount.get_default_location().get_basename();
116 | // let appname = this._appName(basename);
117 | // let mount_id = `${appname}.desktop`;
118 | // delete this.state.mounts[mount_id];
119 | // remove mount_ids
120 | // this.emit('mounts-update', this);
121 | this.sync();
122 | }
123 |
124 | checkMounts() {
125 | if (!this._volumeMonitor) return;
126 | let mounts = this._volumeMonitor.get_mounts();
127 | let mount_ids = mounts.map((mount) => {
128 | this._setupMountIcon(mount);
129 | let basename = mount.get_default_location().get_basename();
130 | let appname = this._appName(basename);
131 | return appname + ".desktop";
132 | });
133 |
134 | this.state = {
135 | mounts,
136 | mount_ids,
137 | };
138 | }
139 |
140 | sync() {
141 | this.checkMounts();
142 | this.emit("mounts-update", this);
143 | }
144 | },
145 | );
146 |
147 | export default Mounts;
148 |
--------------------------------------------------------------------------------
/src/services/power.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import UPower from "gi://UPowerGlib";
7 | import { Extension } from "../lib/extensionInterface.js";
8 |
9 | const BUS_NAME = "org.freedesktop.UPower";
10 | const OBJECT_PATH = "/org/freedesktop/UPower/devices/DisplayDevice";
11 |
12 | const PowerDeviceInterface = `
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | `;
24 |
25 | const Power = GObject.registerClass(
26 | {
27 | Signals: {
28 | "power-update": { param_types: [GObject.TYPE_OBJECT] },
29 | },
30 | },
31 | class Power extends Extension {
32 | _init(params) {
33 | super._init(params);
34 | this.state = {};
35 | }
36 |
37 | async enable() {
38 | super.enable();
39 | this.state = {};
40 |
41 | const PowerManagerProxy =
42 | Gio.DBusProxy.makeProxyWrapper(PowerDeviceInterface);
43 |
44 | this.proxy = new PowerManagerProxy(
45 | Gio.DBus.system,
46 | BUS_NAME,
47 | OBJECT_PATH,
48 | (proxy, error) => {
49 | if (error) console.error(error.message);
50 | else
51 | this.proxy.connect("g-properties-changed", () => {
52 | this.sync();
53 | });
54 | this.sync();
55 | },
56 | );
57 | }
58 |
59 | disable() {
60 | super.disable();
61 | }
62 |
63 | sync() {
64 | let _proxy = this.proxy;
65 | let visible = _proxy.IsPresent;
66 | if (!visible) return;
67 |
68 | let chargingState =
69 | _proxy.State === UPower.DeviceState.CHARGING ? "-charging" : "";
70 | let fillLevel = 10 * Math.floor(_proxy.Percentage / 10);
71 | const charged =
72 | _proxy.State === UPower.DeviceState.FULLY_CHARGED ||
73 | (_proxy.State === UPower.DeviceState.CHARGING && fillLevel === 100);
74 |
75 | const icon = charged
76 | ? "battery-level-100-charged-symbolic"
77 | : `battery-level-${fillLevel}${chargingState}-symbolic`;
78 |
79 | this.state = {
80 | chargingState,
81 | percentage: Math.floor(_proxy.Percentage),
82 | icon,
83 | };
84 |
85 | if (_proxy.State === UPower.DeviceState.FULLY_CHARGED) {
86 | // //
87 | } else if (
88 | _proxy.State === UPower.DeviceState.CHARGING &&
89 | _proxy.TimeToFull > 0
90 | ) {
91 | this.state.timeToFull = _proxy.TimeToFull;
92 | } else if (_proxy.TimeToEmpty > 0) {
93 | this.state.timeToEmpty = _proxy.TimeToEmpty;
94 | }
95 |
96 | this.emit("power-update", this);
97 | }
98 | },
99 | );
100 |
101 | export default Power;
102 |
--------------------------------------------------------------------------------
/src/services/powerProfiles.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import UPower from "gi://UPowerGlib";
7 | import { Extension } from "../lib/extensionInterface.js";
8 |
9 | const BUS_NAME = "net.hadess.PowerProfiles";
10 | const OBJECT_PATH = "/net/hadess/PowerProfiles";
11 |
12 | function C_(section, name) {
13 | return name;
14 | }
15 |
16 | const PROFILE_PARAMS = {
17 | performance: {
18 | name: C_("Power profile", "Performance"),
19 | iconName: "power-profile-performance-symbolic",
20 | },
21 |
22 | balanced: {
23 | name: C_("Power profile", "Balanced"),
24 | iconName: "power-profile-balanced-symbolic",
25 | },
26 |
27 | "power-saver": {
28 | name: C_("Power profile", "Power Saver"),
29 | iconName: "power-profile-power-saver-symbolic",
30 | },
31 | };
32 |
33 | const PowerProfilesInterface = `
34 |
54 |
55 |
62 |
63 |
64 |
75 |
76 |
77 |
92 |
93 |
94 |
101 |
102 |
103 |
104 |
105 | `;
106 |
107 | const PowerProfiles = GObject.registerClass(
108 | {
109 | Signals: {
110 | "power-profiles-update": { param_types: [GObject.TYPE_OBJECT] },
111 | },
112 | },
113 | class PowerProfiles extends Extension {
114 | _init(params) {
115 | super._init(params);
116 | this.state = {};
117 | }
118 |
119 | async enable() {
120 | super.enable();
121 | this.state = {};
122 |
123 | const PowerProfilesProxy = Gio.DBusProxy.makeProxyWrapper(
124 | PowerProfilesInterface,
125 | );
126 |
127 | this.proxy = new PowerProfilesProxy(
128 | Gio.DBus.system,
129 | BUS_NAME,
130 | OBJECT_PATH,
131 | (proxy, error) => {
132 | if (error) console.error(error.message);
133 | else
134 | this.proxy.connect("g-properties-changed", () => {
135 | this.sync();
136 | });
137 | this.sync();
138 | },
139 | );
140 | }
141 |
142 | disable() {
143 | super.disable();
144 | }
145 |
146 | sync() {
147 | let _proxy = this.proxy;
148 | let active = _proxy.ActiveProfile;
149 | this.state = {
150 | profile: active,
151 | icon: PROFILE_PARAMS[active]?.iconName ?? "",
152 | name: PROFILE_PARAMS[active]?.name ?? "",
153 | };
154 |
155 | this.emit("power-profiles-update", this);
156 | }
157 | },
158 | );
159 |
160 | export default PowerProfiles;
161 |
--------------------------------------------------------------------------------
/src/services/systemActions.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 | import { getAppInfo, getAppInfoFromFile } from "../lib/appInfo.js";
8 |
9 | const POWER_OFF_ACTION_ID = "power-off";
10 | const RESTART_ACTION_ID = "restart";
11 | const LOCK_SCREEN_ACTION_ID = "lock-screen";
12 | const LOGOUT_ACTION_ID = "logout";
13 | const SUSPEND_ACTION_ID = "suspend";
14 |
15 | const SystemActions = GObject.registerClass(
16 | {
17 | Signals: {},
18 | },
19 | class SystemActions extends Extension {
20 | _init(params) {
21 | super._init(params);
22 |
23 | let actions = [
24 | {
25 | id: POWER_OFF_ACTION_ID,
26 | name: POWER_OFF_ACTION_ID,
27 | description: "Shutdown",
28 | icon_name: "system-shutdown-symbolic",
29 | exec: "shutdown",
30 | },
31 | {
32 | id: RESTART_ACTION_ID,
33 | name: RESTART_ACTION_ID,
34 | description: "Restart",
35 | icon_name: "system-restart-symbolic",
36 | exec: "reboot",
37 | },
38 | {
39 | id: SUSPEND_ACTION_ID,
40 | name: SUSPEND_ACTION_ID,
41 | description: "Suspend",
42 | icon_name: "system-suspend-symbolic",
43 | exec: "systemctl suspend",
44 | },
45 | {
46 | id: LOCK_SCREEN_ACTION_ID,
47 | name: LOCK_SCREEN_ACTION_ID,
48 | description: "Lock Screen",
49 | icon_name: "system-lock-screen-symbolic",
50 | script: () => {},
51 | },
52 | {
53 | id: LOGOUT_ACTION_ID,
54 | name: LOGOUT_ACTION_ID,
55 | description: "Logout",
56 | icon_name: "system-log-out-symbolic",
57 | script: () => {},
58 | },
59 | ];
60 |
61 | this.actions = {};
62 | actions.forEach((a) => {
63 | a.id = a.id + ".desktop";
64 | this.actions[a.id] = getAppInfo(a);
65 | });
66 | }
67 |
68 | async enable() {
69 | super.enable();
70 | }
71 |
72 | disable() {
73 | super.disable();
74 | }
75 | },
76 | );
77 |
78 | export default SystemActions;
79 |
--------------------------------------------------------------------------------
/src/services/systemApps.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 | import * as FileUtils from "../lib/fileUtils.js";
8 | import { getAppInfo, getAppInfoFromFile } from "../lib/appInfo.js";
9 | import { Search } from "./fuzzy-app-search/search.js";
10 |
11 | // Simple function to calculate Levenshtein distance
12 | function levenshtein(a, b) {
13 | let tmp;
14 | let i, j;
15 | let alen = a.length;
16 | let blen = b.length;
17 | let arr = [];
18 |
19 | for (i = 0; i <= alen; i++) {
20 | arr[i] = [i];
21 | }
22 | for (j = 0; j <= blen; j++) {
23 | arr[0][j] = j;
24 | }
25 | for (i = 1; i <= alen; i++) {
26 | for (j = 1; j <= blen; j++) {
27 | tmp = a.charAt(i - 1) === b.charAt(j - 1) ? 0 : 1;
28 | arr[i][j] = Math.min(
29 | arr[i - 1][j] + 1,
30 | arr[i][j - 1] + 1,
31 | arr[i - 1][j - 1] + tmp,
32 | );
33 | }
34 | }
35 | return arr[alen][blen];
36 | }
37 |
38 | const SystemApps = GObject.registerClass(
39 | {},
40 | class SystemApps extends Extension {
41 | _init(params) {
42 | super._init(params);
43 | }
44 |
45 | async collectApps() {
46 | this.apps = Gio.app_info_get_all();
47 | if (this._search) {
48 | let actions = Object.keys(Main.actions.actions).map((k) => {
49 | return Main.actions.actions[k];
50 | });
51 | this._search.refresh([...this.apps, ...actions]);
52 | }
53 | }
54 |
55 | async findAppDirs() {
56 | let dataDirs = GLib.get_system_data_dirs();
57 | dataDirs.unshift(GLib.get_user_data_dir());
58 |
59 | this.appDirs = [];
60 | for (let i = 0; i < dataDirs.length; i++) {
61 | let path = GLib.build_filenamev([dataDirs[i], "applications"]);
62 | this.appDirs.push(path);
63 | }
64 |
65 | return this.appDirs;
66 | }
67 |
68 | async enable() {
69 | super.enable();
70 | try {
71 | this._search = new Search();
72 | } catch (err) {
73 | console.log(err);
74 | }
75 |
76 | // debug only
77 | // this.findAppDirs();
78 |
79 | // let _search do its thing
80 | this.monitor = Gio.AppInfoMonitor.get();
81 | this.monitor.connectObject("changed", this.collectApps.bind(this), this);
82 | this.collectApps();
83 | }
84 |
85 | disable() {
86 | super.disable();
87 | if (this.monitor) {
88 | this.monitor.disconnectObject(this);
89 | this.monitor = null;
90 | }
91 | if (this._search) {
92 | this._search = null;
93 | }
94 | }
95 |
96 | async search(query) {
97 | if (!this._search || !this._search.isReady()) {
98 | return this.search_levenshtein(query);
99 | }
100 | return new Promise((resolve, reject) => {
101 | this._search.find(query.split(" ")).then((res) => {
102 | resolve(res.map((id) => getAppInfo(id)));
103 | });
104 | });
105 | }
106 |
107 | // Function to search applications with fuzzy matching
108 | async search_levenshtein(query) {
109 | let apps = this.apps;
110 | if (!apps) return Promise.reject("apps not available");
111 |
112 | let normalizedQuery = query.toLowerCase();
113 | let result = [];
114 | let fallback = [];
115 | for (let i = 0; i < apps.length; i++) {
116 | let app = apps[i];
117 | let appName = app.get_name().toLowerCase();
118 |
119 | // Use Levenshtein distance to calculate similarity
120 | let distance = levenshtein(normalizedQuery, appName);
121 |
122 | // Define a threshold for fuzzy match (lower is more lenient)
123 | if (distance <= 3) {
124 | // Adjust this threshold for better results
125 | result.push({ app, distance });
126 | }
127 |
128 | if (
129 | result.length == 0 &&
130 | normalizedQuery.length > 3 &&
131 | appName.includes(normalizedQuery)
132 | ) {
133 | fallback.push({ app, distance: 0 });
134 | }
135 | }
136 |
137 | if (result.length == 0) {
138 | result = fallback;
139 | }
140 |
141 | // Sort by the closest match (smallest distance)
142 | result.sort((a, b) => a.distance - b.distance);
143 |
144 | // Return the sorted apps, without the distance information
145 | return Promise.resolve(
146 | result.map((item) => getAppInfo(item.app.get_id() ?? "xxx")),
147 | );
148 | }
149 | },
150 | );
151 |
152 | export default SystemApps;
153 |
--------------------------------------------------------------------------------
/src/services/trash.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import { Extension } from "../lib/extensionInterface.js";
7 |
8 | const TRASH_UPDATE_INTERVAL = 1000 * 45; // every 45 seconds
9 | const TRASH_URI = "trash:///";
10 |
11 | const Trash = GObject.registerClass(
12 | {
13 | Signals: {
14 | "trash-update": { param_types: [GObject.TYPE_OBJECT] },
15 | },
16 | },
17 | class Trash extends Extension {
18 | _init(params) {
19 | super._init(params);
20 | this.state = {};
21 | }
22 |
23 | async enable() {
24 | if (!Main.settings.get_boolean("service-trash")) {
25 | return;
26 | }
27 |
28 | super.enable();
29 | this.monitorTrash();
30 | this.sync();
31 | }
32 |
33 | disable() {
34 | super.disable();
35 | }
36 |
37 | monitorTrash() {
38 | this._trashDir = Gio.File.new_for_uri(TRASH_URI);
39 | this._trashMonitor = this._trashDir.monitor_directory(
40 | // Gio.FileMonitorFlags.WATCH_MOVES,
41 | 0,
42 | null,
43 | );
44 | this._trashMonitor.connect(
45 | "changed",
46 | (fileMonitor, file, otherFile, eventType) => {
47 | // console.log(eventType);
48 | this.sync();
49 | clearInterval(this._scheduleId);
50 | this._scheduleId = setInterval(() => {
51 | this.sync();
52 | }, TRASH_UPDATE_INTERVAL);
53 | },
54 | );
55 |
56 | this._scheduleId = setInterval(() => {
57 | this.sync();
58 | }, TRASH_UPDATE_INTERVAL);
59 | }
60 |
61 | checkTrash() {
62 | if (!this._trashDir) {
63 | return false;
64 | }
65 |
66 | let prevFull = this.state.full ?? false;
67 | let iter = this._trashDir.enumerate_children(
68 | "standard::*",
69 | Gio.FileQueryInfoFlags.NONE,
70 | null,
71 | );
72 | return iter.next_file(null) != null;
73 | }
74 |
75 | sync() {
76 | let prevFull = this.state.full ?? false;
77 | this.state = {
78 | full: this.checkTrash(),
79 | };
80 | this.emit("trash-update", this);
81 | }
82 | },
83 | );
84 |
85 | export default Trash;
86 |
--------------------------------------------------------------------------------
/src/services/volume.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import GLib from "gi://GLib";
4 | import Gio from "gi://Gio";
5 | import GObject from "gi://GObject";
6 | import Gvc from "gi://Gvc";
7 | import { Extension } from "../lib/extensionInterface.js";
8 |
9 | // Each Gvc.MixerControl is a connection to PulseAudio,
10 | // so it's better to make it a singleton
11 | let _mixerControl;
12 |
13 | /**
14 | * @returns {Gvc.MixerControl} - the mixer control singleton
15 | */
16 | export function getMixerControl() {
17 | if (_mixerControl) return _mixerControl;
18 |
19 | _mixerControl = new Gvc.MixerControl({ name: "GNOME Shell Volume Control" });
20 | _mixerControl.open();
21 | return _mixerControl;
22 | }
23 |
24 | const Volume = GObject.registerClass(
25 | {
26 | Signals: {
27 | "volume-update": { param_types: [GObject.TYPE_OBJECT] },
28 | },
29 | },
30 | class Volume extends Extension {
31 | _init(params) {
32 | super._init(params);
33 | }
34 |
35 | async enable() {
36 | super.enable();
37 | this.state = {};
38 |
39 | this._icons = [
40 | "audio-volume-muted-symbolic",
41 | "audio-volume-low-symbolic",
42 | "audio-volume-medium-symbolic",
43 | "audio-volume-high-symbolic",
44 | "audio-volume-overamplified-symbolic",
45 | ];
46 |
47 | this._control = getMixerControl();
48 | this._control.connect("state-changed", () => {
49 | this.sync();
50 | });
51 | this._control.connect("default-sink-changed", () => {
52 | this.sync();
53 | });
54 | this._control.connect("active-output-update", () => {
55 | this.sync();
56 | });
57 |
58 | this.sync();
59 | }
60 |
61 | disable() {
62 | super.disable();
63 | }
64 |
65 | get_icon_index() {
66 | if (!this._stream) return null;
67 |
68 | let volume = this._stream.volume;
69 | let n;
70 | if (this._stream.is_muted || volume <= 0) {
71 | n = 0;
72 | } else {
73 | n = Math.ceil((3 * volume) / this._control.get_vol_max_norm());
74 | n = Math.clamp(n, 1, this._icons.length - 1);
75 | }
76 | return n;
77 | }
78 |
79 | get_icon() {
80 | return this._icons[this.get_icon_index()];
81 | }
82 |
83 | sync() {
84 | let stream = this._control?.get_default_sink();
85 | if (!stream) return;
86 | if (stream != this._stream) {
87 | stream.connect("notify::is-muted", () => {
88 | this.sync();
89 | });
90 | stream.connect("notify::volume", () => {
91 | this.sync();
92 | });
93 | this._stream = stream;
94 | }
95 |
96 | this.state = {
97 | ready: this._control?.get_state() === Gvc.MixerControlState.READY,
98 | icon: this.get_icon(),
99 | icon_index: this.get_icon_index(),
100 | is_muted: this._stream.is_muted,
101 | volume: this._stream.volume,
102 | max_volume: this._control.get_vol_max_norm(),
103 | level: 0,
104 | };
105 | this.state.level =
106 | (100 * this.state.volume) / (this.state.max_volume ?? 1);
107 |
108 | this.emit("volume-update", this);
109 | }
110 | },
111 | );
112 |
113 | const Mic = GObject.registerClass(
114 | {
115 | Signals: {
116 | "mic-update": { param_types: [GObject.TYPE_OBJECT] },
117 | },
118 | },
119 | class Mic extends Extension {
120 | _init(params) {
121 | super._init(params);
122 | }
123 |
124 | async enable() {
125 | this.state = {};
126 |
127 | this._icons = [
128 | "microphone-sensitivity-muted-symbolic",
129 | "microphone-sensitivity-low-symbolic",
130 | "microphone-sensitivity-medium-symbolic",
131 | "microphone-sensitivity-high-symbolic",
132 | ];
133 |
134 | this._control = getMixerControl();
135 | this._control.connect("state-changed", () => {
136 | this.sync();
137 | });
138 | this._control.connect("default-source-changed", () => {
139 | this.sync();
140 | });
141 | this._control.connect("active-input-update", () => {
142 | this.sync();
143 | });
144 |
145 | this.sync();
146 | }
147 |
148 | disable() {}
149 |
150 | get_icon_index() {
151 | if (!this._stream) return null;
152 |
153 | let volume = this._stream.volume;
154 | let n;
155 | if (this._stream.is_muted || volume <= 0) {
156 | n = 0;
157 | } else {
158 | n = Math.ceil((3 * volume) / this._control.get_vol_max_norm());
159 | n = Math.clamp(n, 1, this._icons.length - 1);
160 | }
161 | return n;
162 | }
163 |
164 | get_icon() {
165 | return this._icons[this.get_icon_index()];
166 | }
167 |
168 | sync() {
169 | let stream = this._control?.get_default_source();
170 | if (!stream) return;
171 | if (stream != this._stream) {
172 | stream.connect("notify::is-muted", () => {
173 | this.sync();
174 | });
175 | stream.connect("notify::volume", () => {
176 | this.sync();
177 | });
178 | this._stream = stream;
179 | }
180 |
181 | this.state = {
182 | ready: this._control?.get_state() === Gvc.MixerControlState.READY,
183 | icon: this.get_icon(),
184 | };
185 |
186 | this.emit("mic-update", this);
187 | }
188 | },
189 | );
190 |
191 | export { Volume, Mic };
192 |
--------------------------------------------------------------------------------
/src/style.css:
--------------------------------------------------------------------------------
1 | /* Wallpaper */
2 | #Wallpaper {
3 | /* border: 2px solid red;*/
4 | /* background: yellow;*/
5 | }
6 |
7 | /* Dock */
8 | #Dock {
9 | background: transparent;
10 | opacity: 1;
11 | transition: opacity 0.45s ease-in-out;
12 | }
13 |
14 | #Dock.startup {
15 | opacity: 0;
16 | }
17 |
18 | .dock-item .button {
19 | border: none;
20 | background: rgba(0,0,0,0);
21 | padding: 2px;
22 | }
23 |
24 | .dock-item .button image {
25 | color: rgba(255,255,255,1);
26 | }
27 |
28 | .dock-item .button:hover {
29 | border: none;
30 | background: rgba(0,0,0,0.5);
31 | }
32 |
33 | /* Bar */
34 | #Panel {
35 | background: transparent;
36 | opacity: 1;
37 | transition: opacity 0.45s ease-in-out;
38 | }
39 |
40 | #Panel.startup {
41 | opacity: 0;
42 | }
43 |
44 | .panel-item .icon {
45 | margin: 0px;
46 | padding: 0px;
47 | padding-left: 8px;
48 | padding-right: 8px;
49 | }
50 |
51 | /* Separator */
52 | #Dock separator,
53 | #Panel separator {
54 | transform: scale(0.7);
55 | margin: 4px;
56 | background: rgba(150, 150, 150, 0.6);
57 | }
58 |
59 | /* Common */
60 | #lead, #center, #trail {
61 | }
62 |
63 | #container {
64 | }
65 |
66 | /* Effects */
67 | #Dock.startup, #Panel.startup {
68 | opacity: 0;
69 | }
70 |
71 | /* Menu */
72 | #Menu {
73 | margin-bottom: 10px;
74 | }
75 |
76 | #Menu contents {
77 | background: rgba(0,0,0,0.9);
78 | }
79 |
80 | #Menu arrow {
81 | background: rgba(0,0,0,0.9);
82 | }
83 |
84 | #Menu * {
85 | color: rgba(200,200,200,1);
86 | }
87 |
88 | #MenuItem {
89 | border-radius: 4px;
90 | margin: 0px;
91 | padding: 4px;
92 | }
93 |
94 | #MenuItem:hover {
95 | background: rgba(150,150,150,0.2);
96 | }
97 |
98 | #Menu button {
99 | border: 0px;
100 | padding: 0px;
101 | background: transparent;
102 | }
103 |
104 | /* Search */
105 | #Search {
106 | background: transparent;
107 | opacity: 1;
108 | transition: opacity 0.25s ease-in-out;
109 | }
110 | #Search.startup {
111 | opacity: 0;
112 | }
113 | #Search .entry-container {
114 | padding: 2px;
115 | background: rgba(10,10,10,0.95);
116 | }
117 | #Search.has-results {
118 | background: rgba(10,10,10,0.95);
119 | }
120 | #Search.has-results .entry-container {
121 | background: transparent;
122 | }
123 | #Search scrolledwindow {
124 | padding: 4px;
125 | }
126 | #Search entry {
127 | outline: none;
128 | }
129 | #Search entry {
130 | padding: 8px;
131 | border: 0px;
132 | background: transparent;
133 | margin: 4px;
134 | }
135 |
136 | #Search .results-view {
137 | }
138 | #Search .results-apps {
139 | }
140 |
141 | #Search .results-apps .button {
142 | border: none;
143 | background: transparent;
144 | }
145 | #Search .results-apps .button:hover {
146 | background: rgba(50,50,50,0.6);
147 | }
148 | #Search .result-row {
149 | padding: 8px;
150 | background: transparent;
151 | border: 0px;
152 | }
153 | #Search .result-row:hover,
154 | #Search .result-row:focus {
155 | background: rgba(50,50,50,0.6);
156 | }
157 | #Search .result-name {
158 | font-weight: bold;
159 | }
160 | #Search .result-icon,
161 | #Search .result-name,
162 | #Search .result-description {
163 | padding-left: 4px;
164 | padding-right: 4px;
165 | }
166 |
167 | /* Apps-Grid */
168 | #Apps-Grid {
169 | background: transparent;
170 | opacity: 1;
171 | transition: opacity 0.25s ease-in-out;
172 | }
173 | #Apps-Grid.startup {
174 | opacity: 0;
175 | }
176 | #Apps-Grid .entry-container {
177 | padding: 2px;
178 | background: rgba(10,10,10,0.95);
179 | }
180 | #Apps-Grid.has-results {
181 | background: rgba(10,10,10,0.95);
182 | }
183 | #Apps-Grid.has-results .entry-container {
184 | background: transparent;
185 | }
186 | #Apps-Grid scrolledwindow {
187 | padding: 4px;
188 | }
189 | #Apps-Grid entry {
190 | outline: none;
191 | }
192 | #Apps-Grid entry {
193 | padding: 8px;
194 | border: 0px;
195 | background: transparent;
196 | margin: 4px;
197 | }
198 |
199 | #Apps-Grid gridview {
200 | padding: 10px;
201 | }
202 |
203 | #Apps-Grid gridview,
204 | #Apps-Grid gridview * {
205 | background: transparent;
206 | border: 0px;
207 | }
208 |
209 | /* animation */
210 | @keyframes bounce {
211 | 0% {
212 | opacity: 0;
213 | /* transform: translateY(0);*/
214 | }
215 | 30% {
216 | opacity: 0.3;
217 | /* transform: translateY(-8px);*/
218 | }
219 | 50% {
220 | opacity: 0.5;
221 | /* transform: translateY(0);*/
222 | }
223 | 70% {
224 | opacity: 0.7;
225 | /* transform: translateY(-3px);*/
226 | }
227 | 100% {
228 | opacity: 1.0;
229 | /* transform: translateY(0);*/
230 | }
231 | }
232 |
233 | /* misc */
234 | .bounce-icon image {
235 | animation: bounce 1s ease 2; /* Play twice */
236 | }
237 |
238 | .with-indicators #container .button {
239 | padding-top: 2px;
240 | padding-left: 2px;
241 | padding-right: 2px;
242 | padding-bottom: 6px;
243 | }
244 |
245 | .dots {
246 | transform: translateY(10px);
247 | }
248 |
--------------------------------------------------------------------------------
/src/ui/apps.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | vertical
5 | true
6 |
7 |
8 | vertical
9 | true
10 |
11 |
12 | true
13 |
14 |
15 | true
16 |
17 |
18 |
19 |
20 |
21 |
22 | true
23 | true
24 |
25 |
26 | true
27 | true
28 | true
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/archlinux-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/debian-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/fedora-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/kalilinux-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/linuxmint-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/manjaro-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/ubuntu-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/places/zorin-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/cpu-alt-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/cpu-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/hard-disk-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/hard-drive-2-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/hard-drive-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/memory-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/storage-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/triangle-down-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/triangle-left-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/triangle-right-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/icons/hicolor/scalable/status/triangle-up-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/ui/result-row.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | horizontal
7 | false
8 |
9 |
10 | false
11 | false
12 | 24
13 |
14 |
15 |
16 |
17 | true
18 | vertical
19 |
20 |
21 | start
22 |
23 |
24 |
25 |
26 | start
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/ui/search.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | vertical
5 | true
6 |
7 |
8 | vertical
9 | true
10 |
11 |
12 | true
13 |
14 |
15 | true
16 |
17 |
18 |
19 |
20 |
21 |
22 | true
23 | true
24 |
25 |
26 | vertical
27 |
28 |
29 | false
30 | false
31 | center
32 | horizontal
33 |
34 |
35 |
36 |
37 | false
38 | vertical
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
--------------------------------------------------------------------------------
/src/user-extensions:
--------------------------------------------------------------------------------
1 | /home/iceman/.config/gws/extensions
--------------------------------------------------------------------------------
/src/wallpaper.js:
--------------------------------------------------------------------------------
1 | import Gdk from "gi://Gdk?version=4.0";
2 | import Gtk from "gi://Gtk?version=4.0";
3 | import Gsk from "gi://Gsk";
4 | import GLib from "gi://GLib";
5 | import Gio from "gi://Gio";
6 | import GObject from "gi://GObject";
7 | import LayerShell from "gi://Gtk4LayerShell";
8 | import { PopupMenu } from "./lib/popupMenu.js";
9 | import { Dot } from "./lib/dot.js";
10 | import { Extension } from "./lib/extensionInterface.js";
11 | import { getAppInfo, getAppInfoFromFile } from "./lib/appInfo.js";
12 | import { pointInRectangle, distanceToRectangle } from "./lib/collisions.js";
13 | import { pointerInWindow, getModifierStates } from "./lib/devices.js";
14 |
15 | import { Background } from "./lib/background.js";
16 |
17 | const Wallpaper = GObject.registerClass(
18 | class Wallpaper extends Extension {
19 | _init(params) {
20 | this.name = params?.name ?? "Wallpaper";
21 | delete params?.name;
22 | super._init({
23 | ...(params ?? {}),
24 | });
25 | }
26 |
27 | enable() {
28 | this.window = new Background({
29 | name: this.name,
30 | hexpand: true,
31 | vexpand: true,
32 | });
33 | super.enable();
34 |
35 | this.window.present();
36 |
37 | let m = Main.monitors.getPrimaryMonitor();
38 | let g = m.get_geometry();
39 | this.window.set_size_request(g.width, g.height);
40 | }
41 |
42 | disable() {
43 | super.disable();
44 | }
45 | },
46 | );
47 |
48 | export default Wallpaper;
49 |
--------------------------------------------------------------------------------
/src/windowManager.js:
--------------------------------------------------------------------------------
1 | import { WindowManagerInterface } from "./compositors/wmInterface.js";
2 | import NiriShell from "./compositors/niri.js";
3 | import HyprShell from "./compositors/hyprland.js";
4 | import SwayShell from "./compositors/sway.js";
5 | import DwlShell from "./compositors/dwl.js";
6 |
7 | function WindowManagerService(wm) {
8 | let supportedWM = {
9 | niri: NiriShell,
10 | hyprland: HyprShell,
11 | sway: SwayShell,
12 | dwl: DwlShell,
13 | labwc: DwlShell,
14 | mutter: DwlShell,
15 | };
16 |
17 | let testWMs = Object.keys(supportedWM);
18 | if (wm && supportedWM[wm]) {
19 | testWMs = [supportedWM[wm]];
20 | }
21 |
22 | for (let i = 0; i < testWMs.length; i++) {
23 | let target = testWMs[i];
24 | console.log(`checking ${target}...`);
25 | let wm = new supportedWM[target]();
26 | if (wm.isAvailable(target)) {
27 | console.log(`${target} found running`);
28 | return wm;
29 | }
30 | }
31 |
32 | return new WindowManagerInterface();
33 | }
34 |
35 | export default WindowManagerService;
36 |
--------------------------------------------------------------------------------
/tests/bluetooth.js:
--------------------------------------------------------------------------------
1 | // import
2 |
--------------------------------------------------------------------------------
/tests/clipboard.js:
--------------------------------------------------------------------------------
1 | import Gdk from 'gi://Gdk?version=4.0';
2 | import Gtk from 'gi://Gtk?version=4.0';
3 | import GLib from 'gi://GLib';
4 | import Gio from 'gi://Gio';
5 | import GObject from 'gi://GObject';
6 | // import ByteArray from 'gi://byteArray';
7 |
8 | Gtk.init();
9 |
10 | let clipboard = Gdk.Display.get_default().get_clipboard();
11 | // let contentProvider = Gdk.ContentProvider.new_for_value(new GLib.Variant('s', 'hello world'));
12 | // clipboard.set_content(contentProvider);
13 | // clipboard.set_text('hi');
14 |
15 | let string = 'Hello World!';
16 | // const data = Uint8Array.from(string.split("").map(x => x.charCodeAt()))
17 | const data = new TextEncoder().encode(string);
18 |
19 | let provider = Gdk.ContentProvider.new_for_bytes('text/plain', data);
20 | clipboard.set_content(provider);
21 | console.log(provider.formats);
22 |
23 | // let cc = clipboard.get_content();
24 | // let val = cc.get_value();
25 |
26 | console.log(clipboard);
27 |
--------------------------------------------------------------------------------
/tests/dbus:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/icedman/gjs-wayland-shell/cedee2ea0c30b3d847b9c558b9cb0295406a777b/tests/dbus
--------------------------------------------------------------------------------
/tests/dbus.c:
--------------------------------------------------------------------------------
1 | #include
2 | #include
3 |
4 | #define SERVICE_NAME "com.example.MyService"
5 | #define OBJECT_PATH "/com/example/MyService"
6 | #define INTERFACE_NAME "com.example.MyInterface"
7 |
8 | // static gboolean handle_hello_world(GDBusMethodInvocation *invocation, gpointer user_data) {
9 | // const char *response = "Hello, world!";
10 | // g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", response));
11 | // return TRUE;
12 | // }
13 |
14 | // static const GDBusInterfaceVTable interface_vtable = {
15 | // .method_call = NULL,
16 | // .get_property = NULL,
17 | // .set_property = NULL,
18 | // };
19 |
20 | // Property storage
21 | static gchar *property_message = NULL;
22 | static gint property_count = 0;
23 |
24 | // Method handler
25 | static void handle_method_call(GDBusConnection *connection, const gchar *sender, const gchar *object_path,
26 | const gchar *interface_name, const gchar *method_name, GVariant *parameters,
27 | GDBusMethodInvocation *invocation, gpointer user_data) {
28 | if (g_strcmp0(method_name, "HelloWorld") == 0) {
29 | const char *response = "Hello from D-Bus!";
30 | g_print("HelloWorld method called by %s\n", sender);
31 | g_dbus_method_invocation_return_value(invocation, g_variant_new("(s)", response));
32 | } else {
33 | g_dbus_method_invocation_return_error(invocation, G_DBUS_ERROR, G_DBUS_ERROR_UNKNOWN_METHOD,
34 | "Unknown method: %s", method_name);
35 | }
36 | }
37 |
38 | // Property handlers
39 | static GVariant *get_property(GDBusConnection *connection, const gchar *sender, const gchar *object_path,
40 | const gchar *interface_name, const gchar *property_name, GError **error, gpointer user_data) {
41 | if (g_strcmp0(property_name, "Message") == 0) {
42 | return g_variant_new_string(property_message ? property_message : "Default Message");
43 | } else if (g_strcmp0(property_name, "Count") == 0) {
44 | return g_variant_new_int32(property_count);
45 | }
46 | return NULL; // Property not found
47 | }
48 |
49 | static gboolean set_property(GDBusConnection *connection, const gchar *sender, const gchar *object_path,
50 | const gchar *interface_name, const gchar *property_name, GVariant *value, GError **error, gpointer user_data) {
51 | if (g_strcmp0(property_name, "Message") == 0) {
52 | g_free(property_message);
53 | property_message = g_strdup(g_variant_get_string(value, NULL));
54 | return TRUE;
55 | }
56 | g_set_error(error, G_DBUS_ERROR, G_DBUS_ERROR_PROPERTY_READ_ONLY, "The 'Count' property is read-only.");
57 | return FALSE;
58 | }
59 |
60 | // VTable
61 | static const GDBusInterfaceVTable interface_vtable = {
62 | .method_call = handle_method_call,
63 | .get_property = get_property,
64 | .set_property = set_property,
65 | };
66 |
67 |
68 | static GDBusNodeInfo *introspection_data = NULL;
69 |
70 | const gchar *introspection_xml =
71 | ""
72 | " "
73 | " "
74 | " "
75 | " "
76 | " "
77 | " "
78 | " "
79 | "";
80 |
81 | static void on_bus_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) {
82 | GError *error = NULL;
83 |
84 | // Register the object
85 | guint registration_id = g_dbus_connection_register_object(
86 | connection,
87 | OBJECT_PATH,
88 | introspection_data->interfaces[0],
89 | &interface_vtable,
90 | NULL, // user data
91 | NULL, // user data free function
92 | &error);
93 |
94 | if (registration_id == 0) {
95 | g_printerr("Failed to register object: %s\n", error->message);
96 | g_error_free(error);
97 | }
98 | }
99 |
100 | static void on_name_acquired(GDBusConnection *connection, const gchar *name, gpointer user_data) {
101 | g_print("Service name '%s' acquired.\n", SERVICE_NAME);
102 | }
103 |
104 | static void on_name_lost(GDBusConnection *connection, const gchar *name, gpointer user_data) {
105 | g_print("Service name '%s' lost.\n", SERVICE_NAME);
106 | }
107 |
108 | int main(int argc, char *argv[]) {
109 | GMainLoop *loop;
110 | guint owner_id;
111 |
112 | // Create introspection data
113 | introspection_data = g_dbus_node_info_new_for_xml(introspection_xml, NULL);
114 |
115 | // Acquire the bus name
116 | owner_id = g_bus_own_name(
117 | G_BUS_TYPE_SESSION,
118 | SERVICE_NAME,
119 | G_BUS_NAME_OWNER_FLAGS_NONE,
120 | on_bus_acquired,
121 | on_name_acquired,
122 | on_name_lost,
123 | NULL,
124 | NULL);
125 |
126 | // Run the main loop
127 | loop = g_main_loop_new(NULL, FALSE);
128 | g_main_loop_run(loop);
129 |
130 | // Cleanup
131 | g_bus_unown_name(owner_id);
132 | g_dbus_node_info_unref(introspection_data);
133 | g_main_loop_unref(loop);
134 |
135 | return 0;
136 | }
137 |
--------------------------------------------------------------------------------
/tests/ff.js:
--------------------------------------------------------------------------------
1 | import Gdk from 'gi://Gdk?version=4.0';
2 | import Gtk from 'gi://Gtk?version=4.0';
3 | import GLib from 'gi://GLib';
4 | import Gio from 'gi://Gio';
5 |
6 | // Initialize GTK
7 | Gtk.init();
8 |
9 | let provider = new Gtk.CssProvider();
10 | provider.load_from_path('style.css');
11 | Gtk.StyleContext.add_provider_for_display(
12 | Gdk.Display.get_default(),
13 | provider,
14 | Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION,
15 | );
16 |
17 | // Create a GTK window
18 | const window = new Gtk.Window({
19 | title: 'Fastfetch Output',
20 | default_width: 800,
21 | default_height: 600,
22 | });
23 | window.connect('destroy', () => Gtk.main_quit());
24 |
25 | // Create a scrolled window and text view
26 | const scrolledWindow = new Gtk.ScrolledWindow();
27 | const textView = new Gtk.TextView({
28 | editable: false,
29 | wrap_mode: Gtk.WrapMode.WORD,
30 | });
31 | scrolledWindow.set_child(textView);
32 | window.set_child(scrolledWindow);
33 |
34 | // Run fastfetch and capture output
35 | function runCommand(command, args) {
36 | const subprocess = new Gio.Subprocess({
37 | argv: [command, ...args],
38 | flags: Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE,
39 | });
40 |
41 | subprocess.init(null);
42 | const stdout = subprocess.communicate_utf8(null, null)[1];
43 | return stdout;
44 | }
45 |
46 | // Parse ANSI color codes (basic example, extend as needed)
47 | function parseAnsiToGtk(textBuffer, text) {
48 | const iter = textBuffer.get_end_iter(); // Get iterator for the end of the buffer
49 | console.log(text);
50 | textBuffer.insert(iter, text, text.length);
51 | return;
52 | }
53 |
54 | // Insert fastfetch output into text view
55 | const output = runCommand('fastfetch', []); // Replace 'fastfetch' with 'neofetch' as needed
56 | const textBuffer = textView.get_buffer();
57 | parseAnsiToGtk(textBuffer, output);
58 |
59 | // Show the window
60 | window.show();
61 | // Gtk.main();
62 |
63 | let loop = GLib.MainLoop.new(null, false);
64 | loop.run();
65 |
--------------------------------------------------------------------------------
/tests/grid.js:
--------------------------------------------------------------------------------
1 | const { Gtk, Gio, GObject } = imports.gi;
2 |
3 | // Initialize GTK
4 | Gtk.init(null);
5 |
6 | // Define a simple model for the GridView
7 | const ListModel = GObject.registerClass(
8 | {},
9 | class ListModel extends GObject.Object {
10 | _init() {
11 | super._init();
12 | }
13 |
14 | static [GObject.properties] = {
15 | label: GObject.ParamSpec.string(
16 | 'label',
17 | 'Label',
18 | 'Button Label',
19 | GObject.ParamFlags.READWRITE,
20 | '',
21 | ),
22 | icon_name: GObject.ParamSpec.string(
23 | 'icon_name',
24 | 'Icon Name',
25 | 'Icon Name',
26 | GObject.ParamFlags.READWRITE,
27 | '',
28 | ),
29 | };
30 |
31 | _init(label, icon_name) {
32 | super._init();
33 | this.label = label;
34 | this.icon_name = icon_name;
35 | }
36 | },
37 | );
38 |
39 | // Create a button widget to represent each item in the grid
40 | function createButtonWidget(item) {
41 | const button = new Gtk.Button();
42 |
43 | const vbox = new Gtk.Box({
44 | orientation: Gtk.Orientation.VERTICAL,
45 | spacing: 5,
46 | });
47 |
48 | const icon = new Gtk.Image({
49 | icon_name: item.icon_name,
50 | pixel_size: 48,
51 | });
52 |
53 | const label = new Gtk.Label({ label: item.label });
54 |
55 | vbox.append(icon);
56 | vbox.append(label);
57 |
58 | button.set_child(vbox);
59 |
60 | return button;
61 | }
62 |
63 | // App Activation
64 | function onActivate() {
65 | // Create a new window
66 | const window = new Gtk.Window({
67 | title: 'GridView Example',
68 | default_width: 400,
69 | default_height: 300,
70 | });
71 |
72 | // Connect the destroy signal to exit the application
73 | window.connect('destroy', () => Gtk.main_quit());
74 |
75 | // Create a ListStore to hold the data
76 | const items = new Gio.ListStore({ item_type: ListModel });
77 |
78 | // Add items to the ListStore
79 | items.append(new ListModel('Open Folder', 'folder-open-symbolic'));
80 | items.append(new ListModel('Save', 'document-save-symbolic'));
81 | items.append(new ListModel('Trash', 'user-trash-symbolic'));
82 | items.append(new ListModel('Settings', 'emblem-system-symbolic'));
83 |
84 | // Create a GridView
85 | const gridView = new Gtk.GridView({
86 | model: items,
87 | enable_rubberband: false,
88 | });
89 |
90 | // Define how each item in the grid should look
91 | gridView.set_factory(
92 | Gtk.SignalListItemFactory.new((listItem) => {
93 | // Bind the item's data to the button widget
94 | const item = listItem.get_item();
95 | if (item) {
96 | const button = createButtonWidget(item);
97 | listItem.set_child(button);
98 | }
99 | }),
100 | );
101 |
102 | // Add the GridView to the window
103 | window.set_child(gridView);
104 |
105 | // Show the window
106 | window.show();
107 | }
108 |
109 | // Create the application
110 | const app = new Gtk.Application({
111 | application_id: 'com.example.gridview',
112 | flags: Gio.ApplicationFlags.FLAGS_NONE,
113 | });
114 |
115 | app.connect('activate', onActivate);
116 |
117 | // Run the application
118 | app.run([]);
119 |
--------------------------------------------------------------------------------
/tests/icons.js:
--------------------------------------------------------------------------------
1 | import Gdk from 'gi://Gdk?version=4.0';
2 | import Gtk from 'gi://Gtk?version=4.0';
3 | import GdkPixbuf from 'gi://GdkPixbuf';
4 | import GObject from 'gi://GObject';
5 | import GLib from 'gi://GLib';
6 | import Gio from 'gi://Gio';
7 |
8 | Gtk.init();
9 |
10 | function createIconGridView() {
11 | const window = new Gtk.Window({
12 | title: 'Icon Grid Example',
13 | default_width: 400,
14 | default_height: 300,
15 | });
16 |
17 | window.connect('close-request', () => {
18 | loop.quit();
19 | return true;
20 | });
21 |
22 | // Define the object to hold data (icon name and label)
23 | const IconItem = GObject.registerClass(
24 | {
25 | Properties: {
26 | iconName: GObject.ParamSpec.string(
27 | 'iconName',
28 | 'Icon Name',
29 | 'The icon name',
30 | GObject.ParamFlags.READWRITE,
31 | '',
32 | ),
33 | label: GObject.ParamSpec.string(
34 | 'label',
35 | 'Label',
36 | 'The label text',
37 | GObject.ParamFlags.READWRITE,
38 | '',
39 | ),
40 | },
41 | },
42 | class IconItem extends GObject.Object {
43 | constructor(props) {
44 | super(props);
45 | }
46 | },
47 | );
48 |
49 | // Create a Gio.ListStore and set the item type to IconItem
50 | const listStore = new Gio.ListStore({ item_type: IconItem });
51 |
52 | // Populate the list store with data
53 | const items = [
54 | ['folder', 'Folder'],
55 | ['user-home', 'Home'],
56 | ['folder-downloads', 'Downloads'],
57 | ];
58 |
59 | for (let i = 0; i < 20; i++) {
60 | items.forEach(([iconName, label]) => {
61 | const iconItem = new IconItem();
62 | iconItem.iconName = iconName;
63 | iconItem.label = label;
64 | listStore.append(iconItem);
65 | });
66 | }
67 | // Create a selection model based on the Gio.ListStore
68 | const selectionModel = new Gtk.SingleSelection({ model: listStore });
69 |
70 | // Create a `Gtk.GridView` with a custom factory
71 | const gridView = new Gtk.GridView({
72 | model: selectionModel,
73 | });
74 |
75 | const factory = Gtk.SignalListItemFactory.new();
76 |
77 | factory.connect('setup', (factory, listItem) => {
78 | const box = new Gtk.Box({
79 | orientation: Gtk.Orientation.VERTICAL,
80 | spacing: 4,
81 | });
82 |
83 | const icon = new Gtk.Image();
84 | const label = new Gtk.Label({ xalign: 0.5, visible: false });
85 | icon.set_pixel_size(48);
86 |
87 | box.append(icon);
88 | box.append(label);
89 | listItem.set_child(box);
90 | });
91 |
92 | factory.connect('bind', (factory, listItem) => {
93 | const box = listItem.get_child();
94 | const icon = box.get_first_child();
95 | const label = icon.get_next_sibling();
96 | const item = listItem.get_item();
97 | icon.set_from_icon_name(item.iconName);
98 | label.set_label(item.label);
99 | });
100 |
101 | gridView.set_factory(factory);
102 |
103 | const scrolledWindow = new Gtk.ScrolledWindow();
104 | scrolledWindow.set_child(gridView);
105 | window.set_child(scrolledWindow);
106 | window.show();
107 | }
108 |
109 | createIconGridView();
110 |
111 | let loop = GLib.MainLoop.new(null, false);
112 | loop.run();
113 |
--------------------------------------------------------------------------------
/tests/search.js:
--------------------------------------------------------------------------------
1 | import Gdk from 'gi://Gdk?version=4.0';
2 | import Gtk from 'gi://Gtk?version=4.0';
3 | import GLib from 'gi://GLib';
4 | import Gio from 'gi://Gio';
5 | import GObject from 'gi://GObject';
6 |
7 | function callGetInitialResultSet(query) {
8 | try {
9 | // Connect to the D-Bus session
10 | const proxy = new Gio.DBusProxy({
11 | g_connection: Gio.DBus.session,
12 | g_name: 'org.gnome.Nautilus',
13 | g_object_path: '/org/gnome/Nautilus/SearchProvider',
14 | g_interface_name: 'org.gnome.Shell.SearchProvider2',
15 | });
16 |
17 | // console.log(proxy);
18 | // console.log(proxy.GetInitialResultSet);
19 |
20 | const queryArray = [query]; // This should be an array of strings, even if it contains one string
21 |
22 | // Call the method synchronously using call_sync()
23 | const resultSet = proxy.call_sync(
24 | 'GetInitialResultSet', // Method name
25 | GLib.Variant.new('(as)', [queryArray]), // Arguments (array of strings)
26 | Gio.DBusCallFlags.NONE, // No flags
27 | -1, // No timeout (wait indefinitely)
28 | null, // No cancellable
29 | );
30 |
31 | const resultArray = resultSet.deepUnpack(); // Unpack the variant result
32 | console.log(resultArray);
33 |
34 | // Handle and log the result set
35 | // log(`Received initial result set: ${JSON.stringify(resultSet)}`);
36 | } catch (e) {
37 | logError('Error calling GetInitialResultSet: ' + e.message);
38 | }
39 | }
40 |
41 | function getTermsForSearchString(searchString) {
42 | searchString = searchString.replace(/^\s+/g, '').replace(/\s+$/g, '');
43 | if (searchString === '') return [];
44 | return searchString.split(/\s+/);
45 | }
46 |
47 | callGetInitialResultSet('board');
48 |
49 | /*
50 | gdbus call --session --dest=org.gnome.Nautilus --object-path=/org/gnome/Nautilus/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetInitialResultSet "['board']"
51 | */
52 |
--------------------------------------------------------------------------------
/tests/search.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | gdbus introspect --session --dest org.gnome.Nautilus --object-path /org/gnome/Nautilus/SearchProvider
3 | gdbus call --session --dest=org.gnome.Nautilus --object-path=/org/gnome/Nautilus/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetInitialResultSet "['board']"
4 | gdbus call --session --dest=org.gnome.Nautilus --object-path=/org/gnome/Nautilus/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetResultMetas "['file:///home/iceman/Documents/board-resolution.md']"
5 |
6 | # gdbus introspect --session --dest org.gnome.Calculator.SearchProvider --object-path /org/gnome/Calculator/SearchProvider
7 | gdbus call --session --dest=org.gnome.Calculator.SearchProvider --object-path=/org/gnome/Calculator/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetInitialResultSet "['3 * 3']"
8 | gdbus call --session --dest=org.gnome.Calculator.SearchProvider --object-path=/org/gnome/Calculator/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetResultMetas "['3 * 3', 'copy-to-clipboard-3 * 3']"
9 | # gdbus call --session --dest=org.gnome.Calculator.SearchProvider --object-path=/org/gnome/Calculator/SearchProvider --method=org.gnome.Shell.SearchProvider2.GetSubsearchResultSet "['3 * 3', 'copy-to-clipboard-3 * 3']" "['3 * 3']"
--------------------------------------------------------------------------------
/tests/style.css:
--------------------------------------------------------------------------------
1 | textview {
2 | font-family: "Hack Nerd Font";
3 | /* font-family: "Fira Code";*/
4 | font-size: 12px;
5 | }
--------------------------------------------------------------------------------
/tests/system.js:
--------------------------------------------------------------------------------
1 | import GLib from 'gi://GLib';
2 |
3 | Object.keys(GLib).forEach((k) => {
4 | console.log(k);
5 | });
6 |
7 | const prettyName = GLib.get_os_info('PRETTY_NAME');
8 | const name = prettyName ? prettyName : GLib.get_os_info('NAME');
9 |
10 | console.log(name);
11 |
12 | // get_application_name
13 | // get_charset
14 | // get_codeset
15 | // get_console_charset
16 | // get_current_dir
17 | // get_current_time
18 | // get_environ
19 | // get_filename_charsets
20 | // get_home_dir
21 | // get_host_name
22 | // get_language_names
23 | // get_language_names_with_category
24 | // get_locale_variants
25 | // get_monotonic_time
26 | // get_num_processors
27 | // get_os_info
28 | // get_prgname
29 | // get_real_name
30 | // get_real_time
31 | // get_system_config_dirs
32 | // get_system_data_dirs
33 | // get_tmp_dir
34 | // get_user_cache_dir
35 | // get_user_config_dir
36 | // get_user_data_dir
37 | // get_user_name
38 | // get_user_runtime_dir
39 | // get_user_special_dir
40 | // get_user_state_dir
41 |
--------------------------------------------------------------------------------
/tests/test.sh:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | cd "$(dirname "$0")"
4 | export GI_TYPELIB_PATH=/usr/lib64/gnome-shell
5 | export LD_LIBRARY_PATH=/usr/lib64/gnome-shell
6 | export LD_PRELOAD=/usr/lib64/libgtk4-layer-shell.so
7 | gjs -m ./tests.js
8 |
--------------------------------------------------------------------------------
/tests/tests.js:
--------------------------------------------------------------------------------
1 | import Gdk from 'gi://Gdk?version=4.0';
2 | import Gtk from 'gi://Gtk?version=4.0';
3 | import Gio from 'gi://Gio';
4 | import GLib from 'gi://GLib';
5 |
6 | import Power from '../src/services/power.js';
7 | import Network from '../src/services/network.js';
8 | import Brightness from '../src/services/brightness.js';
9 | import Mounts from '../src/services/mounts.js';
10 | import { Volume, Mic } from '../src/services/volume.js';
11 | import Trash from '../src/services/trash.js';
12 | import WindowManagerService from '../src/windowManager.js';
13 | import Search from '../src/search.js';
14 | import '../src/lib/environment.js';
15 | import Console from '../src/extensions/console/extension.js';
16 |
17 | globalThis.Main = {};
18 |
19 | function test_shell() {
20 | let s = WindowManagerService();
21 | s.enable();
22 | s.connectObject('windows-update', () => {
23 | console.log('update...');
24 | // console.log('--------------------------');
25 | // console.log(s.currentWindows());
26 | });
27 | s.connectObject('window-focused', (w) => {
28 | console.log('focused...');
29 | console.log(s.findWindow(s.focused));
30 | });
31 | s.connectObject('window-opened', (w) => {
32 | console.log('opened...');
33 | // console.log(w);
34 | });
35 | s.connectObject('window-closed', (w) => {
36 | console.log('closed...');
37 | // console.log(w);
38 | });
39 |
40 | s.listen();
41 | s.getWindows()
42 | .then((res) => {
43 | // console.log(res);
44 | })
45 | .catch((err) => {
46 | console.log('oops');
47 | console.log(err);
48 | });
49 | // s.spawn('kitty');
50 |
51 | Main.shell = s;
52 | }
53 |
54 | function test_bar_items() {
55 | Main = {
56 | trash: new Trash(),
57 | power: new Power(),
58 | mounts: new Mounts(),
59 | brightness: new Brightness(),
60 | volume: new Volume(),
61 | mic: new Mic(),
62 | };
63 | Object.keys(Main).forEach((k) => {
64 | let service = Main[k];
65 | service.connect(`${k}-update`, (obj) => {
66 | console.log(obj.state);
67 | });
68 | service.enable();
69 | });
70 | }
71 |
72 | async function test_network() {
73 | let n = new Network();
74 | n.connect('network-update', (obj) => {
75 | console.log(obj.state);
76 | });
77 | n.enable();
78 | Main.network = n;
79 | }
80 |
81 | Gtk.init();
82 |
83 | test_shell();
84 | // test_bar_items();
85 | // test_network();
86 |
87 | try {
88 | let c = new Console();
89 | c.enable();
90 | } catch (err) {
91 | console.log(err);
92 | }
93 |
94 | // try {
95 | // let s = new Search();
96 | // s.enable();
97 | // Main.search = s;
98 | // } catch (err) {
99 | // console.log(err);
100 | // }
101 |
102 | let loop = GLib.MainLoop.new(null, false);
103 | loop.run();
104 |
--------------------------------------------------------------------------------
/tests/tests.md:
--------------------------------------------------------------------------------
1 | ```js
2 |
3 |
4 | let size = 48; // Icon size in pixels
5 | let scale = 1; // Scale factor (e.g., 1 for standard resolution, 2 for HiDPI)
6 | let direction = Gtk.TextDirection.LTR; // Left-to-right or right-to-left
7 | let state = Gtk.IconLookupFlags.FORCE_SVG; // Example: force SVG or other flags
8 | let flags
9 |
10 | // Look up the icon
11 | let iconInfo = iconTheme.lookup_icon('brightness-display-symbolic', null, size, scale, direction, flags)
12 |
13 | if (iconInfo) {
14 | // Get the file path of the icon
15 | let iconPath = iconInfo.get_file().get_path();
16 | console.log('-----???------');
17 | print(`Icon path: ${iconPath}`);
18 | } else {
19 | print("Icon not found in the current theme.");
20 | }
21 |
22 | ```
--------------------------------------------------------------------------------