├── .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 | [!["Buy Me A Coffee"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/icedman) 13 | 14 | ![Screen Shot](https://raw.githubusercontent.com/icedman/gjs-wayland-shell/main/screenshots/screenshot-2024-12-11-01.png) 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 | ![Screen Shot](https://raw.githubusercontent.com/icedman/gjs-wayland-shell/main/screenshots/screenshot-2024-12-26-01.png) -------------------------------------------------------------------------------- /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 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/applications-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 32 | 34 | 37 | 38 | 44 | 47 | 51 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/block-symbolic.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/bottom-panel-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 32 | 34 | 37 | 38 | 44 | 45 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/dash-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 14 | 32 | 34 | 37 | 38 | 44 | 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 | 14 | 16 | 35 | 39 | 42 | 45 | 48 | 53 | 58 | 63 | 64 | 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 | 14 | 35 | 37 | 40 | 41 | 47 | 50 | 55 | 56 | 57 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/pageview-symbolic.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/pulse-symbolic.svg: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/remove-window-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /prefs/ui/icons/hicolor/scalable/actions/reset-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 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 | 4 |
5 | 6 | Project page 7 | prefs.open-readme 8 | 9 | 10 | Buy me a coffee 11 | prefs.open-buy-coffee 12 | 13 | 14 | Report a Bug 15 | prefs.open-bug-report 16 | 17 | 18 | License 19 | prefs.open-license 20 | 21 | 34 |
35 |
36 | 37 | info_menu_model 38 | heart-filled-symbolic 39 | 40 | 41 | 42 | -------------------------------------------------------------------------------- /prefs/ui/panel-row.ui: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | true 7 | horizontal 8 | 11 | 12 | 13 | extension-symbolic 14 | 15 | 16 | 17 | 18 | start 19 | 20 | 21 | 22 | 23 | start 24 | 25 | 26 | 27 | 28 | 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 | 30 | 31 | 32 | false 33 | Search Everywhere 34 | slot.search-global 35 | edit-find-symbolic 36 | 37 | 38 | 39 | 40 | false 41 | Files 42 | 43 | 44 | 45 | 46 | false 47 | Main Menu 48 | open-menu-symbolic 49 | 50 | 51 | 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 | ![fuzzy search enabled](screenshot_after.png "GNOME search showing 'Calculator' for the query 'calxu'") 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 | Arch Linux -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/debian-symbolic.svg: -------------------------------------------------------------------------------- 1 | Debian -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/fedora-symbolic.svg: -------------------------------------------------------------------------------- 1 | Fedora -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/kalilinux-symbolic.svg: -------------------------------------------------------------------------------- 1 | Kali Linux -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/linuxmint-symbolic.svg: -------------------------------------------------------------------------------- 1 | Linux Mint -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/manjaro-symbolic.svg: -------------------------------------------------------------------------------- 1 | Manjaro -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/ubuntu-symbolic.svg: -------------------------------------------------------------------------------- 1 | Ubuntu -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/places/zorin-symbolic.svg: -------------------------------------------------------------------------------- 1 | Zorin -------------------------------------------------------------------------------- /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 | 2 | 3 | -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/status/triangle-left-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/status/triangle-right-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /src/ui/icons/hicolor/scalable/status/triangle-up-symbolic.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /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 | ``` --------------------------------------------------------------------------------