├── .gitignore
├── LICENSE
├── README.md
├── closeSession.js
├── constants.js
├── dbus-interfaces
├── org.freedesktop.login1.Manager.xml
├── org.gnome.SessionManager.EndSessionDialog.xml
├── org.gnome.Shell.Extensions.awsm.Autostart.xml
└── org.gnome.Shell.Extensions.awsm.PickWindow.xml
├── extension.js
├── icons
├── autorestore-symbolic.svg
├── choose-window-symbolic.svg
├── close-symbolic.svg
├── empty-symbolic.svg
├── move-symbolic.svg
├── readme.md
├── restore-symbolic.svg
├── save-symbolic.svg
├── separator-symbolic.svg
├── toggle-off-autorestore-symbolic.svg
└── toggle-on-autorestore-symbolic.svg
├── indicator.js
├── metadata.json
├── model
├── closeWindowsRule.js
└── sessionConfig.js
├── moveSession.js
├── openWindowsTracker.js
├── prefs.js
├── prefsCloseWindow.js
├── prefsColumnView.js
├── prefsWidgets.js
├── prefsWindowPickableEntry.js
├── restoreSession.js
├── saveSession.js
├── schemas
├── gschemas.compiled
└── org.gnome.shell.extensions.another-window-session-manager.gschema.xml
├── stylesheet.css
├── template
├── 60-awsm-ydotool-uinput.rules
├── _awsm-restore-previous-session.desktop
├── _gnome-shell-extension-another-window-session-manager.desktop
├── launch-app.sh
└── template.desktop
├── ui
├── autoclose.js
├── autostart.js
├── button.js
├── popupMenuButtonItems.js
├── prefs-gtk4.ui
├── searchSessionItem.js
├── sessionItem.js
├── sessionItemButtons.js
└── uiHelper.js
├── utils
├── CommonError.js
├── WindowPicker.js
├── dateUtils.js
├── fileUtils.js
├── function.js
├── iconFinder.js
├── log.js
├── metaWindowUtils.js
├── prefsUtils.js
├── signal.js
├── stringUtils.js
├── subprocessUtils.js
└── tooltip.js
└── windowTilingSupport.js
/.gitignore:
--------------------------------------------------------------------------------
1 | .vscode
2 | .history
3 | .idea
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # gnome-shell-extension-another-window-session-manager
2 | Close open windows gracefully and save them as a session. And you can restore them when necessary manually or automatically at startup.
3 |
4 | Most importantly, it supports both X11 and Wayland!
5 |
6 | This extension is based on several [Gnome technologies](https://www.gnome.org/technologies/) and APIs including [Meta](https://gjs-docs.gnome.org/meta9~9_api), [Shell](https://gjs-docs.gnome.org/shell01~0.1_api/) and [St(Shell Toolkit)](https://gjs-docs.gnome.org/st10~1.0_api/).
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | # Screenshot
16 |
17 | ## Overview
18 | 
19 |
20 | ## Close open windows
21 | Click item to close open windows:
22 |
23 | 
24 |
25 |
26 | After confirm to close:
27 |
28 | 
29 |
30 | ## Save open windows
31 | Click item to save open windows as a session:
32 |
33 | 
34 |
35 |
36 | After confirm to save:
37 |
38 | 
39 |
40 | ## Activate the current session to be restored at startup
41 | 
42 |
43 | ## Preferences
44 |
45 | ### Restore sessions
46 | 
47 |
48 | ### Close windows
49 | 
50 |
51 | # Main features
52 | 1. Restore the previous session at startup. **disabled by default**, to enable it please activate `Restore previous apps and windows at startup` under `Restore sessions`. (See also: [Restore previous apps and windows at startup](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager#restore-previous-apps-and-windows-at-startup)).
53 | 1. Save running apps and windows automatically when necessary, this will be used to restore the previous session at startup.
54 | 1. Close running apps and windows automatically before `Log Out`, `Restart`, `Power Off`. **disabled by default**, to enable it please activate `Auto close session` under `Close windows`. (See also: [Auto close session](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager#auto-close-session)).
55 | 1. Close running windows gracefully
56 | 1. Close apps with multiple windows gracefully via `ydotool` so you don't lose sessions of this app (See also: [How to make Close by rules work](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager#how-to-make-close-by-rules-work))
57 | 1. Save running apps and windows manually
58 | 1. Restore a selected session at startup (See also: [#9](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/9#issuecomment-1097012874)). **disabled by default**.
59 | 1. Restore a saved session manually
60 | 1. Restore window state, including `Always on Top`, `Always on Visible Workspace` and maximization
61 | 1. Restore window workspace, size and position
62 | 1. Restore 2 column window tiling
63 | 1. Stash all supported window states so that those states will be restored after gnome shell restarts via `Alt+F2 -> r` or `killall -3 gnome-shell`.
64 | 1. Move windows to their own workspace according to a saved session
65 | 1. Support multi-monitor
66 | 1. Remove saved session to trash
67 | 1. Search saved session by the session name fuzzily
68 | 1. ...
69 |
70 | ## Close windows
71 |
72 | ### Auto close session
73 | Enable this feature through `Auto close session` under `Close windows`:
74 |
75 | 
76 |
77 | After you click the `Log Out/Restart/Power Off` button:
78 |
79 | 
80 |
81 | If the second button on the above dialog has `via AWSM`, it means this feature is enabled.
82 |
83 | After you click `Log Out(via AWSM)`, all apps and windows will be closed automatically by AWSM. But some apps might be still opening, you have to close them yourself; then if there are no running apps, this extension logs out the current user immediately.
84 |
85 | 
86 |
87 | You can move it around in case it covers other windows.
88 |
89 | Please note that currently if this option is enabled, it modifies the Gnome Shell `endSessionDialog` **globally**, which means running `gnome-session-quit --logout` will also popup the new modified dialog.
90 |
91 | ### How to make `Close by rules` work
92 |
93 | To make this feature work, you need to install [ydotool](https://github.com/ReimuNotMoe/ydotool):
94 |
95 | ```bash
96 | # 1. Install `ydotool` using the package manager and make sure the version is greater than v1.0.0
97 | sudo dnf install ydotool
98 | #Or install it from the source code: https://github.com/ReimuNotMoe/ydotool
99 |
100 | #Check the permission of `/dev/uinput`, if it's `crw-rw----+`, you can skip step 2
101 | # 2. Get permission to access to `/dev/uinput` as the normal user
102 | sudo touch /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
103 | # Here we use `tee`, not redirect(>), to avoid `warning: An error occurred while redirecting file '/etc/udev/rules.d/60-awsm-ydotool-uinput.rules' open: Permission denied`
104 | # See: https://www.shellhacks.com/sudo-echo-to-file-permission-denied/
105 | echo '# See:
106 | # https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules
107 | # https://github.com/ReimuNotMoe/ydotool/issues/25
108 |
109 | # ydotool udev write access
110 | KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' | sudo tee --append /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
111 |
112 | cat /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
113 | #Remove executable permission (a.k.a. x)
114 | sudo chmod 644 /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
115 |
116 | # 3. Copy ydotool.service to /usr/lib/systemd/user, so `systemctl --user enable ydotool.service` can work
117 | sudo cp /usr/lib/systemd/system/ydotool.service /usr/lib/systemd/user
118 | # 4. Start ydotool.service at startup automatically for the current normal user
119 | systemctl --user enable ydotool.service
120 | # 5. Note that you may have to restart the system if the following commands are not working
121 | # 6. Start the ydotoold service for the current normal user
122 | systemctl --user start ydotool.service
123 | # 7. Check if ydotoold service is working. The word `hello` should print on the terminal, if not you might need to reboot the system or try to relogin your account.
124 | ydotool type 'hello'
125 |
126 | ## misc. ##
127 |
128 | # Check if the ydotoold service is running, if not you may have to restart the system or start ydotool.service
129 | systemctl --user status ydotool.service
130 |
131 | ```
132 |
133 | Note that it's no necessary to run `systemctl --user enable ydotool.service`, because this extension starts `ydotool.service` every time while you use it to close windows.
134 |
135 | Feel free to fill an issue if `ydotool` does not work under normal user, you may also want to do that in [its git issue area](https://github.com/ReimuNotMoe/ydotool/issues)
136 |
137 | ## Restore sessions
138 |
139 | ### Restore previous apps and windows at startup
140 | 
141 |
142 | Activate `Restore previous apps and windows at startup` to enable this feature. This option and `Restore selected session at startup` are exclusive. And this option works for shutting down the system normally (via Log Out/Restart/Power Off buttons) and other ways (like pressing the physical power-off button).
143 |
144 | Then while startup, AWSM will launch and restore apps and states from the previous saved session configs.
145 |
146 | The session configs are saved in the path `~/.config/another-window-session-manager/sessions/currentSession`.
147 |
148 | You can use the below command to test it.
149 | ```bash
150 | gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestorePreviousSession "{'removeAfterRestore': }"
151 | ```
152 |
153 | ### How to `Restore a session at startup`?
154 |
155 | To make it work, you must enable it through `Restore sessions -> Restore at startup` in the Preferences AND active a session by clicking
in the popup menu.
156 |
157 | While you enable it through `Restore sessions -> Restore at startup`, it creates a `_gnome-shell-extension-another-window-session-manager.desktop` under the folder `~/.config/autostart/`.
158 |
159 | Test the settings in command line via:
160 | ```Bash
161 | gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestoreSession
162 | ```
163 |
164 | Please do not modify `_gnome-shell-extension-another-window-session-manager.desktop`, all changes by yourself could be overidden or deleted.
165 |
166 | # Panel menu items
167 |
168 | ## Icons description
169 |
170 | | Icon | Description |
171 | |--------------------------------------------------------------|--------------------------------------------------------------|
172 | |
| Save open windows as a session, which name is the item's name |
173 | |
| Restore the saved session using the item's name |
174 | |
| Move the open windows using the item's name |
175 | |
| Close the current open windows |
176 | |
| Activate the current session to be restored at startup |
177 | |
| Inactivate the current session to be restored at startup |
178 | |
| Indicate the autorestore button |
179 |
180 |
181 | # Dependencies
182 | * procps-ng
183 |
184 | Use `ps` and `pwdx` to get some information from a process, install it via `dnf install procps-ng` if you don't have.
185 |
186 | * glib2
187 |
188 | Use `gdbus` to call the remote method, which is provided by this exension, to implement the `restore at start` feature. `gdbus` is part of `glib2`.
189 |
190 | * ydotool
191 |
192 | Send keys to close the application gracefully with multiple windows.
193 |
194 | * libgtop2
195 |
196 | As of version 34, AWSM also uses `libgtop2` to query process information, just like `ps`. The cost of calling `ps` is very high, so I'm planing to remove this entirely.
197 |
198 | To install it:
199 |
200 | * Fedora and derivatives:
201 | `dnf install libgtop2`
202 |
203 | * Debian, Ubuntu, Pop!_OS, and derivatives:
204 | `apt install gir1.2-gtop-2.0 libgtop2-dev`
205 |
206 | * Arch and derivatives:
207 | `pacman -S libgtop`
208 |
209 | # Known issues
210 |
211 | 1. On both X11 and Wayland, if click restore button (
) continually during the process of restoring, the window size and position may can't be restored, and it may restore many instances of an application. **As a workaround, click the restore button (
) only once until all apps are restored.**
212 |
213 | # Support applications launched via a command line or applications that don't have a proper .desktop file
214 | If the .desktop is missing from a session file, restoring an application relies on the command line completely.
215 |
216 | In this case this extension will generate a .desktop in the `journalctl` when you click the save button (
). Search `Generated a .desktop file` in `journalctl /usr/bin/gnome-shell -r` to find it: `journalctl /usr/bin/gnome-shell -b -o cat --no-pager | grep 'Generated a .desktop file'`. To make it work, You need to copy it to `~/.local/share/applications`, and relaunch the app and save the session again. This extension should be able to restore the workspace, state, size and position of this application.
217 |
218 | **The generated .desktop might not work sometimes, it's better to check whether the value of `Exec` is correct or not.** If you restore an app using a bad .desktop, this extension will give you a notification and log error level logs in the `journalctl`.
219 |
220 | I tested on Anki, VirtualBox machine and two .AppImage apps, they all have no .desktop and are launched in the terminal. By using the generated .desktop, Anki, VirtualBox machine works. One .AppImage app works. Another .AppImage app is `Wire_x86_64.AppImage` and doesn't work, because the command line returned is something like `/tmp/.mount_Wire-3xxxxx/wire-desktop`, you can use it to launch Wire but files in the `/tmp` will be deleted during the OS shutdown and start.
221 |
222 | It's impossible / hard to query the command line from a process, the pid of a window might not be right too and I don't find a standard way for this.
223 |
224 | ## How can I know whether a .desktop of an application is proper or not?
225 |
226 | One of the following should be enough to prove the .desktop is not proper:
227 | 1. Right click on the icon in the panel or dash, if there is no `Add to Favorites` in the menu
228 | 2. This extension can launch an application, but can't move the window to its workspace. (But it might suggest there is a bug in this extension, LOL :))
229 |
230 | Most existing applications should have a proper .desktop. I'm just handling the special case. Someone like myself might want this feature.
231 |
232 | # Where are the saved sessions?
233 | They are all in `~/.config/another-window-session-manager/sessions`. When use an existing name to save the current open windows, the previous file will be copied to `~/.config/another-window-session-manager/sessions/backups` as a new name, which is the-old-session-name**.backup-current-timestamp**.
234 |
235 | Note that I've marked `backups` as a reserved word, so you can't use it as a session name when saving a session. But you do have the freedom to manually create a file named `backups` in `~/.config/another-window-session-manager/sessions`. But this extension will only backup the session file that you are clicking the save button and you will receive an error log in the `journalctl` and an error notification every time you save an existing session.
236 |
237 | # TODO
238 | 1. - Close open windows
239 | - [ ] Close all windows on the current workspace. (WIP, see https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/pull/71)
240 | 1. - Save open windows
241 | - [x] Save open windows
242 | 1. - Restore saved open windows
243 | - [x] Restore saved open windows
244 | - [x] Move to belonging workspace automatically
245 | - [x] Restore window size and position ([issue 17](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/17))
246 | - [x] Restore window workspace, size and position of applications launched via a command line and don't have a recognizable `.desktop` file by `Shell.AppSystem.get_default().get_running()`.
247 | - [x] Support multi-monitor ([issue 21](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/21))
248 | 1. - Saved open windows list
249 | - [x] Save open windows button
250 | - [x] Restore button
251 | - [ ] Rename button (double click text to rename?)
252 | - [x] Move button
253 | - [x] Delete button
254 | 1. - [x] Move windows according to a saved session.
255 | 1. - [ ] Settings
256 | - [x] Debugging mode
257 | - [ ] whitelist using for closing application with multiple windows
258 | 1. - [x] Support restoring a saved session at startup ([issue 9](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/9))
259 | 1. - [x] Support saving and closing windows when Log Out, Power off, Reboot ([issue 9](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/9))
260 | 1. - [ ] All TODO tags in the projects
261 | 1. - [ ] Translation?
262 | 1. - [ ] A client tool called `awsm-client` (See: [issue 34](https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues/34))
263 | 1. - [ ] Fix any typo or grammar errors.
264 | 1. - [ ] Open the Preferences on the popup menu
265 | 1. - [x] Open the session file from the popup menu
266 |
267 |
--------------------------------------------------------------------------------
/constants.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /*
4 | * GdkModifierType
5 | *
6 | * See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L74
7 | */
8 | export const GDK_SHIFT_MASK = 1 << 0;
9 | export const GDK_CONTROL_MASK = 1 << 2;
10 | export const GDK_ALT_MASK = 1 << 3;
11 |
12 | export const GDK_META_MASK = 1 << 28;
13 |
14 | /* prefs settings */
15 | export const PREFS_SETTING_AUTORESTORE_SESSIONS = 'autorestore-sessions';
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/dbus-interfaces/org.freedesktop.login1.Manager.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/dbus-interfaces/org.gnome.SessionManager.EndSessionDialog.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/dbus-interfaces/org.gnome.Shell.Extensions.awsm.Autostart.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/dbus-interfaces/org.gnome.Shell.Extensions.awsm.PickWindow.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
--------------------------------------------------------------------------------
/extension.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
4 |
5 | import * as OpenWindowsTracker from './openWindowsTracker.js';
6 |
7 | import * as Indicator from './indicator.js';
8 | import * as Autostart from './ui/autostart.js';
9 | import * as Autoclose from './ui/autoclose.js';
10 | import {WindowTilingSupport} from './windowTilingSupport.js';
11 | import * as WindowPicker from './utils/WindowPicker.js';
12 |
13 | import {Extension, gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
14 |
15 | import * as Log from './utils/log.js';
16 | import * as FileUtils from './utils/fileUtils.js';
17 | import {prefsUtilsInit, prefsUtilsDestroy} from './utils/prefsUtils.js';
18 |
19 |
20 | let _indicator;
21 | let _autostartServiceProvider;
22 | let _openWindowsTracker;
23 | let _autoclose;
24 | let _windowPickerServiceProvider;
25 |
26 | export default class AnotherWindowSessionManagerExtension extends Extension {
27 |
28 | constructor(metadata) {
29 | super(metadata);
30 | }
31 |
32 | enable() {
33 | // settings is needed by the initialization of some utils
34 | this._settings = this.getSettings('org.gnome.shell.extensions.another-window-session-manager');
35 |
36 | this.initUtils();
37 |
38 | this._settings.connect('changed::show-indicator', () => this.showOrHideIndicator());
39 | this.showOrHideIndicator();
40 |
41 | _autostartServiceProvider = new Autostart.AutostartServiceProvider();
42 |
43 | WindowTilingSupport.initialize();
44 |
45 | _openWindowsTracker = new OpenWindowsTracker.OpenWindowsTracker();
46 | _autoclose = new Autoclose.Autoclose();
47 |
48 | _windowPickerServiceProvider = new WindowPicker.WindowPickerServiceProvider();
49 | _windowPickerServiceProvider.enable();
50 | }
51 |
52 | initUtils() {
53 | prefsUtilsInit(this, this._settings);
54 | FileUtils.init(this);
55 | }
56 |
57 | showOrHideIndicator() {
58 | if (this._settings.get_boolean('show-indicator')) {
59 | if (!_indicator) {
60 | _indicator = new Indicator.AwsIndicator();
61 | Main.panel.addToStatusArea('Another Window Session Manager', _indicator);
62 | }
63 | } else {
64 | this.hideIndicator();
65 | }
66 | }
67 |
68 | hideIndicator() {
69 | if (_indicator) {
70 | _indicator.destroy();
71 | _indicator = null;
72 | }
73 | }
74 |
75 | disable() {
76 |
77 | this.hideIndicator();
78 |
79 | if (_autostartServiceProvider) {
80 | _autostartServiceProvider.disable();
81 | _autostartServiceProvider = null;
82 | }
83 |
84 | if (_openWindowsTracker) {
85 | _openWindowsTracker.destroy();
86 | _openWindowsTracker = null;
87 | }
88 |
89 | WindowTilingSupport.destroy();
90 |
91 | if (_autoclose) {
92 | _autoclose.destroy();
93 | _autoclose = null;
94 | }
95 |
96 | Log.Log.destroyDefault();
97 |
98 | if (_windowPickerServiceProvider) {
99 | _windowPickerServiceProvider.destroy();
100 | _windowPickerServiceProvider = null;
101 | }
102 |
103 | if (this._settings) {
104 | this._settings = null;
105 | }
106 |
107 | prefsUtilsDestroy();
108 |
109 | }
110 |
111 | }
112 |
--------------------------------------------------------------------------------
/icons/autorestore-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
83 |
--------------------------------------------------------------------------------
/icons/choose-window-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/close-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/empty-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
47 |
--------------------------------------------------------------------------------
/icons/move-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/readme.md:
--------------------------------------------------------------------------------
1 | [restore-symbolic.svg](How can I get the absolute path from a St.Icon object? I don't find a method to do that in the doc https://gjs-docs.gnome.org/st10~1.0_api/st.icon#method-get_icon_name, any suggestion?)
2 |
3 | [move-symbolic.svg](https://iconduck.com/icons/21634/alternate-arrows)
4 |
5 | [close-symbolic.svg](https://iconduck.com/icons/47700/close)
6 |
7 | [save-symbolic.svg](https://iconduck.com/icons/21869/download)
8 |
9 | [separator-symbolic.svg](https://iconduck.com/icons/59149/separator)
10 |
11 | [toggle-off-symbolic.svg modified based on toggle-off.svg](https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/data/theme/toggle-off.svg)
12 |
13 | [toggle-on-symbolic.svg modified based on toggle-on.svg](https://gitlab.gnome.org/GNOME/gnome-shell/-/blob/main/data/theme/toggle-on.svg)
14 |
15 | [choose-window-symbolic.svg](https://iconduck.com/icons/230732/choose-your-profession)
16 |
--------------------------------------------------------------------------------
/icons/restore-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/save-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/separator-symbolic.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/icons/toggle-off-autorestore-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
79 |
--------------------------------------------------------------------------------
/icons/toggle-on-autorestore-symbolic.svg:
--------------------------------------------------------------------------------
1 |
2 |
78 |
--------------------------------------------------------------------------------
/metadata.json:
--------------------------------------------------------------------------------
1 | {
2 | "_generated": "Generated by SweetTooth, do not edit",
3 | "description": "Close open windows gracefully and save them as a session. And you can restore them when necessary manually or automatically at startup. Most importantly, it supports both X11 and Wayland!\n\nMain features:\n- Restore the previous session at startup. disabled by default.\n- Save running apps and windows automatically when necessary, this will be used to restore the previous session at startup.\n- Close running apps and windows automatically before Log Out, Restart, Power Off. disabled by default.\n- Close running windows gracefully\n- Close apps with multiple windows gracefully via ydotool so you don't lose sessions of this app (See also: How to make Close by rules work)\n- Save running apps and windows manually\n- Restore a selected session at startup (See also: #9). disabled by default.\n- Restore a saved session manually\n- Restore window state, including Always on Top, Always on Visible Workspace and maximization\n- Restore window workspace, size and position\n- Restore 2 column window tiling\n- Stash all supported window states so that those states will be restored after gnome shell restarts via Alt+F2 -> r or killall -3 gnome-shell.\n- Move windows to their own workspace according to a saved session\n- Support multi-monitor\n- Remove saved session to trash\n- Search saved session by the session name fuzzily\n\nFor more information, please visit https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/blob/feature-close-save-session-while-logout/README.md.\n\nPlease report issues on Github.",
4 | "name": "Another Window Session Manager",
5 | "shell-version": [
6 | "45",
7 | "46"
8 | ],
9 | "url": "https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager",
10 | "uuid": "another-window-session-manager@gmail.com",
11 | "version": 49
12 | }
13 |
--------------------------------------------------------------------------------
/model/closeWindowsRule.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 |
5 | import * as CloseWindowsRule from './closeWindowsRule.js';
6 |
7 |
8 | export const CloseWindowsWhitelist = GObject.registerClass({
9 | }, class CloseWindowsWhitelist extends GObject.Object {
10 | id; // int. just like the id in MySQL. Used to update or delete rows.
11 | name; // string. Can be any string
12 | compareWith; // string. title, wm_class, wm_class_instance, app_name...
13 | method; // string. equals
14 | enabled; // boolean
15 | enableWhenCloseWindows; // boolean
16 | enableWhenLogout; // boolean
17 |
18 | static new(param) {
19 | return Object.assign(new CloseWindowsRule.CloseWindowsWhitelist(), param);
20 | }
21 | });
22 |
23 | export const CloseWindowsRuleBase = class {
24 | category; // string. Applications, Keywords
25 | type; // string, rule type, such as 'shortcut'
26 | value; // GdkShortcuts, order and the rule pairs, such as "{1: 'Ctrl+Q}'".
27 | // wm_class; // string
28 | // wm_class_instance; // string
29 | enabled; // boolean
30 | keyDelay; // int, for example: `enabydotool key --key-delay 500 29:1 16:1 16:0 29:0`
31 | }
32 |
33 | export const CloseWindowsRuleByKeyword = class extends CloseWindowsRuleBase {
34 | id; // int. just like the id in MySQL. Used to update or delete rows.
35 | keyword; // string. Can be any string
36 | compareWith; // string. title, wm_class, wm_class_instance, app_name...
37 | // enableRegex; // int. 0, 1
38 | method; // string. endsWith, includes, startsWith, equals, regex.
39 |
40 | static new(param) {
41 | return Object.assign(new CloseWindowsRule.CloseWindowsRuleByKeyword(), param);
42 | }
43 | }
44 |
45 | export const CloseWindowsRuleByApp = class extends CloseWindowsRuleBase {
46 | appId; // string, such as 'firefox.desktop'
47 | appDesktopFilePath; // string, such as '/usr/share/applications/firefox.desktop'
48 | appName; // string, such as 'Firefox'
49 |
50 | static new(param) {
51 | return Object.assign(new CloseWindowsRule.CloseWindowsRuleByApp(), param);
52 | }
53 | }
54 |
55 | /**
56 | * See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L73
57 | * See: https://gitlab.gnome.org/GNOME/gtk/blob/1ce79b29e363e585872901424d3b72041b55e3e4/gtk/gtkeventcontrollerkey.c:L203
58 | */
59 | export const GdkShortcuts = GObject.registerClass({
60 | }, class GdkShortcuts extends GObject.Object{
61 | /**
62 | * For example: Ctrl+Q
63 | */
64 | shortcut;
65 | order;
66 | /**
67 | * the pressed key.
68 | */
69 | keyval;
70 | /**
71 | * the raw code of the pressed key.
72 | */
73 | keycode;
74 | /**
75 | * the bitmask, representing the state of modifier keys and pointer buttons. See `GdkModifierType` in Gtk source.
76 | */
77 | state;
78 | /**
79 | * Indicate the right Ctrl key was pressed
80 | */
81 | controlRightPressed;
82 | /**
83 | * Indicate the right Shift key was pressed
84 | */
85 | shiftRightPressed;
86 |
87 | static new(param) {
88 | return Object.assign(new CloseWindowsRule.GdkShortcuts(), param);
89 | }
90 | });
--------------------------------------------------------------------------------
/model/sessionConfig.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | class WindowState {
4 | // If always on visible workspace
5 | is_sticky; // bool
6 | // If always on top
7 | is_above; // bool
8 |
9 | // Additional fields
10 |
11 | // https://gjs-docs.gnome.org/meta9~9_api/meta.window#method-get_maximized
12 | // 0: Not in the maximization mode
13 | // 1: Horizontal - Meta.MaximizeFlags.HORIZONTAL
14 | // 2: Vertical - Meta.MaximizeFlags.VERTICAL
15 | // 3. Both - Meta.MaximizeFlags.BOTH
16 | meta_maximized;
17 | }
18 |
19 | class WindowPosition {
20 | provider; // str
21 | x_offset; // int
22 | y_offset; // int
23 | width; // int
24 | height; // int
25 | }
26 |
27 | class WindowTilingFor {
28 | app_name; // str
29 | // the .desktop file name
30 | desktop_file_id; // str
31 | // The full .desktop file path
32 | desktop_file_id_full_path; // str
33 | window_title; // str
34 | }
35 |
36 | class WindowTiling {
37 | window_tile_for = new WindowTilingFor(); // WindowTilingFor
38 | }
39 |
40 | export const SessionConfigObject = class {
41 |
42 | window_id; // str, hexadecimal on X11, int on Wayland
43 | desktop_number; // int
44 | pid; // int
45 | username; // str
46 | window_position = new WindowPosition(); // WindowPosition
47 | client_machine_name; // str
48 | window_title; // str
49 |
50 | app_name; // str
51 | wm_class; // str
52 | wm_class_instance; // str
53 |
54 | cmd; // list
55 | process_create_time; // str
56 |
57 | window_state = new WindowState(); // WindowState
58 |
59 | windows_count; // int
60 |
61 | cpu_percent; // float
62 | memory_percent; // float
63 |
64 | // Additional fields
65 |
66 | // the .desktop file name
67 | desktop_file_id; // str
68 | // The full .desktop file path
69 | desktop_file_id_full_path; // str
70 | // The index of the monitor that this window is on.
71 | monitor_number;
72 | // TODO Primary monitor can be changed, what if the primary monitor have been changed when restoring apps? The monitor number is the same as saved monitor_number?
73 | is_on_primary_monitor;
74 |
75 | fullscreen; // boolean
76 | minimized; // boolean
77 |
78 | window_tiling; // WindowTiling
79 |
80 | is_focused; // boolean, whether is the currently active window
81 |
82 | compositor_type; // string. X11, Wayland
83 | }
84 |
85 | export const SessionConfig = class {
86 | session_name; // str
87 | session_create_time; // str
88 | backup_time; // str
89 | restore_times; // list = []
90 | active_workspace_index; // int
91 | n_workspace; // int. the total number of workspaces
92 | // TODO
93 | // https://gjs-docs.gnome.org/meta9~9_api/meta.workspace#method-activate_with_focus
94 | // https://gjs-docs.gnome.org/meta9~9_api/meta.window#method-activate
95 | focused_window; // SessionConfigObject or SessionConfigObject.window_id?
96 | x_session_config_objects = []; // list[SessionConfigObject]
97 |
98 |
99 | /**
100 | * Sort session_config_objects by desktop number
101 | *
102 | */
103 | sort() {
104 | let x_session_config_objects_copy = this.x_session_config_objects.slice();
105 | x_session_config_objects_copy.sort((o1, o2) => {
106 | const desktop_number1 = o1.desktop_number;
107 | const desktop_number2 = o2.desktop_number;
108 |
109 | const diff = desktop_number1 - desktop_number2;
110 | if (diff === 0) {
111 | return 0;
112 | }
113 |
114 | if (diff > 0) {
115 | return 1;
116 | }
117 |
118 | if (diff < 0) {
119 | return -1;
120 | }
121 |
122 | });
123 | return x_session_config_objects_copy;
124 | }
125 | }
--------------------------------------------------------------------------------
/prefs.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import Gio from 'gi://Gio';
5 | import GLib from 'gi://GLib';
6 | import Gtk from 'gi://Gtk';
7 | import GdkWayland from 'gi://GdkWayland';
8 | import Gdk from 'gi://Gdk';
9 |
10 | import {ExtensionPreferences, gettext as _} from 'resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js';
11 |
12 | import * as FileUtils from './utils/fileUtils.js';
13 | import * as Log from './utils/log.js';
14 | import {prefsUtilsInit, prefsUtilsDestroy, PrefsUtils} from './utils/prefsUtils.js';
15 | import * as StringUtils from './utils/stringUtils.js';
16 |
17 | import * as PrefsCloseWindow from './prefsCloseWindow.js';
18 |
19 |
20 | export default class AnotherWindowSessionManagerPreferences extends ExtensionPreferences {
21 | fillPreferencesWindow(window) {
22 | window.set_default_size(1000, 800);
23 |
24 | const settings = this.getSettings('org.gnome.shell.extensions.another-window-session-manager');
25 |
26 | this.initUtils(settings);
27 |
28 | this._log = new Log.Log();
29 |
30 | this.render_ui();
31 | new PrefsCloseWindow.UICloseWindows(this._builder).init();
32 | this._bindSettings();
33 |
34 | // Set sensitive AFTER this._bindSettings() to make it work
35 | this._setSensitive();
36 |
37 | this._addPages(window);
38 | window.connect('close-request', () => {
39 | this._destroy();
40 | });
41 | }
42 |
43 | initUtils(settings) {
44 | prefsUtilsInit(this, settings);
45 | FileUtils.init(this);
46 | }
47 |
48 | _addPages(window) {
49 | const pages = [
50 | this._builder.get_object('close_windows_page'),
51 | this._builder.get_object('save_windows_page'),
52 | this._builder.get_object('restore_sessions_page'),
53 | this._builder.get_object('general_page'),
54 | ];
55 | pages.forEach(page => window.add(page));
56 | }
57 |
58 | _setSensitive() {
59 | const activeOfRestorePrevious = this.restore_previous_switch.get_active();
60 | this.restore_previous_delay_spinbutton.set_sensitive(activeOfRestorePrevious);
61 |
62 | const restore_at_startup_switch_state = this.restore_at_startup_switch.get_active();
63 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(restore_at_startup_switch_state);
64 | this.restore_at_startup_without_asking_switch.set_sensitive(restore_at_startup_switch_state);
65 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(
66 | restore_at_startup_switch_state && !this.restore_at_startup_without_asking_switch.get_active()
67 | );
68 |
69 | const display = Gdk.Display.get_default();
70 | if (display instanceof GdkWayland.WaylandDisplay) {
71 | this.stash_and_restore_states_switch.set_sensitive(false);
72 | }
73 | }
74 |
75 | _bindSettings() {
76 | PrefsUtils.getSettings().bind(
77 | 'debugging-mode',
78 | this.debugging_mode_switch,
79 | 'active',
80 | Gio.SettingsBindFlags.DEFAULT
81 | );
82 |
83 | PrefsUtils.getSettings().bind(
84 | 'verbose-logging',
85 | this.verbose_logging_switch,
86 | 'active',
87 | Gio.SettingsBindFlags.DEFAULT
88 | );
89 |
90 | PrefsUtils.getSettings().bind(
91 | 'show-indicator',
92 | this.show_indicator_switch,
93 | 'active',
94 | Gio.SettingsBindFlags.DEFAULT
95 | );
96 |
97 | PrefsUtils.getSettings().bind(
98 | 'enable-save-session-notification',
99 | this.save_session_notification_switch,
100 | 'active',
101 | Gio.SettingsBindFlags.DEFAULT
102 | );
103 |
104 | PrefsUtils.getSettings().bind(
105 | 'enable-autorestore-sessions',
106 | this.restore_at_startup_switch,
107 | 'active',
108 | Gio.SettingsBindFlags.DEFAULT
109 | );
110 |
111 | PrefsUtils.getSettings().bind(
112 | 'enable-restore-previous-session',
113 | this.restore_previous_switch,
114 | 'active',
115 | Gio.SettingsBindFlags.DEFAULT
116 | );
117 |
118 | PrefsUtils.getSettings().bind(
119 | 'restore-at-startup-without-asking',
120 | this.restore_at_startup_without_asking_switch,
121 | 'active',
122 | Gio.SettingsBindFlags.DEFAULT
123 | );
124 |
125 | PrefsUtils.getSettings().bind(
126 | 'autorestore-sessions-timer',
127 | this.timer_on_the_autostart_dialog_spinbutton,
128 | 'value',
129 | Gio.SettingsBindFlags.DEFAULT
130 | );
131 |
132 | PrefsUtils.getSettings().bind(
133 | 'restore-previous-delay',
134 | this.restore_previous_delay_spinbutton,
135 | 'value',
136 | Gio.SettingsBindFlags.DEFAULT
137 | );
138 |
139 | PrefsUtils.getSettings().bind(
140 | 'restore-session-interval',
141 | this.restore_session_interval_spinbutton,
142 | 'value',
143 | Gio.SettingsBindFlags.DEFAULT
144 | );
145 |
146 | PrefsUtils.getSettings().bind(
147 | 'autostart-delay',
148 | this.autostart_delay_spinbutton,
149 | 'value',
150 | Gio.SettingsBindFlags.DEFAULT
151 | );
152 |
153 | PrefsUtils.getSettings().bind(
154 | 'restore-window-tiling',
155 | this.restore_window_tiling_switch,
156 | 'active',
157 | Gio.SettingsBindFlags.DEFAULT
158 | );
159 |
160 | PrefsUtils.getSettings().bind(
161 | 'raise-windows-together',
162 | this.raise_windows_together_switch,
163 | 'active',
164 | Gio.SettingsBindFlags.DEFAULT
165 | );
166 |
167 | PrefsUtils.getSettings().bind(
168 | 'stash-and-restore-states',
169 | this.stash_and_restore_states_switch,
170 | 'active',
171 | Gio.SettingsBindFlags.DEFAULT
172 | );
173 |
174 | PrefsUtils.getSettings().bind(
175 | 'enable-autoclose-session',
176 | this.auto_close_session_switch,
177 | 'active',
178 | Gio.SettingsBindFlags.DEFAULT
179 | );
180 |
181 | PrefsUtils.getSettings().bind(
182 | 'enable-close-by-rules',
183 | this.close_by_rules_switch,
184 | 'active',
185 | Gio.SettingsBindFlags.DEFAULT
186 | );
187 |
188 | PrefsUtils.getSettings().connect('changed::enable-autorestore-sessions', (settings) => {
189 | if (PrefsUtils.getSettings().get_boolean('enable-autorestore-sessions')) {
190 | this._installAutostartDesktopFile(FileUtils.desktop_template_path_restore_at_autostart,
191 | FileUtils.autostart_restore_desktop_file_path);
192 | }
193 | });
194 |
195 | PrefsUtils.getSettings().connect('changed::enable-restore-previous-session', (settings) => {
196 | if (PrefsUtils.getSettings().get_boolean('enable-restore-previous-session')) {
197 | this._installAutostartDesktopFile(FileUtils.desktop_template_path_restore_previous_at_autostart,
198 | FileUtils.autostart_restore_previous_desktop_file_path);
199 | }
200 | });
201 |
202 | PrefsUtils.getSettings().connect('changed::restore-at-startup-without-asking', (settings) => {
203 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(
204 | !PrefsUtils.getSettings().get_boolean('restore-at-startup-without-asking')
205 | );
206 | });
207 |
208 | PrefsUtils.getSettings().connect('changed::autostart-delay', (settings) => {
209 | this._installAutostartDesktopFile(FileUtils.desktop_template_path_restore_at_autostart,
210 | FileUtils.autostart_restore_desktop_file_path);
211 | this._installAutostartDesktopFile(FileUtils.desktop_template_path_restore_previous_at_autostart,
212 | FileUtils.autostart_restore_previous_desktop_file_path);
213 | });
214 |
215 | }
216 |
217 | render_ui() {
218 | this._builder = new Gtk.Builder();
219 | this._builder.set_scope(new BuilderScope(this));
220 | this._builder.add_from_file(this.path + '/ui/prefs-gtk4.ui');
221 |
222 | this.debugging_mode_switch = this._builder.get_object('debugging_mode_switch');
223 | this.verbose_logging_switch = this._builder.get_object('verbose_logging_switch');
224 | this.show_indicator_switch = this._builder.get_object('show_indicator_switch');
225 |
226 | this.save_session_notification_switch = this._builder.get_object('save_session_notification_switch');
227 |
228 | this.restore_session_interval_spinbutton = this._builder.get_object('restore_session_interval_spinbutton');
229 | this.timer_on_the_autostart_dialog_spinbutton = this._builder.get_object('timer_on_the_autostart_dialog_spinbutton');
230 | this.autostart_delay_spinbutton = this._builder.get_object('autostart_delay_spinbutton');
231 | this.restore_window_tiling_switch = this._builder.get_object('restore_window_tiling_switch');
232 | this.restore_window_tiling_switch.connect('notify::active', (widget) => {
233 | const active = widget.active;
234 | this.raise_windows_together_switch.set_sensitive(active);
235 | });
236 | this.raise_windows_together_switch = this._builder.get_object('raise_windows_together_switch');
237 | this.stash_and_restore_states_switch = this._builder.get_object('stash_and_restore_states_switch');
238 |
239 | this.restore_previous_delay_spinbutton = this._builder.get_object('restore_previous_delay_spinbutton');
240 | this.restore_previous_switch = this._builder.get_object('restore_previous_switch');
241 | this.restore_previous_switch.connect('notify::active', (widget) => {
242 | const active = widget.active;
243 | const activeOfRestoreAtStartup = this.restore_at_startup_switch.get_active();
244 | if (activeOfRestoreAtStartup) {
245 | this.restore_at_startup_switch.set_active(!active);
246 | }
247 | this.restore_previous_delay_spinbutton.set_sensitive(active);
248 | });
249 |
250 | this.restore_at_startup_switch = this._builder.get_object('restore_at_startup_switch');
251 | this.restore_at_startup_switch.connect('notify::active', (widget) => {
252 | const active = widget.active;
253 | this.restore_at_startup_without_asking_switch.set_sensitive(active);
254 | const enableTimerSpinButton = active && !PrefsUtils.getSettings().get_boolean('restore-at-startup-without-asking');
255 | if (enableTimerSpinButton) {
256 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(true);
257 | } else {
258 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(false);
259 | }
260 |
261 | const activeOfRestorePrevious = this.restore_previous_switch.get_active();
262 | if (activeOfRestorePrevious) {
263 | this.restore_previous_switch.set_active(!active);
264 | }
265 | });
266 |
267 | this.restore_at_startup_without_asking_switch = this._builder.get_object('restore_at_startup_without_asking_switch');
268 | this.restore_at_startup_without_asking_switch.connect('notify::active', (widget) => {
269 | const active = widget.active;
270 | this.timer_on_the_autostart_dialog_spinbutton.set_sensitive(!active);
271 | });
272 |
273 | this.close_by_rules_switch = this._builder.get_object('close_by_rules_switch');
274 | this.auto_close_session_switch = this._builder.get_object('auto_close_session_switch');
275 |
276 | }
277 |
278 | _installAutostartDesktopFile(desktopFileTemplate, targetDesktopFilePath) {
279 | const argument = {
280 | autostartDelay: PrefsUtils.getSettings().get_int('autostart-delay'),
281 | };
282 | const desktopFileContent = StringUtils.format(FileUtils.loadTemplate(desktopFileTemplate), argument);
283 | this._installDesktopFileToAutostartDir(targetDesktopFilePath, desktopFileContent);
284 | }
285 |
286 | _installDesktopFileToAutostartDir(desktopFilePath, desktopFileContents) {
287 | const autostart_restore_desktop_file = Gio.File.new_for_path(desktopFilePath);
288 | const autostart_restore_desktop_file_path_parent = autostart_restore_desktop_file.get_parent().get_path();
289 | if (GLib.mkdir_with_parents(autostart_restore_desktop_file_path_parent, 0o744) === 0) {
290 | let [success, tag] = autostart_restore_desktop_file.replace_contents(
291 | desktopFileContents,
292 | null,
293 | false,
294 | Gio.FileCreateFlags.REPLACE_DESTINATION,
295 | null
296 | );
297 |
298 | if (success) {
299 | this._log.info(`Installed the autostart desktop file: ${desktopFilePath}!`);
300 | } else {
301 | this._log.error(new Error(`Failed to install the autostart desktop file: ${desktopFilePath}`))
302 | }
303 | } else {
304 | this._log.error(new Error(`Failed to create folder: ${autostart_restore_desktop_file_path_parent}`));
305 | }
306 | }
307 |
308 | _destroy() {
309 | prefsUtilsDestroy();
310 |
311 | }
312 | }
313 |
314 |
315 | const BuilderScope = GObject.registerClass({
316 | // Should be a globally unique GType name
317 | GTypeName: "AnotherWindowSessionManagerBuilderScope",
318 | Implements: [Gtk.BuilderScope],
319 | }, class BuilderScope extends GObject.Object {
320 | _init(preferences) {
321 | this._preferences = preferences;
322 | super._init();
323 | }
324 |
325 | // Fix: Gtk.BuilderError: Creating closures is not supported by Gjs_BuilderScope
326 | // https://docs.w3cub.com/gtk~4.0/gtkbuilder#gtk-builder-create-closure
327 | vfunc_create_closure(builder, handlerName, flags, connectObject) {
328 | if (flags & Gtk.BuilderClosureFlags.SWAPPED)
329 | throw new Error('Unsupported template signal flag "swapped"');
330 |
331 | if (typeof this[handlerName] === 'undefined')
332 | throw new Error(`${handlerName} is undefined`);
333 |
334 | return this[handlerName].bind(connectObject || this);
335 | }
336 |
337 | });
338 |
--------------------------------------------------------------------------------
/prefsColumnView.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import Gtk from 'gi://Gtk';
5 | import Gio from 'gi://Gio';
6 |
7 | import * as PrefsWindowPickableEntry from './prefsWindowPickableEntry.js';
8 | import * as PrefsWidgets from './prefsWidgets.js';
9 |
10 | import {PrefsUtils} from './utils/prefsUtils.js';
11 |
12 |
13 | export const ColumnView = GObject.registerClass({
14 | Signals: {
15 | 'activate': {
16 | param_types: [Gtk.CheckButton, GObject.TYPE_OBJECT]
17 | },
18 | 'row-deleted': {
19 | param_types: [GObject.TYPE_OBJECT]
20 | },
21 | },
22 | }, class ColumnView extends Gtk.Box {
23 |
24 | _init(datalist, params = {}) {
25 | super._init({
26 | orientation: Gtk.Orientation.VERTICAL
27 | });
28 |
29 | datalist = datalist ? datalist : [];
30 | this.datalist = datalist;
31 |
32 | this._initUI();
33 | this.updateView(datalist);
34 | }
35 |
36 | _initUI() {
37 | this.model = new Gio.ListStore({ item_type: GObject.TYPE_OBJECT });
38 | this.selectionModel = new Gtk.SingleSelection({ model: this.model });
39 |
40 | this.view = new Gtk.ColumnView({
41 | css_classes: ['view'],
42 | // I feel it's ugly to set this to true
43 | // show_column_separators: true
44 | });
45 | this.view.set_model(this.selectionModel);
46 |
47 | const enabledColumn = PrefsWidgets.newColumnViewColumn('Enabled',
48 | (factory, listItem) => {
49 | const checkButton = new Gtk.CheckButton()
50 | listItem.set_child(checkButton);
51 | }, (factory, listItem) => {
52 | const widget = listItem.get_child();
53 | // item is the CloseWindowsWhitelist instance that is added into the model
54 | const item = listItem.get_item();
55 | // So we can get `enabled` value from `item` here
56 | const enabled = item.enabled
57 | widget.set_active(enabled);
58 | widget.connect('notify::active', () => {
59 | this.emit('activate', widget, item);
60 | });
61 | });
62 |
63 | const operationColumn = PrefsWidgets.newColumnViewColumn('Operation',
64 | (factory, listItem) => {
65 | const button = PrefsWidgets.newRemoveButton();
66 | listItem.set_child(button);
67 | button.connect('clicked', () => {
68 | const item = listItem.get_item();
69 | this.emit('row-deleted', item);
70 | });
71 | }, null);
72 |
73 | this.view.append_column(enabledColumn);
74 | this.view.append_column(operationColumn);
75 |
76 | // Add the ColumnView to the Box
77 | this.append(this.view);
78 | }
79 |
80 | updateView(dataList) {
81 | this.model.remove_all();
82 | for(const item of dataList) {
83 | this.model.append(item);
84 | }
85 | }
86 |
87 | updateRow(settingName, keyName, keyValue, propertyName, value) {
88 | const oldCloseWindowsRules = this._settings.get_string(settingName);
89 | let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules);
90 | const rule = oldCloseWindowsRulesObj[keyValue];
91 | rule[propertyName] = value;
92 | const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj);
93 | this._settings.set_string(settingName, newCloseWindowsRules);
94 | }
95 |
96 | });
97 |
98 | export const WhitelistColumnView = GObject.registerClass({
99 | Signals: {},
100 | Properties: {},
101 | }, class WhitelistColumnView extends ColumnView {
102 |
103 | _init(datalist) {
104 | super._init(datalist, {});
105 |
106 | const settingKey = 'close-windows-whitelist';
107 | this._settings = PrefsUtils.getSettings();
108 |
109 | const nameColumn = PrefsWidgets.newColumnViewColumn('Name',
110 | null, (factory, listItem) => {
111 | const item = listItem.get_item();
112 | const name = item.name ? item.name : '';
113 | const nameEntry = new PrefsWindowPickableEntry.WindowPickableEntry({
114 | text: name,
115 | tooltip_text: name,
116 | pickConditionFunc: (() => {
117 | return 'wm_class';
118 | }).bind(this)
119 | });
120 | listItem.set_child(nameEntry);
121 | nameEntry.connect('entry-edit-complete', (source, entry) => {
122 | this.updateRow(settingKey, 'id', item.id, 'name', entry.get_text());
123 | });
124 | });
125 |
126 | const closeWindowsColumn = PrefsWidgets.newColumnViewColumn('Close windows',
127 | (factory, listItem) => {
128 | const switcher = new Gtk.Switch({halign: Gtk.Align.START, valign: Gtk.Align.CENTER});
129 | listItem.set_child(switcher);
130 | }, (factory, listItem) => {
131 | const widget = listItem.get_child();
132 | const item = listItem.get_item();
133 | const enableWhenCloseWindows = item.enableWhenCloseWindows
134 | widget.set_active(enableWhenCloseWindows);
135 | widget.connect('notify::active', (source) => {
136 | this.updateRow(settingKey, 'id', item.id, 'enableWhenCloseWindows', source.get_active());
137 | });
138 | });
139 |
140 | const logoffColumn = PrefsWidgets.newColumnViewColumn('Log Out, Reboot, Power Off',
141 | (factory, listItem) => {
142 | const switcher = new Gtk.Switch({halign: Gtk.Align.START, valign: Gtk.Align.CENTER});
143 | listItem.set_child(switcher);
144 | }, (factory, listItem) => {
145 | const widget = listItem.get_child();
146 | const item = listItem.get_item();
147 | const enableWhenLogout = item.enableWhenLogout;
148 | widget.set_active(enableWhenLogout);
149 | widget.connect('notify::active', (source) => {
150 | this.updateRow(settingKey, 'id', item.id, 'enableWhenLogout', source.get_active());
151 | });
152 | });
153 |
154 | // The first column is assigned to Enabled column
155 | let index = 1;
156 | this.view.insert_column(index++, nameColumn);
157 | this.view.insert_column(index++, closeWindowsColumn);
158 | this.view.insert_column(index++, logoffColumn);
159 |
160 | this.connect('activate', (source, checkButton, item) => {
161 | const enabled = checkButton.get_active();
162 | this.updateRow(settingKey, 'id', item.id, 'enabled', enabled);
163 | });
164 | this.connect('row-deleted', (source, item) => {
165 | const oldCloseWindowsRules = this._settings.get_string(settingKey);
166 | let oldCloseWindowsRulesObj = JSON.parse(oldCloseWindowsRules);
167 | delete oldCloseWindowsRulesObj[item.id];
168 | const newCloseWindowsRules = JSON.stringify(oldCloseWindowsRulesObj);
169 | this._settings.set_string(settingKey, newCloseWindowsRules);
170 | });
171 | }
172 |
173 | });
174 |
175 |
--------------------------------------------------------------------------------
/prefsWidgets.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import Gtk from 'gi://Gtk';
5 | import GLib from 'gi://GLib';
6 |
7 |
8 | export const boxProperties = {
9 | spacing: 0,
10 | margin_start: 6,
11 | margin_end: 6,
12 | hexpand: true,
13 | halign: Gtk.Align.START,
14 | margin_top: 0,
15 | margin_bottom: 0,
16 | };
17 |
18 | export const newColumnViewColumn = function(title, factorySetupFunc, factoryBindFunc) {
19 | const factory = new Gtk.SignalListItemFactory();
20 | const columnViewColumn = new Gtk.ColumnViewColumn({
21 | title,
22 | factory
23 | });
24 |
25 | if (factorySetupFunc) {
26 | factory.connect('setup', (factory, listItem) => {
27 | factorySetupFunc(factory, listItem);
28 | });
29 | }
30 |
31 | if (factoryBindFunc) {
32 | factory.connect('bind', (factory, listItem) => {
33 | factoryBindFunc(factory, listItem);
34 | });
35 | }
36 | return columnViewColumn;
37 | }
38 |
39 | export const newRemoveButton = function() {
40 | return new BoxRemoveButton();
41 | }
42 |
43 | export const newLabelSwitch = function(text, tooltipText, active) {
44 | return new LabelSwitch(text, tooltipText, active);
45 | }
46 |
47 | export const updateStyle = function(widget, css) {
48 | const cssProvider = new Gtk.CssProvider();
49 | cssProvider.load_from_data(css, -1);
50 | widget.get_style_context().add_provider(cssProvider, Gtk.STYLE_PROVIDER_PRIORITY_APPLICATION);
51 | }
52 |
53 | export const _newBox = function(properties) {
54 | const box = new Gtk.Box({
55 | spacing: 6,
56 | margin_top: 6,
57 | margin_bottom: 6,
58 | margin_start: 6,
59 | margin_end: 6,
60 | })
61 | Object.assign(box, properties);
62 | return box;
63 | }
64 |
65 | export const _newDropDown = function(values, activeValue) {
66 | const dropDownValues = values.map(cv => cv[1]);
67 | const dropDown = Gtk.DropDown.new_from_strings(dropDownValues);
68 | dropDown.set_valign(Gtk.Align.BASELINE);
69 | for (let i = 0; i < dropDownValues.length; i++) {
70 | if (dropDownValues[i] === activeValue)
71 | dropDown.set_selected(i);
72 | }
73 | const factory = dropDown.get_factory();
74 | factory.connect('bind', (factory, listItem) => {
75 | const box = listItem.get_child();
76 | const label = box.get_first_child();
77 | const widthChars = Math.max(...dropDownValues.map(
78 | // GLib.utf8_strlen(v, -1) causes right margin between the label and box is too large, so -2 to reduce this margin
79 | v => GLib.utf8_strlen(v, -1) - 2));
80 | label.set_width_chars(widthChars);
81 | });
82 | return dropDown;
83 | }
84 |
85 | export const LabelSwitch = GObject.registerClass({
86 | Signals: {
87 | 'active': {
88 | param_types: [GObject.TYPE_BOOLEAN, Gtk.Switch]
89 | }
90 | }
91 | }, class LabelSwitch extends Gtk.Box {
92 |
93 | _init(text, tooltipText, active) {
94 | super._init(boxProperties);
95 | this.tooltip_text = tooltipText;
96 |
97 | const [button, switcherBox, switcher] = this._initSwitch(text, tooltipText);
98 | switcher.active = active ? active : false;
99 |
100 | this.append(button);
101 | this.append(switcherBox);
102 |
103 | switcher.connect('notify::active', (switcher) => {
104 | this.emit('active', switcher.get_active(), switcher);
105 | });
106 | }
107 |
108 | _initSwitch(text, tooltipText) {
109 | const button = new Gtk.Button({
110 | label: text,
111 | can_target: false
112 | });
113 | // Imitate a button
114 | // Here we don't use Gtk.Button with a Gtk.Switch. Because I don't want to get into the trouble that
115 | // the click event can't be propagated down to the Gtk.Switch.
116 | const switcherBox = new Gtk.Box({css_name: 'button'});
117 | const switcher = new Gtk.Switch({valign: Gtk.Align.CENTER});
118 |
119 | updateStyle(button,
120 | // Use .text-button if the button displays a label; Use .image-button if it displays an image
121 | `.text-button {
122 | padding-right: 0px;
123 | padding-left: 6px;
124 | border-top-right-radius: 0px;
125 | border-bottom-right-radius: 0px;
126 | }`);
127 |
128 | updateStyle(switcherBox,
129 | `button {
130 | padding-right: 6px;
131 | padding-left: 6px;
132 | border-top-left-radius: 0px;
133 | border-bottom-left-radius: 0px;
134 | }`);
135 |
136 | switcherBox.append(switcher);
137 |
138 | return [button, switcherBox, switcher];
139 | }
140 |
141 | });
142 |
143 | export const BoxRemoveButton = GObject.registerClass({
144 | Signals: {'clicked': {}}
145 | }, class BoxRemoveButton extends Gtk.Box {
146 |
147 | _init() {
148 | super._init(boxProperties);
149 | Object.assign(this, {
150 | hexpand: true,
151 | halign: Gtk.Align.START
152 | });
153 |
154 | const boxRemoveButton = new Gtk.Button({
155 | icon_name: 'edit-delete-symbolic',
156 | });
157 |
158 | this.append(boxRemoveButton);
159 |
160 | boxRemoveButton.connect('clicked', () => {
161 | this.emit('clicked');
162 | });
163 | }
164 |
165 | });
166 |
167 | // TODO This function does not work
168 | export const addScrolledWindow = function(widget) {
169 | const scroll = new Gtk.ScrolledWindow({
170 | vexpand: true,
171 | hexpand: true,
172 | hscrollbar_policy: Gtk.PolicyType.NEVER,
173 | vscrollbar_policy: Gtk.PolicyType.AUTOMATIC
174 | });
175 |
176 | const parent = widget.get_parent();
177 | widget.unparent();
178 | scroll.set_child(widget);
179 | // How to add widget to Adw.PreferencesPage. parent is a Adw.PreferencesPage?
180 | parent.add_child(scroll);
181 |
182 | }
183 |
184 |
--------------------------------------------------------------------------------
/prefsWindowPickableEntry.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Gio from 'gi://Gio';
4 | import GObject from 'gi://GObject';
5 | import Gtk from 'gi://Gtk';
6 |
7 | import * as PrefsWidgets from './prefsWidgets.js';
8 |
9 |
10 | export const WindowPickableEntry = GObject.registerClass({
11 | Signals: {
12 | 'entry-changed': {
13 | param_types: [Gtk.Entry]
14 | },
15 | 'entry-edit-complete': {
16 | param_types: [Gtk.Entry]
17 | },
18 | }
19 | }, class WindowPickableEntry extends Gtk.Box {
20 |
21 | _init(entryParams, boxParams) {
22 |
23 | super._init(PrefsWidgets.gtkBoxProperties);
24 | Object.assign(this, boxParams);
25 |
26 | const entry = new Gtk.Entry({
27 | editable: false,
28 | can_focus: false,
29 | focus_on_click: false,
30 | halign: Gtk.Align.START,
31 | hexpand: true,
32 | // Make sure that text align left
33 | xalign: 0,
34 | width_chars: 20,
35 | max_width_chars: 20,
36 | // ellipsize: Pango.EllipsizeMode.END,
37 | });
38 | this.entry = entry;
39 | this.pickConditionFunc = entryParams.pickConditionFunc;
40 | Object.assign(entry, entryParams);
41 |
42 | this._initEntry(entry);
43 | entry.set_tooltip_text(entry.get_text());
44 |
45 | this.append(entry);
46 | this.append(this.chooseButton);
47 | }
48 |
49 | setText(text) {
50 | this.entry.set_text(text);
51 | }
52 |
53 | _initEntry(entry) {
54 | entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'document-edit-symbolic');
55 | entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, 'Edit the entry');
56 | entry.set_icon_activatable(Gtk.EntryIconPosition.SECONDARY, true);
57 | const iconPressId = entry.connect('icon-press', (source, icon_pos) => {
58 | if (icon_pos !== Gtk.EntryIconPosition.SECONDARY)
59 | return;
60 |
61 | if (source._showSaveIconAWSM) {
62 | delete source._showSaveIconAWSM;
63 | this._completeEditEntry(entry);
64 | if (this._prefsDialogCloseRequestId) {
65 | const prefsDialogWindow = entry.get_root();
66 | if (prefsDialogWindow) prefsDialogWindow.disconnect(this._prefsDialogCloseRequestId);
67 | }
68 | } else {
69 | source.block_signal_handler(iconPressId);
70 |
71 | source.set_can_focus(true);
72 | source.set_editable(true);
73 | source.grab_focus_without_selecting();
74 | // -1 put the cursor to the end
75 | source.set_position(-1);
76 |
77 | source.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'emblem-ok-symbolic');
78 | source.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, 'Complete editing');
79 | source._showSaveIconAWSM = true;
80 |
81 | // Save the entry when we close the prefs dialog window
82 | const prefsDialogWindow = entry.get_root();
83 | if (prefsDialogWindow) {
84 | this._prefsDialogCloseRequestId = prefsDialogWindow.connect('close-request', () => {
85 | this.emit('entry-edit-complete', entry);
86 | prefsDialogWindow.disconnect(this._prefsDialogCloseRequestId);
87 | });
88 | }
89 | source.unblock_signal_handler(iconPressId);
90 | }
91 | });
92 | // Accept Enter key to complete the editing
93 | entry.connect('activate', () => {
94 | this._completeEditEntry(entry);
95 | });
96 | let entryController = Gtk.EventControllerFocus.new();
97 | entry.add_controller(entryController);
98 | entryController.connect('leave', (source) => {
99 | this._completeEditEntry(entry);
100 | });
101 | entry.connect('changed', (source) => {
102 | this.emit('entry-changed', source);
103 | });
104 |
105 | // const image = new Gtk.Image({
106 | // file: IconFinder.findPath('choose-window-symbolic.svg'),
107 | // });
108 | const chooseButton = new Gtk.Button({
109 | icon_name: 'find-location-symbolic',
110 | // label: 'Pick...',
111 | tooltip_text: 'Choose a window to fill the entry based on the current setting',
112 | });
113 | this.chooseButton = chooseButton;
114 |
115 | PrefsWidgets.updateStyle(entry,
116 | `entry {
117 | border-top-right-radius: 0px;
118 | border-bottom-right-radius: 0px;
119 | }`);
120 | PrefsWidgets.updateStyle(chooseButton,
121 | // Use .text-button if the button displays a label; Use .image-button if it displays an image
122 | `.image-button {
123 | padding-left: 0px;
124 | padding-right: 6px;
125 | border-top-left-radius: 0px;
126 | border-bottom-left-radius: 0px;
127 | }`);
128 |
129 | // Pick a window to fetch application and window infos according to the current rule setting
130 | chooseButton.connect('clicked', (source, pickedWidget) => {
131 | if (this._dbusConnection) {
132 | // Unsubscribe the existing PickWindow DBus service, just in case of modifying another entry.
133 | Gio.DBus.session.signal_unsubscribe(this._dbusConnection);
134 | this._dbusConnection = null;
135 | }
136 |
137 | Gio.DBus.session.call(
138 | 'org.gnome.Shell',
139 | '/org/gnome/shell/extensions/awsm',
140 | 'org.gnome.Shell.Extensions.awsm.PickWindow', 'PickWindow',
141 | null, null, Gio.DBusCallFlags.NO_AUTO_START, -1, null, null);
142 |
143 | this._dbusConnection = this._subscribeSignal('WindowPicked', (conn, sender, obj_path, iface, signal, results) => {
144 | // Unsubscribe the PickWindow DBus service, it's really no necessary to keep the subscription all the time
145 | Gio.DBus.session.signal_unsubscribe(this._dbusConnection);
146 | this._dbusConnection = null;
147 |
148 | this._unfocus(entry);
149 |
150 | const resultsArray = results.recursiveUnpack();
151 | // Pick nothing, so we ignore this pick
152 | if(!resultsArray.length) {
153 | return;
154 | }
155 |
156 | const [appName, wmClass, wmClassInstance, title] = resultsArray;
157 | let entryValue = '';
158 | switch (this.pickConditionFunc()) {
159 | case 'wm_class':
160 | entryValue = wmClass;
161 | break;
162 | case 'wm_class_instance':
163 | entryValue = wmClassInstance;
164 | break;
165 | case 'app_name':
166 | entryValue = appName;
167 | break;
168 | case 'title':
169 | entryValue = title;
170 | break;
171 | default:
172 | break;
173 | }
174 |
175 | entry.set_text(entryValue);
176 | entry.set_tooltip_text(entryValue);
177 | this.emit('entry-edit-complete', entry);
178 | });
179 | });
180 |
181 | this._subscribeSignal('WindowPickCancelled', () => {
182 | // Unsubscribe the PickWindow DBus service, it's really no necessary to keep the subscription all the time
183 | Gio.DBus.session.signal_unsubscribe(this._dbusConnection);
184 | this._dbusConnection = null;
185 |
186 | this._unfocus(entry);
187 | });
188 | }
189 |
190 | _subscribeSignal(signalName, callback) {
191 | const dbusConnection = Gio.DBus.session.signal_subscribe(
192 | 'org.gnome.Shell', 'org.gnome.Shell.Extensions.awsm.PickWindow',
193 | signalName,
194 | '/org/gnome/shell/extensions/awsm', null, Gio.DBusSignalFlags.NONE,
195 | callback);
196 | return dbusConnection;
197 | }
198 |
199 | _completeEditEntry(entry) {
200 | entry.set_can_focus(false);
201 | entry.set_editable(false);
202 | this._unfocus(entry);
203 | entry.set_icon_from_icon_name(Gtk.EntryIconPosition.SECONDARY, 'document-edit-symbolic');
204 | entry.set_icon_tooltip_text(Gtk.EntryIconPosition.SECONDARY, 'Edit the entry');
205 | entry.set_tooltip_text(entry.get_text());
206 | this.emit('entry-edit-complete', entry);
207 | }
208 |
209 | _unfocus(widget) {
210 | const prefsDialogWindow = widget.get_root();
211 | if (prefsDialogWindow)
212 | // Pass `null` to unfocus the entry
213 | prefsDialogWindow.set_focus(null);
214 | }
215 |
216 | });
217 |
--------------------------------------------------------------------------------
/restoreSession.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Shell from 'gi://Shell';
4 | import Gio from 'gi://Gio';
5 | import GLib from 'gi://GLib';
6 |
7 | import * as FileUtils from './utils/fileUtils.js';
8 | import * as Log from './utils/log.js';
9 | import {PrefsUtils} from './utils/prefsUtils.js';
10 | import * as SubprocessUtils from './utils/subprocessUtils.js';
11 | import * as DateUtils from './utils/dateUtils.js';
12 | import * as StringUtils from './utils/stringUtils.js';
13 |
14 |
15 | export const restoreSessionObject = {
16 | // All launching apps by Shell.App#launch()
17 | restoringApps: new Map()
18 | }
19 |
20 | export const RestoreSession = class {
21 |
22 | constructor() {
23 | this._log = new Log.Log();
24 | this._settings = PrefsUtils.getSettings();
25 |
26 | this.sessionName = FileUtils.default_sessionName;
27 | this._defaultAppSystem = Shell.AppSystem.get_default();
28 | this._windowTracker = Shell.WindowTracker.get_default();
29 |
30 | this._restore_session_interval = this._settings.get_int('restore-session-interval');
31 |
32 | // TODO Add to Preferences?
33 | // Launch apps using discrete graphics card might cause issues, like the white main window of superproductivity
34 | this._useDiscreteGraphicsCard = false;
35 |
36 | // All launched apps info by Shell.App#launch()
37 | this._restoredApps = new Map();
38 |
39 | // Tracking cmd and appId mapping
40 | this._cmdAppIdMap = new Map();
41 |
42 | this._display = global.display;
43 |
44 | this._connectIds = [];
45 | }
46 |
47 | /**
48 | * Restore workspaces and make them persistent, etc
49 | */
50 | static restoreFromSummary() {
51 | Log.Log.getDefault().debug(`Prepare to restore summary`);
52 | FileUtils.loadSummary().then(([summary, path]) => {
53 | Log.Log.getDefault().info(`Restoring summary from ${path}`);
54 | const savedNWorkspace = summary.n_workspace;
55 | const workspaceManager = global.workspace_manager;
56 | const currentNWorkspace = workspaceManager.n_workspaces;
57 | const moreWorkspace = savedNWorkspace - currentNWorkspace;
58 | if (moreWorkspace) {
59 | for (let i = currentNWorkspace; i <= savedNWorkspace; i++) {
60 | workspaceManager.append_new_workspace(false, DateUtils.get_current_time());
61 | workspaceManager.get_workspace_by_index(i)._keepAliveId = true;
62 | }
63 | }
64 | }).catch(e => Log.Log.getDefault().error(e));
65 | }
66 |
67 | restoreSession(sessionName) {
68 | if (!sessionName) {
69 | sessionName = this.sessionName;
70 | }
71 |
72 | const sessions_path = FileUtils.get_sessions_path();
73 | const session_file_path = GLib.build_filenamev([sessions_path, sessionName]);
74 | if (!GLib.file_test(session_file_path, GLib.FileTest.EXISTS)) {
75 | logError(new Error(`Session file not found: ${session_file_path}`));
76 | return;
77 | }
78 |
79 | this._log.info(`Restoring saved session from ${session_file_path}`);
80 | try {
81 | this.restoreSessionFromFile(session_file_path);
82 | } catch (e) {
83 | logError(e, `Failed to restore ${session_file_path}`);
84 | }
85 | }
86 |
87 | restoreSessionFromFile(session_file_path) {
88 | const session_file = Gio.File.new_for_path(session_file_path);
89 | let [success, contents] = session_file.load_contents(null);
90 | if (!success) {
91 | return;
92 | }
93 |
94 | let session_config = FileUtils.getJsonObj(contents);
95 | let session_config_objects = session_config.x_session_config_objects;
96 | if (!(session_config_objects && session_config_objects.length)) {
97 | this._log.error(new Error(`Session details not found: ${session_file_path}`));
98 | global.notify_error(`No session to restore from ${session_file_path}`, `session config is empty.`);
99 | return;
100 | }
101 |
102 | session_config_objects = session_config_objects.filter(session_config_object => {
103 | const desktop_file_id = session_config_object.desktop_file_id;
104 | if (!desktop_file_id) {
105 | return true;
106 | }
107 | const shellApp = this._defaultAppSystem.lookup_app(desktop_file_id);
108 | if (!shellApp) {
109 | return true;
110 | }
111 |
112 | if (this._appIsRunning(shellApp)) {
113 | this._log.debug(`${shellApp.get_name()} is already running`)
114 | return false;
115 | }
116 |
117 | return true;
118 | });
119 | if (session_config_objects.length === 0) return;
120 |
121 | this._restoreOneSession(session_config_objects.shift());
122 | if (session_config_objects.length === 0) return;
123 |
124 | this._restoreSessionTimeoutId = GLib.timeout_add(GLib.PRIORITY_DEFAULT,
125 | // In milliseconds.
126 | // Note that this timing might not be precise, see https://gjs-docs.gnome.org/glib20~2.66.1/glib.timeout_add
127 | this._restore_session_interval,
128 | () => {
129 | if (!session_config_objects.length) {
130 | return GLib.SOURCE_REMOVE;
131 | }
132 | this._restoreOneSession(session_config_objects.shift());
133 | return GLib.SOURCE_CONTINUE;
134 | }
135 | );
136 | }
137 |
138 | async restorePreviousSession(removeAfterRestore) {
139 | try {
140 | this._log.info(`Restoring the previous session from ${FileUtils.current_session_path}`);
141 |
142 | const ignoringParentFolders = [
143 | GLib.build_filenamev([FileUtils.current_session_path, 'null']),
144 | ];
145 | const ignoringFilePaths = [
146 | GLib.build_filenamev([FileUtils.current_session_path, 'summary.json'])
147 | ];
148 | FileUtils.listAllSessions(FileUtils.current_session_path, true, (file, info) => {
149 | const contentType = info.get_content_type();
150 | if (contentType !== 'application/json') {
151 | return;
152 | }
153 | if (ignoringParentFolders.includes(file.get_parent().get_path())) {
154 | return;
155 | }
156 | if (ignoringFilePaths.includes(file.get_path())) {
157 | return;
158 | }
159 | file.load_contents_async(
160 | null,
161 | (file, asyncResult) => {
162 | const [success, contents, _] = file.load_contents_finish(asyncResult);
163 | if (!success) {
164 | return;
165 | }
166 | const sessionConfig = FileUtils.getJsonObj(contents);
167 | sessionConfig._file_path = file.get_path();
168 | this._restoreOneSession(sessionConfig).then(([launched, running]) => {
169 | if (removeAfterRestore && launched && !running) {
170 | const path = file.get_path();
171 | this._log.debug(`Restored ${sessionConfig.window_title}(${sessionConfig.app_name}), cleaning ${path}`);
172 | FileUtils.removeFile(path);
173 | }
174 | }).catch(e => this._log.error(e));
175 | });
176 |
177 | });
178 | } catch (error) {
179 | this._log.error(error);
180 | }
181 | }
182 |
183 | async _restoreOneSession(session_config_object) {
184 | const app_name = session_config_object.app_name;
185 | let launched = false;
186 | let running = false;
187 | try {
188 | return await new Promise((resolve, reject) => {
189 | let desktop_file_id = session_config_object.desktop_file_id;
190 | const shell_app = desktop_file_id ? this._defaultAppSystem.lookup_app(desktop_file_id) : null;
191 | if (shell_app) {
192 | const restoringShellAppData = restoreSessionObject.restoringApps.get(shell_app);
193 | if (restoringShellAppData) {
194 | restoringShellAppData.saved_window_sessions.push(session_config_object);
195 | } else {
196 | restoreSessionObject.restoringApps.set(shell_app, {
197 | saved_window_sessions: [session_config_object]
198 | });
199 | }
200 |
201 | const desktopNumber = session_config_object.desktop_number;
202 | [launched, running] = this.launch(shell_app, desktopNumber);
203 | if (launched) {
204 | if (!running) {
205 | this._log.info(`${app_name} has been launched! Preparing to restore window ${session_config_object.window_title}(${app_name})!`);
206 | }
207 | const existingShellAppData = this._restoredApps.get(shell_app);
208 | if (existingShellAppData) {
209 | existingShellAppData.saved_window_sessions.push(session_config_object);
210 | } else {
211 | this._restoredApps.set(shell_app, {
212 | saved_window_sessions: [session_config_object]
213 | });
214 | }
215 | } else {
216 | this._log.error(`Failed to launch ${app_name}`, `Failed to launch ${app_name}`);
217 | global.notify_error(`Failed to launch ${app_name}`, `Failed to launch ${app_name}`);
218 | }
219 | resolve([launched, running]);
220 | } else {
221 | // https://gjs-docs.gnome.org/gio20~2.0/gio.subprocesslauncher#method-set_environ
222 | // TODO Support snap apps
223 |
224 | const cmd = session_config_object.cmd;
225 | if (cmd && cmd.length) {
226 | const cmdString = cmd.join(' ');
227 | const pid = this._cmdAppIdMap.get(cmdString);
228 | if (pid) {
229 | this._log.debug(`${app_name} might be running, preparing to restore window (${session_config_object.window_title}) states.`);
230 |
231 | // Here we use pid as the key, because the associated ShellApp might not be instantiated at this moment
232 | const restoringShellAppData = restoreSessionObject.restoringApps.get(pid);
233 | if (restoringShellAppData) {
234 | restoringShellAppData.saved_window_sessions.push(session_config_object);
235 | } else {
236 | restoreSessionObject.restoringApps.set(pid, {
237 | saved_window_sessions: [session_config_object]
238 | });
239 | }
240 | }
241 |
242 | const launchAppTemplate = FileUtils.desktop_template_launch_app_shell_script;
243 | const launchAppShellScript = StringUtils.format(FileUtils.loadTemplate(launchAppTemplate), {cmdString});
244 | this._log.info(`Launching ${app_name} via command line ${cmdString}!`);
245 | SubprocessUtils.trySpawnCmdstr(`bash -c '${launchAppShellScript}'`).then(
246 | ([success, status, stdoutInputStream, stderrInputStream]) => {
247 | if (success) {
248 | stdoutInputStream.read_line_async(
249 | GLib.PRIORITY_DEFAULT,
250 | null,
251 | (stream, res) => {
252 | try {
253 | let pid = stream.read_line_finish_utf8(res)[0];
254 | if (!pid) return;
255 |
256 | pid = Number(pid);
257 | this._cmdAppIdMap.set(cmdString, pid);
258 | const restoringShellAppData = restoreSessionObject.restoringApps.get(pid);
259 | if (restoringShellAppData) {
260 | restoringShellAppData.saved_window_sessions.push(session_config_object);
261 | } else {
262 | restoreSessionObject.restoringApps.set(pid, {
263 | saved_window_sessions: [session_config_object]
264 | });
265 | }
266 | launched = true;
267 | resolve([launched, running]);
268 | } catch (e) {
269 | this._log.error(e);
270 | reject(e);
271 | }
272 | }
273 | );
274 | } else {
275 | if (status === 79) {
276 | launched = true;
277 | running = true;
278 | this._log.info(`${app_name} is running, skipping`)
279 | } else {
280 | const msg = `Failed to launch ${app_name} via command line`;
281 | let errorDetail = `Can't restore this app from ${session_config_object._file_path}: ${stderr}.`;
282 | this._log.error(`${msg}. output: ${errorDetail}`);
283 | global.notify_error(`${msg}`, errorDetail);
284 | }
285 | resolve([launched, running]);
286 | }
287 | }).catch(e => {
288 | this._log.error(e)
289 | reject(e);
290 | });
291 | } else {
292 | // TODO try to launch via app_info by searching the app name?
293 | let errorMsg = `Failed to launch ${app_name} via command line`;
294 | let errorDetail = `Can't restore this app from ${session_config_object._file_path}: Invalid command line: ${cmd}.`;
295 | this._log.error(errorMsg, errorDetail);
296 | global.notify_error(errorMsg, errorDetail);
297 | resolve([launched, running]);
298 | }
299 | }
300 | });
301 | } catch (e) {
302 | logError(e, `Failed to restore ${app_name}`);
303 | if (!launched) {
304 | global.notify_error(`Failed to restore ${app_name}`, e.message);
305 | }
306 | return [launched, running];
307 | }
308 | }
309 |
310 | launch(shellApp, desktopNumber) {
311 | if (this._restoredApps.has(shellApp)) {
312 | this._log.info(`${shellApp.get_name()} is restored, skipping`);
313 | return [true, false];
314 | }
315 |
316 | if (this._appIsRunning(shellApp)) {
317 | this._log.info(`${shellApp.get_name()} is running, skipping`);
318 | // Delete shellApp from restoringApps to prevent it move the same app when close and open it manually.
319 | restoreSessionObject.restoringApps.delete(shellApp);
320 | return [true, true];
321 | }
322 |
323 | const launched = shellApp.launch(
324 | // 0 for current event timestamp
325 | 0,
326 | desktopNumber,
327 | this._getProperGpuPref(shellApp));
328 | return [launched, false];
329 | }
330 |
331 | _appIsRunning(app) {
332 | // Running apps can be empty even if there are apps running when gnome-shell starting
333 | const running_apps = this._defaultAppSystem.get_running();
334 | for (const running_app of running_apps) {
335 | if (running_app.get_id() === app.get_id() &&
336 | running_app.get_state() >= Shell.AppState.STARTING) {
337 | return true;
338 | }
339 | }
340 | return false;
341 | }
342 |
343 | _getProperGpuPref(shell_app) {
344 | if (this._useDiscreteGraphicsCard) {
345 | const app_info = shell_app.get_app_info();
346 | if (app_info) {
347 | return app_info.get_boolean('PrefersNonDefaultGPU')
348 | ? Shell.AppLaunchGpu.DEFAULT
349 | : Shell.AppLaunchGpu.DISCRETE;
350 | }
351 | }
352 | return Shell.AppLaunchGpu.DEFAULT;
353 | }
354 |
355 | destroy() {
356 | if (restoreSessionObject.restoringApps) {
357 | restoreSessionObject.restoringApps.clear();
358 | restoreSessionObject.restoringApps = null;
359 | }
360 |
361 | if (this._restoredApps) {
362 | this._restoredApps.clear();
363 | this._restoredApps = null;
364 | }
365 |
366 | if (this._defaultAppSystem) {
367 | this._defaultAppSystem = null;
368 | }
369 |
370 | if (this._windowTracker) {
371 | this._windowTracker = null;
372 | }
373 |
374 | if (this._log) {
375 | this._log.destroy();
376 | this._log = null;
377 | }
378 |
379 | if (this._connectIds) {
380 | for (let [obj, id] of this._connectIds) {
381 | obj.disconnect(id);
382 | }
383 | this._connectIds = null;
384 | }
385 |
386 | if (this._restoreSessionTimeoutId) {
387 | GLib.Source.remove(this._restoreSessionTimeoutId);
388 | this._restoreSessionTimeoutId = null;
389 | }
390 |
391 | }
392 |
393 | }
394 |
--------------------------------------------------------------------------------
/schemas/gschemas.compiled:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/nlpsuge/gnome-shell-extension-another-window-session-manager/9379f86e95fd121bc76d10d69936b622f4ee81d1/schemas/gschemas.compiled
--------------------------------------------------------------------------------
/schemas/org.gnome.shell.extensions.another-window-session-manager.gschema.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | true
8 | Enable or disable the ability of closing windows by customed rules, such as a shortcut
9 |
10 |
11 |
12 |
13 |
14 | '[]'
15 | The mapping of xid and stable sequence of a window on X11
16 |
17 |
18 |
19 |
20 | '{}'
21 | Rules that are used to close applications
22 |
23 | Rules that are used to close applications
24 |
25 |
26 |
27 | '{}'
28 | Rules that are used to close applications by keyword
29 |
30 | Rules that are used to close applications by keyword
31 |
32 |
33 |
34 | '{}'
35 |
36 | A whitelist that contains apps or windwows that can be closed safely,
37 | even they have multiple windows.
38 |
39 |
40 |
41 |
42 |
43 | ''
44 | Session(s) restored on startup
45 |
46 |
47 |
48 |
49 | true
50 |
51 | Enable to close running apps and windows while Logout, Reboot and Shutdown
52 | via endSessionDialog.
53 |
54 |
55 |
56 |
57 |
58 | true
59 |
60 | Show a notification when users click buttons on the popup menu to save a session.
61 |
62 |
63 |
64 |
65 |
66 | false
67 | Enable to Restore previous apps and windows at startup
68 |
69 |
70 |
71 |
72 | false
73 | Enable to restore session(s) on startup
74 |
75 | Enable to restore session(s) on startup and install
76 | `~/.config/autostart/_gnome-shell-extension-another-window-session-manager.desktop`.
77 |
78 |
79 |
80 | 0
81 | The interval restoring applications
82 |
83 | Restore applications at intervals of some milliseconds, up to 5 minutes
84 |
85 |
86 |
87 | false
88 | Restore at startup without asking
89 |
90 | Restore immediately at startup without asking
91 |
92 |
93 |
94 | 10
95 | Set the timer on the AutostartDialog for some seconds
96 |
97 | Set the timer on the AutostartDialog for some seconds, the upper value is 3600s
98 |
99 |
100 |
101 | 5
102 | Start to restore the previous session after a specified delay
103 |
104 | Start to restore the previous session after a specified delay, the upper value is 3600s
105 |
106 |
107 |
108 | 5
109 | Autostart delay
110 |
111 | A specified amount of time to delay to execute the target command.
112 |
113 |
114 |
115 | true
116 | Restore window edge tiling
117 |
118 |
119 |
120 |
121 | true
122 | Raise all two windows together
123 |
124 | Raise all two windows together while one of the pairs is raised while tiling.
125 |
126 |
127 |
128 | true
129 | Stash and restore states
130 |
131 | Stash states while Gnome Shell restarts via `Alt+F2 -> r` or `killall -3 gnome-shell`,
132 | and restore the windows states after the restart finishes.
133 |
134 | Only enabled on X11, since Wayand doesn't support restart on fly.
135 |
136 |
137 |
138 | true
139 | Show or hide indicator on the panel
140 |
141 |
142 |
143 |
144 |
145 | false
146 | Enable or disable the debugging mode
147 |
148 | Enable the debugging mode to see what happened, for debugging or issue reporting or development purpose
149 |
150 |
151 |
152 | false
153 | Enable or disable verbose logging
154 |
155 | Enable verbose logging for more informational output like windows position saving, session information etc
156 |
157 |
158 |
159 |
--------------------------------------------------------------------------------
/stylesheet.css:
--------------------------------------------------------------------------------
1 | .awsm-toggle-switch {
2 | background-image: url("./icons/toggle-off-autorestore-symbolic.svg");
3 | }
4 |
5 | .awsm-toggle-switch:checked {
6 | background-image: url("./icons/toggle-on-autorestore-symbolic.svg");
7 | }
8 |
9 | /* Restore Session Dialog */
10 | .restore-session-dialog {
11 | width: 30em;
12 | }
13 |
14 |
15 | /**
16 | * *-tooltip: Adapted from: https://github.com/GSConnect/gnome-shell-extension-gsconnect/blob/master/src/stylesheet.css
17 | */
18 | .awsm-tooltip {
19 | border-radius: 3px;
20 | min-width: 0;
21 | min-height: 0;
22 | padding: 6px;
23 | }
24 |
25 | .awsm-tooltip > StBoxLayout {
26 | spacing: 6px;
27 | }
28 |
29 | .awsm-tooltip StIcon {
30 | icon-size: 16px;
31 | }
32 |
33 | .awsm-tooltip StLabel {
34 | font-weight: normal;
35 | text-align: left;
36 | }
37 |
38 | .awsm-tooltip StLabel:rtl {
39 | text-align: right;
40 | }
41 |
42 |
43 |
44 | .session-menu-section {
45 | max-height:600px;
46 | }
47 |
48 | .confirm-before-operate {
49 | color: #F00;
50 | font-weight: bold;
51 | font-style: italic
52 | }
53 |
54 | .confirm-before-operate:hover, .confirm-before-operate:focus {
55 | background-color: rgba(255,255,255,0.2);
56 | border: none;
57 | padding: 5px;
58 | }
59 |
60 | .button-item > StIcon {
61 | icon-size: 14px;
62 | }
63 |
64 | .aws-item-separator {
65 | border-radius: 32px;
66 | padding: 4px;
67 | /*
68 | Remove cycle / border in the background in case everyone think this separator is a clickable button, actually it's just a view-only separator.
69 |
70 | https://developer.mozilla.org/en-US/docs/Web/CSS/border
71 | */
72 | border: none;
73 | }
74 |
75 | .aws-item-separator > StIcon {
76 | icon-size: 14px;
77 | }
78 |
79 | /**
80 | Add highlight effect when hover over the buttons in the item
81 |
82 | Based on https://gitlab.com/bartl/todo-txt-gnome-shell-extension/-/blob/master/stylesheet.css
83 | */
84 | .aws-item-button {
85 | border-radius: 32px;
86 | padding: 4px;
87 | border: 1px solid #282c2c;
88 | }
89 |
90 | .aws-item-button:hover, .aws-item-button:focus {
91 | background-color: rgba(255,255,255,0.2);
92 | border: none;
93 | padding: 5px;
94 | }
95 |
96 | .aws-item-button > StIcon {
97 | icon-size: 14px;
98 | }
99 |
--------------------------------------------------------------------------------
/template/60-awsm-ydotool-uinput.rules:
--------------------------------------------------------------------------------
1 | # This file is part of gnome-shell-extension-another-window-session-manager
2 |
3 | # See:
4 | # https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules
5 | # https://github.com/ReimuNotMoe/ydotool/issues/25
6 |
7 | # ydotool udev write access
8 | KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"
9 |
--------------------------------------------------------------------------------
/template/_awsm-restore-previous-session.desktop:
--------------------------------------------------------------------------------
1 | # This file is part of gnome-shell-extension-another-window-session-manager
2 |
3 | # Do NOT modify this file, which could be overridden or deleted by this extension.
4 |
5 |
6 | [Desktop Entry]
7 | Name=Restore the previous session at startup
8 | Comment=Restore the previous session at startup
9 | Icon=
10 | Exec=bash -c 'sleep ${autostartDelay} && gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestorePreviousSession "{}"'
11 | Terminal=false
12 | Type=Application
13 | # This option does not work on my machine, so I use `sleep` instead ...
14 | X-GNOME-Autostart-Delay=
15 |
--------------------------------------------------------------------------------
/template/_gnome-shell-extension-another-window-session-manager.desktop:
--------------------------------------------------------------------------------
1 | # This file is part of gnome-shell-extension-another-window-session-manager
2 |
3 | # Do NOT modify this file, which could be overridden or deleted by this extension.
4 |
5 |
6 | [Desktop Entry]
7 | Name=Restore saved session(s) at startup
8 | Comment=Restore saved session(s) at startup
9 | Icon=
10 | Exec=bash -c 'sleep ${autostartDelay} && gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestoreSession'
11 | Terminal=false
12 | Type=Application
13 | # This option does not work on my machine, so I use `sleep` instead ...
14 | X-GNOME-Autostart-Delay=
15 |
--------------------------------------------------------------------------------
/template/launch-app.sh:
--------------------------------------------------------------------------------
1 | if ! pgrep -f "${cmdString}" | grep -v "$$"; then
2 | ${cmdString} > /dev/null &
3 | echo $! >&1
4 | else
5 | # App must be started or running
6 | # See: https://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
7 | exit 79
8 | fi
9 |
10 |
--------------------------------------------------------------------------------
/template/template.desktop:
--------------------------------------------------------------------------------
1 | # This file is part of gnome-shell-extension-another-window-session-manager
2 |
3 | # I adopted this file from VirtualBox
4 | # Generated by gnome-shell-extension-another-window-session-manager
5 | # https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager
6 |
7 |
8 | [Desktop Entry]
9 | Encoding=UTF-8
10 | Version=1.0
11 | Name=${appName}
12 | Comment=${appName}
13 | Type=Application
14 | Exec=${commandLine}
15 | Icon=${icon}
16 | StartupWMClass=${wmClass}
17 | # If the former does not work, use the below line instead.
18 | # If it still does not work, please fill an issue at:
19 | # https://github.com/nlpsuge/gnome-shell-extension-another-window-session-manager/issues
20 | # StartupWMClass=${wmClassInstance}
21 |
--------------------------------------------------------------------------------
/ui/autostart.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /* exported AutostartServiceProvider, AutostartService, AutostartDialog */
4 |
5 | import GObject from 'gi://GObject';
6 | import Gio from 'gi://Gio';
7 | import GLib from 'gi://GLib';
8 | import Clutter from 'gi://Clutter';
9 |
10 | import * as EndSessionDialog from 'resource:///org/gnome/shell/ui/endSessionDialog.js';
11 |
12 | import {gettext as _} from 'resource:///org/gnome/shell/extensions/extension.js';
13 |
14 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
15 | import * as Dialog from 'resource:///org/gnome/shell/ui/dialog.js';
16 | import * as ModalDialog from 'resource:///org/gnome/shell/ui/modalDialog.js';
17 |
18 | import * as Autoclose from './autoclose.js';
19 | import * as RestoreSession from '../restoreSession.js';
20 | import * as Constants from '../constants.js';
21 |
22 | import * as Log from '../utils/log.js';
23 | import {PrefsUtils} from '../utils/prefsUtils.js';
24 | import * as FileUtils from '../utils/fileUtils.js';
25 |
26 |
27 | let _requiredToRestorePrevious = false;
28 |
29 | export const AutostartServiceProvider = GObject.registerClass(
30 | class AutostartServiceProvider extends GObject.Object {
31 |
32 | _init() {
33 | super._init();
34 |
35 | this._log = new Log.Log();
36 |
37 | this._autostartDbusXml = new TextDecoder().decode(
38 | FileUtils.current_extension_dir.get_child('dbus-interfaces').get_child('org.gnome.Shell.Extensions.awsm.Autostart.xml').load_contents(null)[1]);
39 |
40 | this._autostartService = null;
41 | this._autostartDbusImpl = null;
42 |
43 | // https://gjs.guide/guides/gio/dbus.html#exporting-interfaces
44 | this._dbusNameOwnerId = Gio.bus_own_name(
45 | Gio.BusType.SESSION,
46 | 'org.gnome.Shell.Extensions.awsm',
47 | Gio.BusNameOwnerFlags.NONE,
48 | this.onBusAcquired.bind(this),
49 | this.onNameAcquired.bind(this),
50 | this.onNameLost.bind(this),
51 | );
52 |
53 |
54 | }
55 |
56 | onBusAcquired(connection, name) {
57 | this._log.debug(`DBus bus with name ${name} acquired!`);
58 |
59 | this._autostartService = new AutostartService();
60 |
61 | // Gio.DBusExportedObject.wrapJSObject(interfaceInfo, jsObj) is a private method of gjs
62 | // See: https://gitlab.gnome.org/GNOME/gjs/-/blob/master/modules/core/overrides/Gio.js#L391
63 | this._autostartDbusImpl = Gio.DBusExportedObject.wrapJSObject(this._autostartDbusXml, this._autostartService);
64 | this._autostartDbusImpl.export(connection, '/org/gnome/Shell/Extensions/awsm');
65 |
66 | }
67 |
68 | onNameAcquired(connection, name) {
69 | this._log.debug(`DBus name ${name} acquired!`);
70 | }
71 |
72 | onNameLost(connection, name) {
73 | this._log.debug(`Dbus name ${name} lost`);
74 | }
75 |
76 | disable() {
77 | // To avoid the below error
78 | // JS ERROR: Gio.IOErrorEnum: An object is already exported for the interface org.gnome.Shell.Extensions.awsm.Autostart at /org/gnome/Shell/Extensions/awsm
79 | // when disable and enable this extension
80 | this._autostartDbusImpl.flush();
81 | this._autostartDbusImpl.unexport();
82 |
83 | if (this._autostartService) {
84 | this._autostartService._disable();
85 | this._autostartService = null;
86 | }
87 | }
88 | });
89 |
90 | const AutostartService = GObject.registerClass(
91 | class AutostartService extends GObject.Object {
92 |
93 | _init() {
94 | super._init();
95 |
96 | this._log = new Log.Log();
97 | this._autostartDialog = null;
98 | this._restorePreviousSourceId = 0;
99 | this._idleIdOpenRestoreSessionDialog = 0;
100 |
101 | this._settings = PrefsUtils.getSettings();
102 | this._sessionName = this._settings.get_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS);
103 | }
104 |
105 | // Call this method asynchronously through `gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestoreSession`
106 | RestoreSession() {
107 | const enableRestoreSelectedSession = this._settings.get_boolean('enable-autorestore-sessions');
108 | if (!enableRestoreSelectedSession) {
109 | const enableRestorePreviousSession = this._settings.get_boolean('enable-restore-previous-session');
110 | if (enableRestorePreviousSession) {
111 | return "Ignoring this operation. RestoreSession is disabled, but RestorePreviousSession is enabled";
112 | }
113 | const disabledFeatureMsg = "ERROR: RestoreSession is disabled, please enable it through 'Preferences -> Restore sessions -> Restore selected session at startup'";
114 | Main.notify('Another Window Session Manager', disabledFeatureMsg);
115 | return disabledFeatureMsg;
116 | }
117 |
118 | const restoringMsg = `Restoring selected session '${this._sessionName}'`;
119 | this._log.info(restoringMsg);
120 | Main.notify('Another Window Session Manager', restoringMsg);
121 |
122 | this._autostartDialog = new AutostartDialog();
123 | if (this._settings.get_boolean('restore-at-startup-without-asking')) {
124 | this._autostartDialog._confirm();
125 | return `Restore session '${this._sessionName}' without asking ...`;
126 | } else {
127 | // Since this._autostartDialog.open() is idempotent (it will check the dialog state),
128 | // it's ok to call it twice.
129 | // Before the startup-complete emits, `Main.pushModal(this, params).get_seat_state()`
130 | // returns CLUTTER_GRAB_STATE_NONE (0), which causes the dialog can't open. See: modalDialog.open() -> modalDialog.pushModal()
131 | Main.layoutManager.connect('startup-complete', () => {
132 | this._idleIdOpenRestoreSessionDialog = GLib.idle_add(GLib.PRIORITY_LOW, () => {
133 | this._autostartDialog.open();
134 | this._idleIdOpenRestoreSessionDialog = null;
135 | return GLib.SOURCE_REMOVE;
136 | });
137 | });
138 | this._autostartDialog.open();
139 | return 'Opening dialog to restore ...';
140 | }
141 |
142 | }
143 |
144 | // TODO Press some hotkey (like Ctrl) so this time will not restore the previous session?
145 | // Call this method asynchronously through, for example:
146 | // `gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestorePreviousSession "{'removeAfterRestore': }"`
147 | // `gdbus call --session --dest org.gnome.Shell.Extensions.awsm --object-path /org/gnome/Shell/Extensions/awsm --method org.gnome.Shell.Extensions.awsm.Autostart.RestorePreviousSession "{}"`
148 | RestorePreviousSession(args) {
149 | let removeAfterRestore = args.removeAfterRestore;
150 | if (removeAfterRestore) {
151 | removeAfterRestore = removeAfterRestore.get_boolean();
152 | } else {
153 | removeAfterRestore = true;
154 | }
155 | return this._restorePreviousSession(removeAfterRestore);
156 | }
157 |
158 | _restorePreviousSession(removeAfterRestore) {
159 | const enableRestorePreviousSession = this._settings.get_boolean('enable-restore-previous-session');
160 | if (!enableRestorePreviousSession) {
161 | const enableRestoreSelectedSession = this._settings.get_boolean('enable-autorestore-sessions');
162 | if (enableRestoreSelectedSession) {
163 | return "Ignoring this operation. RestorePreviousSession is disabled, but RestoreSession is enabled";
164 | }
165 | const disabledFeatureMsg = "ERROR: RestorePreviousSession is disabled, please enable it through 'Preferences -> Restore sessions -> Restore previous apps and windows at startup'";
166 | Main.notify('Another Window Session Manager', disabledFeatureMsg);
167 | return disabledFeatureMsg;
168 | }
169 |
170 | if (!Main.layoutManager._startingUp) {
171 | const msg = 'Restoring the previous apps and windows';
172 | this._log.info(`${msg}, gnome shell layoutManager has been started up.`);
173 | Main.notify('Another Window Session Manager', msg);
174 |
175 | this._restorePreviousWithDelay(removeAfterRestore);
176 | return msg;
177 | } else {
178 | if (_requiredToRestorePrevious) return;
179 |
180 | _requiredToRestorePrevious = true;
181 | const msg = 'Required to restore the previous apps and windows';
182 | Main.notify('Another Window Session Manager', msg);
183 | Main.layoutManager.connect('startup-complete', () => {
184 | const msg = 'Restoring the previous apps and windows';
185 | this._log.info(`${msg} after startup-complete`);
186 | Main.notify('Another Window Session Manager', msg);
187 | this._restorePreviousWithDelay(removeAfterRestore);
188 | });
189 | return msg;
190 | }
191 |
192 | }
193 |
194 | _restorePreviousWithDelay(removeAfterRestore) {
195 | const restorePreviousDelay = this._settings.get_int('restore-previous-delay');
196 | this._restorePreviousSourceId = GLib.timeout_add(GLib.PRIORITY_LOW, restorePreviousDelay,
197 | () => {
198 | const restoreSession = new RestoreSession.RestoreSession();
199 | restoreSession.restorePreviousSession(removeAfterRestore);
200 | return GLib.SOURCE_REMOVE;
201 | });
202 | }
203 |
204 | _disable() {
205 | if (this._autostartDialog) {
206 | this._autostartDialog.destroy();
207 | this._autostartDialog = null;
208 | }
209 | if (this._restorePreviousSourceId) {
210 | GLib.Source.remove(this._restorePreviousSourceId);
211 | this._restorePreviousSourceId = null;
212 | }
213 | if (this._idleIdOpenRestoreSessionDialog) {
214 | GLib.Source.remove(this._idleIdOpenRestoreSessionDialog);
215 | this._idleIdOpenRestoreSessionDialog = null;
216 | }
217 | }
218 |
219 | });
220 |
221 | // Based on endSessionDialog in Gnome shell
222 | const AutostartDialog = GObject.registerClass(
223 | class AutostartDialog extends ModalDialog.ModalDialog {
224 |
225 | _init() {
226 | super._init({
227 | styleClass: 'restore-session-dialog',
228 | destroyOnClose: true
229 | });
230 |
231 | this._settings = PrefsUtils.getSettings();
232 |
233 | this._sessionName = this._settings.get_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS);
234 |
235 | this._totalSecondsToStayOpen = this._settings.get_int('autorestore-sessions-timer');
236 | this._secondsLeft = 0;
237 |
238 | this.connect('opened', this._onOpened.bind(this));
239 |
240 | this._confirmDialogContent = new Dialog.MessageDialogContent();
241 | this._confirmDialogContent.title = `Restore session '${this._sessionName}'`;
242 |
243 | this.addButton({
244 | action: this._cancel.bind(this),
245 | label: _('Cancel'),
246 | key: Clutter.KEY_Escape,
247 | });
248 |
249 | this._confirmButton = this.addButton({
250 | action: () => {
251 | this.close();
252 | let signalId = this.connect('closed', () => {
253 | this.disconnect(signalId);
254 | this._confirm();
255 | });
256 | },
257 | label: _('Confirm'),
258 | });
259 |
260 | this.contentLayout.add_child(this._confirmDialogContent);
261 |
262 | }
263 |
264 | _confirm() {
265 | Autoclose.autocloseObject.sessionClosedByUser = false;
266 | const _restoreSession = new RestoreSession.RestoreSession();
267 | _restoreSession.restoreSession(this._sessionName);
268 | }
269 |
270 | _cancel() {
271 | this.close();
272 | }
273 |
274 | _onOpened() {
275 | let open = this.state == ModalDialog.State.OPENING || this.state == ModalDialog.State.OPENED;
276 | if (!open)
277 | return;
278 |
279 | if (this._sessionName) {
280 | const [exists, sessionFilePath] = FileUtils.sessionExists(this._sessionName);
281 | if (exists) {
282 | this._startTimer();
283 | this._sync();
284 | } else {
285 | this._confirmDialogContent.description = `ERROR: Session '${this._sessionName}' does not exist`;
286 | this._confirmDialogContent._description.set_style('color:red;');
287 | this._confirmButton.set_reactive(false);
288 | }
289 | } else {
290 | this._confirmDialogContent.description = "ERROR: You don't select any session to restore";
291 | this._confirmDialogContent._description.set_style('color:red;');
292 | this._confirmButton.set_reactive(false);
293 | }
294 | }
295 |
296 | _sync() {
297 |
298 | const displayTime = EndSessionDialog._roundSecondsToInterval(this._totalSecondsToStayOpen,
299 | this._secondsLeft,
300 | 1);
301 | const desc = _.ngettext('\'' + this._sessionName + '\' will be restored in %d second',
302 | '\'' + this._sessionName + '\' will be restored in %d seconds', displayTime).format(displayTime);
303 | this._confirmDialogContent.description = desc;
304 |
305 | }
306 |
307 | _startTimer() {
308 | let startTime = GLib.get_monotonic_time();
309 | this._secondsLeft = this._totalSecondsToStayOpen;
310 |
311 | this._timerId = GLib.timeout_add_seconds(GLib.PRIORITY_DEFAULT, 1, () => {
312 | let currentTime = GLib.get_monotonic_time();
313 | let secondsElapsed = (currentTime - startTime) / 1000000;
314 |
315 | this._secondsLeft = this._totalSecondsToStayOpen - secondsElapsed;
316 | if (this._secondsLeft > 0) {
317 | this._sync();
318 | return GLib.SOURCE_CONTINUE;
319 | }
320 |
321 | this._confirm();
322 | this.close();
323 | this._timerId = 0;
324 |
325 | return GLib.SOURCE_REMOVE;
326 | });
327 | GLib.Source.set_name_by_id(this._timerId, '[gnome-shell-extension-another-window-session-manager] this._confirm');
328 | }
329 |
330 | destroy() {
331 | if (this._timerId > 0) {
332 | GLib.source_remove(this._timerId);
333 | this._timerId = 0;
334 | }
335 | this._secondsLeft = 0;
336 | }
337 |
338 |
339 | });
--------------------------------------------------------------------------------
/ui/button.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import St from 'gi://St';
5 | import Clutter from 'gi://Clutter';
6 |
7 | import * as IconFinder from '../utils/iconFinder.js';
8 |
9 |
10 | export const Button = GObject.registerClass(
11 | class Button extends GObject.Object {
12 |
13 | _init(properties) {
14 | super._init();
15 |
16 | this.button_style_class = null;
17 | this.icon_symbolic = null;
18 |
19 | Object.assign(this, properties);
20 |
21 | this.button = this._createButton(this.icon_symbolic);
22 |
23 | }
24 |
25 | _createButton(iconSymbolic) {
26 | let icon = new St.Icon({
27 | gicon: IconFinder.find(iconSymbolic),
28 | style_class: 'system-status-icon'
29 | });
30 |
31 | let button = new St.Button({
32 | style_class: this.button_style_class ? this.button_style_class : 'aws-item-button',
33 | can_focus: true,
34 | child: icon,
35 | x_align: Clutter.ActorAlign.END,
36 | x_expand: false,
37 | y_expand: true,
38 | track_hover: true
39 | });
40 |
41 | return button;
42 | }
43 |
44 | destroy() {
45 | if (this.button) {
46 | this.button = null;
47 | }
48 |
49 | }
50 |
51 | });
52 |
--------------------------------------------------------------------------------
/ui/popupMenuButtonItems.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import St from 'gi://St';
5 | import Clutter from 'gi://Clutter';
6 |
7 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
8 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
9 |
10 | import * as SaveSession from '../saveSession.js';
11 | import * as CloseSession from '../closeSession.js';
12 | import * as RestoreSession from '../restoreSession.js';
13 |
14 | import * as FileUtils from '../utils/fileUtils.js';
15 | import * as Log from '../utils/log.js';
16 |
17 | import {Button} from './button.js';
18 |
19 |
20 | export const PopupMenuButtonItems = GObject.registerClass(
21 | class PopupMenuButtonItems extends GObject.Object {
22 |
23 | _init() {
24 | super._init();
25 | this.buttonItems = [];
26 | this.addButtonItems();
27 | }
28 |
29 | addButtonItems() {
30 | const popupMenuButtonItemClose = new PopupMenuButtonItemClose('close-symbolic.svg');
31 | const popupMenuButtonItemSave = new PopupMenuButtonItemSave('save-symbolic.svg');
32 |
33 | this.buttonItems.push(popupMenuButtonItemClose);
34 | this.buttonItems.push(popupMenuButtonItemSave);
35 | }
36 |
37 | });
38 |
39 |
40 | const PopupMenuButtonItem = GObject.registerClass(
41 | class PopupMenuButtonItem extends PopupMenu.PopupMenuItem {
42 |
43 | _init() {
44 | super._init('');
45 |
46 | this.yesButton = null;
47 | this.noButton = null;
48 | }
49 |
50 | /**
51 | * Hide both Yes and No buttons by default
52 | */
53 | createYesAndNoButtons() {
54 | this.yesButton = this.createButton('emblem-ok-symbolic');
55 | this.noButton = this.createButton('edit-undo-symbolic');
56 | this.yesButton.add_style_class_name('confirm-before-operate');
57 | this.noButton.add_style_class_name('confirm-before-operate');
58 | this.hideYesAndNoButtons();
59 | }
60 |
61 | showYesAndNoButtons() {
62 | this.yesButton.show();
63 | this.noButton.show();
64 | }
65 |
66 | hideYesAndNoButtons() {
67 | this.yesButton.hide();
68 | this.noButton.hide();
69 | }
70 |
71 | createButton(iconSymbolic) {
72 | const button = new Button({
73 | icon_symbolic: iconSymbolic,
74 | button_style_class: 'button-item',
75 | }).button;
76 | return button;
77 | }
78 |
79 | createTimeLine() {
80 | // Set actor when using
81 | const timeline = new Clutter.Timeline({
82 | // 2s
83 | duration: 2000,
84 | repeat_count: 0,
85 | });
86 | return timeline;
87 | }
88 |
89 | // Add the icon description
90 | addIconDescription(iconDescription) {
91 | this.iconDescriptionLabel = new St.Label({
92 | text: iconDescription
93 | });
94 | this.actor.add_child(this.iconDescriptionLabel);
95 | }
96 |
97 | });
98 |
99 |
100 | const PopupMenuButtonItemClose = GObject.registerClass(
101 | class PopupMenuButtonItemClose extends PopupMenuButtonItem {
102 |
103 | _init(iconSymbolic) {
104 | super._init();
105 | this.confirmLabel;
106 |
107 | this.closingLabel;
108 |
109 | this.closeSession = new CloseSession.CloseSession(CloseSession.flags.closeWindows);
110 |
111 | this._createButton(iconSymbolic);
112 | this.addIconDescription('Close open windows');
113 | this._addConfirm();
114 | this._addYesAndNoButtons();
115 | this._addClosingPrompt();
116 |
117 | this._hideConfirm();
118 |
119 | this._timeline = this.createTimeLine();
120 |
121 | // Respond to menu item's 'activate' signal so user don't need to click the icon whose size is too small to find to click
122 | this.connect('activate', this._onActivate.bind(this));
123 |
124 | }
125 |
126 | _onActivate() {
127 | this._onClicked();
128 | }
129 |
130 | _hideConfirm() {
131 | this.confirmLabel.hide();
132 | this.hideYesAndNoButtons();
133 | this.closingLabel.hide();
134 | }
135 |
136 | _addYesAndNoButtons() {
137 | super.createYesAndNoButtons();
138 |
139 | this.yesButton.connect('clicked', () => {
140 | // TODO Do this when enable_close_by_rules is true?
141 | this._parent.close();
142 | if (Main.overview.visible) {
143 | Main.overview.toggle();
144 | }
145 |
146 | RestoreSession.restoreSessionObject.restoringApps.clear();
147 | this.closeSession.closeWindows();
148 | this._hideConfirm();
149 |
150 | // Set the actor the timeline is associated with to make sure Clutter.Timeline works normally.
151 | // Set the actor in new Clutter.Timeline don't work
152 | this._timeline.set_actor(this.closingLabel);
153 | this._timeline.connect('new-frame', (_timeline, _frame) => {
154 | this.closingLabel.show();
155 | });
156 | this._timeline.start();
157 | this._timeline.connect('completed', () => {
158 | this._timeline.stop();
159 | this.closingLabel.hide();
160 | });
161 |
162 | });
163 |
164 | this.noButton.connect('clicked', () => {
165 | this._hideConfirm();
166 | });
167 |
168 | this.actor.add_child(this.yesButton);
169 | this.actor.add_child(this.noButton);
170 |
171 | }
172 |
173 | _addClosingPrompt() {
174 | this.closingLabel = new St.Label({
175 | style_class: 'confirm-before-operate',
176 | text: 'Closing open windows ...',
177 | x_expand: false,
178 | x_align: Clutter.ActorAlign.CENTER,
179 | });
180 | this.actor.add_child(this.closingLabel);
181 | }
182 |
183 | _createButton(iconSymbolic) {
184 | const closeButton = super.createButton(iconSymbolic);
185 | this.actor.add_child(closeButton);
186 | closeButton.connect('clicked', this._onClicked.bind(this));
187 | }
188 |
189 | _onClicked(button, event) {
190 | // In case someone hide close button again when this.closingLabel is still showing
191 | this._timeline.stop();
192 | this.closingLabel.hide();
193 |
194 | this.confirmLabel.show();
195 | this.showYesAndNoButtons();
196 | }
197 |
198 | _addConfirm() {
199 | this.confirmLabel = new St.Label({
200 | style_class: 'confirm-before-operate',
201 | text: 'Confirm?',
202 | x_expand: false,
203 | x_align: Clutter.ActorAlign.START,
204 | });
205 | this.actor.add_child(this.confirmLabel);
206 | }
207 |
208 | destroy() {
209 | // TODO Nullify others created objects?
210 |
211 | // TODO Also disconnect new-frame and completed?
212 | if (this._timeline) {
213 | this._timeline.stop();
214 | this._timeline = null;
215 | }
216 |
217 | }
218 |
219 | });
220 |
221 |
222 | const PopupMenuButtonItemSave = GObject.registerClass(
223 | class PopupMenuButtonItemSave extends PopupMenuButtonItem {
224 |
225 | _init(iconSymbolic) {
226 | super._init();
227 | this.saveCurrentSessionEntry = null;
228 | this._createButton(iconSymbolic);
229 | this.addIconDescription('Save open windows');
230 | this._addEntry();
231 | // Hide this St.Entry, only shown when user click saveButton.
232 | this.saveCurrentSessionEntry.hide();
233 | this._addYesAndNoButtons();
234 |
235 | this._log = new Log.Log();
236 |
237 | this._saveSession = new SaveSession.SaveSession(true);
238 |
239 | this._timeline = this.createTimeLine();
240 |
241 | this.savingLabel = null;
242 |
243 | this._addSavingPrompt();
244 |
245 | // Respond to menu item's 'activate' signal so user don't need to click the icon whose size is too small to find to click
246 | this.connect('activate', this._onActivate.bind(this));
247 |
248 | }
249 |
250 | _addYesAndNoButtons() {
251 | super.createYesAndNoButtons();
252 |
253 | this.yesButton.connect('clicked', this._onClickedYes.bind(this));
254 | this.noButton.connect('clicked', () => {
255 | // clear entry
256 | this.saveCurrentSessionEntry.set_text('');
257 | this.saveCurrentSessionEntry.hide();
258 | super.hideYesAndNoButtons();
259 | });
260 |
261 | this.actor.add_child(this.yesButton);
262 | this.actor.add_child(this.noButton);
263 |
264 | }
265 |
266 | _onClickedYes(button, event) {
267 | this._gotoSaveSession();
268 | }
269 |
270 | _onActivate() {
271 | this._onClickedBeginSave();
272 | }
273 |
274 | _addSavingPrompt() {
275 | this.savingLabel = new St.Label({
276 | style_class: 'confirm-before-operate',
277 | x_expand: false,
278 | x_align: Clutter.ActorAlign.CENTER,
279 | });
280 | this.actor.add_child(this.savingLabel);
281 | }
282 |
283 | _createButton(iconSymbolic) {
284 | const saveButton = super.createButton(iconSymbolic);
285 | this.actor.add_child(saveButton);
286 | saveButton.connect('clicked', this._onClickedBeginSave.bind(this));
287 | }
288 |
289 | _onClickedBeginSave(button, event) {
290 | this._timeline.stop();
291 | this.savingLabel.hide();
292 |
293 | this.saveCurrentSessionEntry.show();
294 | this.saveCurrentSessionEntry.grab_key_focus();
295 | super.showYesAndNoButtons();
296 | }
297 |
298 | _addEntry() {
299 | this.saveCurrentSessionEntry = new St.Entry({
300 | name: 'saveCurrentSession',
301 | hint_text: "Type a session name, default is defaultSession",
302 | track_hover: true,
303 | can_focus: true
304 | });
305 | const clutterText = this.saveCurrentSessionEntry.clutter_text;
306 | clutterText.connect('activate', this._onTextActivate.bind(this));
307 | this.actor.add_child(this.saveCurrentSessionEntry);
308 |
309 | }
310 |
311 | _onTextActivate(entry, event) {
312 | this._gotoSaveSession();
313 | }
314 |
315 | _gotoSaveSession() {
316 | let sessionName = this.saveCurrentSessionEntry.get_text();
317 | if (sessionName) {
318 | // ' ' is truthy
319 | if (!sessionName.trim()) {
320 | sessionName = FileUtils.default_sessionName;
321 | }
322 | } else {
323 | sessionName = FileUtils.default_sessionName;
324 | }
325 |
326 | const [canSave, reason] = this._canSave(sessionName);
327 | if (!canSave) {
328 | this._displayMessage(reason);
329 | return;
330 | }
331 |
332 | // clear entry
333 | this.saveCurrentSessionEntry.set_text('');
334 |
335 | this.saveCurrentSessionEntry.hide();
336 | super.hideYesAndNoButtons();
337 |
338 | this.savingLabel.set_text(`Saving open windows as '${sessionName}' ...`);
339 | this.savingLabel.show();
340 |
341 | this._saveSession.saveSessionAsync(sessionName).then(() => {
342 | this.savingLabel.hide();
343 | }).catch(e => {
344 | let message = `Failed to save session`;
345 | this._log.error(e, e.desc ?? message);
346 | global.notify_error(message, e.cause?.message ?? e.desc ?? message);
347 | this._displayMessage(e.cause?.message ?? e.message);
348 | });
349 |
350 | }
351 |
352 | _displayMessage(message) {
353 | // To prevent saving session many times by holding and not releasing Enter
354 | this.saveCurrentSessionEntry.hide();
355 | this.savingLabel.set_text(message);
356 | this._timeline.set_actor(this.savingLabel);
357 | const newFrameId = this._timeline.connect('new-frame', (_timeline, _frame) => {
358 | this._timeline.disconnect(newFrameId);
359 | this.savingLabel.show();
360 | this.hideYesAndNoButtons();
361 | });
362 | this._timeline.start();
363 | const completedId = this._timeline.connect('completed', () => {
364 | this._timeline.disconnect(completedId);
365 | this._timeline.stop();
366 | this.savingLabel.hide();
367 | this.saveCurrentSessionEntry.show();
368 | this.showYesAndNoButtons();
369 | });
370 | }
371 |
372 | _canSave(sessionName) {
373 | if (sessionName === FileUtils.sessions_backup_folder_name) {
374 | return [false, `ERROR: ${sessionName} is a reserved word, can't be used.`];
375 | }
376 |
377 | if (FileUtils.isDirectory(sessionName)) {
378 | return [false, `ERROR: Can't save windows using '${sessionName}', it's an existing directory!`];
379 | }
380 |
381 | if (sessionName.indexOf('/') != -1) {
382 | return [false, `ERROR: Session names cannot contain '/'`];
383 | }
384 | return [true, ''];
385 | }
386 |
387 | destroy() {
388 | // TODO Nullify others created objects?
389 |
390 | // TODO Also disconnect new-frame and completed?
391 | if (this._timeline) {
392 | this._timeline.stop();
393 | this._timeline = null;
394 | }
395 |
396 | }
397 |
398 |
399 | });
--------------------------------------------------------------------------------
/ui/searchSessionItem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import St from 'gi://St';
5 | import Clutter from 'gi://Clutter';
6 |
7 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
8 |
9 | import * as Tooltip from '../utils/tooltip.js';
10 |
11 |
12 | export const SearchSessionItem = GObject.registerClass(
13 | class SearchSessionItem extends PopupMenu.PopupBaseMenuItem {
14 |
15 | _init() {
16 | super._init({
17 | activate: false,
18 | reactive: true,
19 | hover: false,
20 | can_focus: false
21 | });
22 |
23 | this._entry = new St.Entry({
24 | name: 'searchEntry',
25 | style_class: 'search-entry',
26 | can_focus: true,
27 | hint_text: _('Type to search'),
28 | track_hover: true,
29 | x_expand: false,
30 | y_expand: true
31 | });
32 |
33 | this._entry.set_primary_icon(new St.Icon({
34 | style_class: 'search-entry-icon',
35 | icon_name: 'edit-find-symbolic'
36 | }));
37 |
38 | this.add_child(this._entry);
39 |
40 | this._clearIcon = new St.Icon({
41 | style_class: 'search-entry-icon',
42 | icon_name: 'edit-clear-symbolic'
43 | });
44 |
45 | this._entry.set_secondary_icon(this._clearIcon);
46 | this._secondaryIconClickedId = this._entry.connect('secondary-icon-clicked', this.reset.bind(this));
47 |
48 | this._addFilters();
49 | }
50 |
51 | _addFilters() {
52 | const filterLabel = new St.Label({
53 | text: 'Filter: ',
54 | x_align: Clutter.ActorAlign.CENTER,
55 | y_align: Clutter.ActorAlign.CENTER,
56 | });
57 | this.add_child(filterLabel);
58 | this._filterAutoRestore();
59 |
60 | }
61 |
62 | _filterAutoRestore() {
63 | this._filterAutoRestoreSwitch = new PopupMenu.Switch(false);
64 | this._filterAutoRestoreSwitch.set_style_class_name('toggle-switch awsm-toggle-switch');
65 | let button = new St.Button({
66 | style_class: 'dnd-button',
67 | can_focus: true,
68 | x_align: Clutter.ActorAlign.END,
69 | toggle_mode: true,
70 | child: this._filterAutoRestoreSwitch,
71 | });
72 | this._filterAutoRestoreSwitch.bind_property('state',
73 | button, 'checked',
74 | GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE);
75 |
76 | new Tooltip.Tooltip({
77 | parent: button,
78 | markup: 'Show only auto-restore item(s)',
79 | });
80 |
81 | this.add_child(button);
82 | }
83 |
84 | reset() {
85 | this._entry.grab_key_focus();
86 | this._entry.set_text('');
87 | let text = this._entry.get_clutter_text();
88 | text.set_cursor_visible(true);
89 | }
90 |
91 | destroy() {
92 | if (this._secondaryIconClickedId) {
93 | this._entry.disconnect(this._secondaryIconClickedId);
94 | this._secondaryIconClickedId = null;
95 | }
96 | }
97 | });
--------------------------------------------------------------------------------
/ui/sessionItem.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 |
5 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
6 |
7 | import * as SessionItemButtons from '../ui/sessionItemButtons.js';
8 |
9 |
10 | export const SessionItem = GObject.registerClass(
11 | class SessionItem extends PopupMenu.PopupMenuItem {
12 |
13 | _init(fileInfo, file, indicator) {
14 | // Initialize this component, so we can use this.label etc
15 | super._init("");
16 |
17 | this._indicator = indicator;
18 |
19 | this._available = true;
20 |
21 | this._filepath = file.get_path();
22 | if(fileInfo != null) {
23 | this._filename = fileInfo.get_name();
24 | const modification_date_time = fileInfo.get_modification_date_time();
25 | if (modification_date_time) {
26 | this._modification_time = modification_date_time.to_local().format('%Y-%m-%d %T');
27 | } else {
28 | this._modification_time = '( Unknown )';
29 | this._available = false;
30 | }
31 | } else {
32 | this._filename = file.get_basename();
33 | this._modification_time = '( Please save this session before using it )';
34 |
35 | this._available = false;
36 | }
37 |
38 | this.label.set_x_expand(true);
39 | this.label.clutter_text.set_text(this._filename);
40 |
41 | this._sessionItemButtons = new SessionItemButtons.SessionItemButtons(this);
42 | this._sessionItemButtons.addButtons();
43 |
44 | }
45 |
46 |
47 |
48 | });
49 |
50 | const EmptySessionItem = GObject.registerClass(
51 | class EmptySessionItem extends PopupMenu.PopupMenuItem {
52 |
53 | _init() {
54 | super._init("(Empty, please save open windows first)");
55 | this.setSensitive(false);
56 | }
57 |
58 | });
59 |
60 |
--------------------------------------------------------------------------------
/ui/sessionItemButtons.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import GObject from 'gi://GObject';
4 | import St from 'gi://St';
5 | import GLib from 'gi://GLib';
6 | import Clutter from 'gi://Clutter';
7 |
8 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
9 | import * as PopupMenu from 'resource:///org/gnome/shell/ui/popupMenu.js';
10 |
11 | import * as IconFinder from '../utils/iconFinder.js';
12 | import * as FileUtils from '../utils/fileUtils.js';
13 | import * as DateUtils from '../utils/dateUtils.js';
14 | import * as Tooltip from '../utils/tooltip.js';
15 | import * as Log from '../utils/log.js';
16 | import {PrefsUtils} from '../utils/prefsUtils.js';
17 |
18 | import * as SaveSession from '../saveSession.js';
19 | import * as RestoreSession from '../restoreSession.js';
20 | import * as MoveSession from '../moveSession.js';
21 | import * as CloseSession from '../closeSession.js';
22 | import * as Constants from '../constants.js';
23 |
24 | import {Button} from './button.js';
25 |
26 | import * as Autoclose from './autoclose.js';
27 |
28 |
29 | export const SessionItemButtons = GObject.registerClass(
30 | class SessionItemButtons extends GObject.Object {
31 |
32 | _init(sessionItem) {
33 | super._init();
34 |
35 | this._log = new Log.Log();
36 |
37 | this.sessionItem = sessionItem;
38 |
39 | // TODO Nullify created object?
40 | this._saveSession = new SaveSession.SaveSession(true);
41 | this._moveSession = new MoveSession.MoveSession();
42 | this._closeSession = new CloseSession.CloseSession(CloseSession.flags.closeWindows);
43 |
44 | this._settings = PrefsUtils.getSettings();
45 | }
46 |
47 | addButtons() {
48 | this._addTags();
49 |
50 | const saveButton = this._addButton('save-symbolic.svg');
51 | new Tooltip.Tooltip({
52 | parent: saveButton,
53 | markup: 'Save open windows using the current session name',
54 | });
55 | saveButton.connect('clicked', this._onClickSave.bind(this));
56 |
57 | const restoreButton = this._addButton('restore-symbolic.svg');
58 | restoreButton.set_reactive(this.sessionItem._available);
59 | new Tooltip.Tooltip({
60 | parent: restoreButton,
61 | markup: 'Restore windows from the saved session',
62 | });
63 | restoreButton.connect('clicked', this._onClickRestore.bind(this));
64 |
65 | const moveButton = this._addButton('move-symbolic.svg');
66 | moveButton.set_reactive(this.sessionItem._available);
67 | new Tooltip.Tooltip({
68 | parent: moveButton,
69 | markup: 'Move windows to their workspace by the saved session',
70 | });
71 | moveButton.connect('clicked', this._onClickMove.bind(this));
72 |
73 | // this._addSeparator();
74 |
75 | // const closeButton = this._addButton('close-symbolic.svg');
76 | // closeButton.connect('clicked', this._onClickClose.bind(this));
77 |
78 | const autoRestoreSwitcher = this._addAutostartSwitcher();
79 | new Tooltip.Tooltip({
80 | parent: autoRestoreSwitcher,
81 | markup: 'Restore at startup',
82 | });
83 | autoRestoreSwitcher.connect('clicked', (button, event) => {
84 | const state = this._autostartSwitch.state;
85 | if (state) {
86 | this._settings.set_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS, this.sessionItem._filename);
87 | } else {
88 | this._settings.set_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS, '');
89 | }
90 | });
91 |
92 | this._settings.connect(`changed::${Constants.PREFS_SETTING_AUTORESTORE_SESSIONS}`, (settings) => {
93 | const toggled = this.sessionItem._filename == this._settings.get_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS);
94 | this._autostartSwitch.state = toggled;
95 | });
96 |
97 | this._addSeparator();
98 |
99 | const viewButton = this._addViewButton();
100 | new Tooltip.Tooltip({
101 | parent: viewButton,
102 | markup: 'Open session file using an external editor',
103 | });
104 | viewButton.connect('clicked', () => {
105 | const sessions_path = FileUtils.get_sessions_path();
106 | const session_file_path = GLib.build_filenamev([sessions_path, this.sessionItem._filename]);
107 | FileUtils.findDefaultApp(session_file_path).then(([app, file]) => {
108 | try {
109 | app.launch([file], global.create_app_launch_context(DateUtils.get_current_time(), -1));
110 | } catch (error) {
111 | this._log.error(error, `Failed to open ${session_file_path} using ${app.get_filename()}`);
112 | }
113 | }).catch(error => {
114 | this._log.error(error, `Failed to find the default application to ${session_file_path}`);
115 | });
116 | });
117 |
118 | const deleteButton = this._addDeleteButton();
119 | new Tooltip.Tooltip({
120 | parent: deleteButton,
121 | markup: 'Move to Trash',
122 | });
123 | deleteButton.connect('clicked', () => {
124 | // We just trash file to trash scan instead of delete in case still need it.
125 | FileUtils.trashSession(this.sessionItem._filename);
126 | });
127 |
128 | }
129 |
130 | _addAutostartSwitcher() {
131 |
132 | const toggled = this.sessionItem._filename == this._settings.get_string(Constants.PREFS_SETTING_AUTORESTORE_SESSIONS);
133 | this._autostartSwitch = new PopupMenu.Switch(toggled);
134 | this._autostartSwitch.set_style_class_name('toggle-switch awsm-toggle-switch');
135 | let button = new St.Button({
136 | style_class: 'dnd-button',
137 | can_focus: true,
138 | x_align: Clutter.ActorAlign.END,
139 | toggle_mode: true,
140 | child: this._autostartSwitch,
141 | reactive: this.sessionItem._available
142 | });
143 | this._autostartSwitch.bind_property('state',
144 | button, 'checked',
145 | GObject.BindingFlags.BIDIRECTIONAL | GObject.BindingFlags.SYNC_CREATE);
146 | this.sessionItem.actor.add_child(button);
147 | return button;
148 | }
149 |
150 | _addViewButton() {
151 | const [exists, sessionFilePath] = FileUtils.sessionExists(this.sessionItem._filename);
152 | return this._addTextButton('View', exists);
153 | }
154 |
155 | _addDeleteButton() {
156 | const reactive = this.sessionItem._filename != FileUtils.recently_closed_session_name;
157 | return this._addTextButton('Delete', reactive);
158 | }
159 |
160 | _addTextButton(label, reactive) {
161 | let button = new St.Button({
162 | style_class: 'button',
163 | can_focus: true,
164 | x_align: Clutter.ActorAlign.END,
165 | x_expand: false,
166 | y_expand: true,
167 | track_hover: true,
168 | reactive: reactive,
169 | });
170 | button.set_label(label);
171 | this.sessionItem.actor.add_child(button);
172 | return button;
173 | }
174 |
175 | _addTags() {
176 | if (!Log.Log.getDefault().isDebug()) return;
177 |
178 | // TODO Make the modification time align left
179 |
180 | let button = new St.Button({
181 | x_align: Clutter.ActorAlign.END,
182 | });
183 |
184 | button.set_label(this.sessionItem._modification_time);
185 | if (!this.sessionItem._available) {
186 | button.set_style('color: red;');
187 | }
188 | this.sessionItem.actor.add_child(button);
189 |
190 | this._addSeparator();
191 | }
192 |
193 | _addSeparator() {
194 | let icon = new St.Icon({
195 | gicon: IconFinder.find('separator-symbolic.svg'),
196 | style_class: 'system-status-icon'
197 | });
198 |
199 | let button = new St.Button({
200 | style_class: 'aws-item-separator',
201 | can_focus: false,
202 | child: icon,
203 | x_align: Clutter.ActorAlign.END,
204 | x_expand: false,
205 | y_expand: false,
206 | track_hover: false
207 | });
208 |
209 | this.sessionItem.actor.add_child(button);
210 | }
211 |
212 | _addButton(iconSymbolic) {
213 | const button = new Button({
214 | icon_symbolic: iconSymbolic,
215 | }).button;
216 | this.sessionItem.actor.add_child(button);
217 | return button;
218 | }
219 |
220 | _onClickSave(button, event) {
221 | this._saveSession.saveSessionAsync(this.sessionItem._filename).catch(e => {
222 | let message = `Failed to save session`;
223 | this._log.error(e, e.desc ?? message);
224 | global.notify_error(message, e.cause?.message ?? e.desc ?? message);
225 | });
226 | }
227 |
228 | _onClickRestore(button, event) {
229 | Autoclose.autocloseObject.sessionClosedByUser = false;
230 | RestoreSession.restoreSessionObject.restoringApps = new Map();
231 | // Using _restoredApps to hold restored apps so we create new instance every time for now
232 | const _restoreSession = new RestoreSession.RestoreSession();
233 | _restoreSession.restoreSession(this.sessionItem._filename);
234 | }
235 |
236 | _onClickMove(button, event) {
237 | this._moveSession.moveWindows(this.sessionItem._filename);
238 | }
239 |
240 | _onClickClose(button, event) {
241 | // TODO Close specified windows in the session?
242 | this._closeSession.closeWindows();
243 | }
244 | });
--------------------------------------------------------------------------------
/ui/uiHelper.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Meta from 'gi://Meta';
4 |
5 |
6 | export function isDialog(metaWindow) {
7 | const dialogTypes = [
8 | // 3
9 | Meta.WindowType.DIALOG,
10 | // 4
11 | Meta.WindowType.MODAL_DIALOG,
12 | ];
13 | const winType = metaWindow.get_window_type();
14 | return dialogTypes.includes(winType) &&
15 | metaWindow.get_transient_for() != null;
16 | }
17 |
18 | export function ignoreWindows(metaWindow) {
19 | if (isDialog(metaWindow)) {
20 | return true;
21 | }
22 |
23 | // The override-redirect windows is invisible to the users,
24 | // and the workspace index is -1 and don't have proper x, y, width, height.
25 | // See also:
26 | // https://gjs-docs.gnome.org/meta9~9_api/meta.window#method-is_override_redirect
27 | // https://wiki.tcl-lang.org/page/wm+overrideredirect
28 | // https://docs.oracle.com/cd/E36784_01/html/E36843/windowapi-3.html
29 | // https://stackoverflow.com/questions/38162932/what-does-overrideredirect-do
30 | // https://ml.cddddr.org/cl-windows/msg00166.html
31 | if (metaWindow.is_override_redirect()) {
32 | return true;
33 | }
34 |
35 | return false;
36 | }
--------------------------------------------------------------------------------
/utils/CommonError.js:
--------------------------------------------------------------------------------
1 |
2 | /**
3 | *
4 | * Create a customized Error with error description
5 | *
6 | * Usage:
7 | * ```js
8 | * const myError = new BaseError('A message', {
9 | * cause: new Error('Caused by another error'),
10 | * desc: "A description"
11 | * });
12 | * ```
13 | *
14 | * @param message
15 | * @param options
16 | */
17 | export const CommonError = class extends Error{
18 |
19 | constructor(message, options = {}) {
20 | if (!options.cause) {
21 | delete options.cause;
22 | }
23 | super(message, options);
24 | this.desc = options.desc;
25 | }
26 |
27 | }
--------------------------------------------------------------------------------
/utils/WindowPicker.js:
--------------------------------------------------------------------------------
1 | // SPDX-FileCopyrightText: nlpsuge
2 | // SPDX-FileCopyrightText: Simon Schneegans
3 | // SPDX-FileCopyrightText: Aurélien Hamy
4 | // SPDX-License-Identifier: GPL-3.0-or-later
5 |
6 | 'use strict';
7 |
8 | import Clutter from 'gi://Clutter';
9 | import GObject from 'gi://GObject';
10 | import Gio from 'gi://Gio';
11 | import GLib from 'gi://GLib';
12 | import Shell from 'gi://Shell';
13 |
14 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
15 | import * as LookingGlass from 'resource:///org/gnome/shell/ui/lookingGlass.js';
16 |
17 | import * as FileUtils from './fileUtils.js';
18 |
19 | // Based on the WindowPicker.js from Burn-My-Windows.
20 | // I modified and enhanced it, so it can be used in my case
21 | // properly for the Another Window Session Manager extension.
22 |
23 |
24 | //////////////////////////////////////////////////////////////////////////////////////////
25 | // This is based on the window-picking functionality of the Blur-My-Shell extension. //
26 | // The PickWindow() method is exposed via the D-Bus and can be called by the //
27 | // preferences dialog of the Burn-My-Windows extensions in order to initiate the window //
28 | // picking. //
29 | //////////////////////////////////////////////////////////////////////////////////////////
30 |
31 | export const WindowPickerServiceProvider = class WindowPickerServiceProvider {
32 | // ------------------------------------------------------------------------- constructor
33 |
34 | constructor() {
35 | const iFace = new TextDecoder().decode(
36 | FileUtils.current_extension_dir.get_child('dbus-interfaces').get_child('org.gnome.Shell.Extensions.awsm.PickWindow.xml').load_contents(null)[1]);
37 | this._dbus = Gio.DBusExportedObject.wrapJSObject(iFace, this);
38 | }
39 |
40 | // --------------------------------------------------------------------- D-Bus interface
41 |
42 | // This method is exposed via the D-Bus. It is called by the preferences dialog of the
43 | // Burn-My-Windows extensions in order to initiate the window picking.
44 | PickWindow() {
45 |
46 | // We use the actor picking from LookingGlass. This seems a bit hacky and also allows
47 | // selecting things of the Shell which are not windows, but it does the trick :)
48 | const lookingGlass = Main.createLookingGlass();
49 | lookingGlass.open();
50 | lookingGlass.hide();
51 |
52 | const inspector = new MyInspector(Main.createLookingGlass());
53 |
54 | Main.popModal(lookingGlass._grab);
55 |
56 | inspector.connect('target', (me, target, x, y) => {
57 | // Remove border effect when window is picked.
58 | target.get_effects()
59 | .filter(e => e.toString().includes('lookingGlass_RedBorderEffect'))
60 | .forEach(e => target.remove_effect(e));
61 |
62 | // While we may switch windows to pick a window, the target actor also changes.
63 | // Here we check the current actor again, make sure it's what we except.
64 | let currentActor = global.stage.get_actor_at_pos(Clutter.PickMode.ALL, x, y);
65 | if (currentActor != target) {
66 | log('Picked window changed to ' + currentActor);
67 | target = currentActor;
68 | }
69 |
70 | let actor = target;
71 | if (target.toString().includes('MetaSurfaceActor')) {
72 | actor = target.get_parent();
73 | }
74 |
75 | let variant;
76 | if (actor.toString().includes('WindowActor')) {
77 | const metaWindow = actor.meta_window;
78 | const app = Shell.WindowTracker.get_default().get_window_app(metaWindow);
79 | const appName = app ? app.get_name() : '';
80 | const wmClass = metaWindow.get_wm_class();
81 | const wmClassInstance = metaWindow.get_wm_class_instance();
82 | const title = metaWindow.get_title();
83 | const result = [
84 | appName,
85 | wmClass ? wmClass : '',
86 | wmClassInstance ? wmClassInstance : '',
87 | title ? title : '',
88 | ];
89 | variant = new GLib.Variant('(ssss)', result)
90 | } else {
91 | variant = new GLib.Variant('()', []);
92 | }
93 |
94 | this._dbus.emit_signal('WindowPicked', variant);
95 | });
96 |
97 | // Close LookingGlass and release the grab when the picking is finished.
98 | inspector.connect('closed', () => {
99 | // Restore the global grab to prevent the error 'incorrect pop' thrown by LookingGlass.close/Main.popModal(this._grab)
100 | lookingGlass._grab = Main.pushModal(lookingGlass, { actionMode: Shell.ActionMode.LOOKING_GLASS });
101 | lookingGlass.close();
102 | });
103 |
104 | inspector.connect('WindowPickCancelled', () => {
105 | this._dbus.emit_signal('WindowPickCancelled', null);
106 | });
107 | }
108 |
109 | // -------------------------------------------------------------------- public interface
110 |
111 | // Call this to make the window-picking API available on the D-Bus.
112 | enable() {
113 | this._dbus.export(Gio.DBus.session, '/org/gnome/shell/extensions/awsm');
114 | }
115 |
116 | // Call this to stop this D-Bus again.
117 | destroy() {
118 | this._dbus.unexport();
119 | }
120 | };
121 |
122 | const MyInspector = GObject.registerClass({
123 | Signals: {
124 | 'WindowPickCancelled': {}
125 | }
126 | }, class MyInspector extends LookingGlass.Inspector {
127 | _init(lookingGlass) {
128 | super._init(lookingGlass);
129 | }
130 |
131 | _onKeyPressEvent(actor, event) {
132 | if (event.get_key_symbol() === Clutter.KEY_Escape) {
133 | this.emit('WindowPickCancelled');
134 | this._close();
135 | }
136 | return Clutter.EVENT_STOP;
137 | }
138 | });
--------------------------------------------------------------------------------
/utils/dateUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * Get the current timestamp through `global.get_current_time()`, but it might return 0,
5 | * which is not a valid value for some function such as `metaWindow.delete(timestamp)`,
6 | * will get a warning if pass 0 to it.
7 | *
8 | * The doc states "If called from outside an event handler, this may return
9 | * %Clutter.CURRENT_TIME (aka 0), or it may return a slightly out-of-date timestamp."
10 | *
11 | * If so we use `global.display.get_current_time_roundtrip()` to get a valid timestamp.
12 | *
13 | * On Wayland, `global.display.get_current_time_roundtrip()` also uses
14 | * `Number.parseInt(GLib.get_monotonic_time() / 1000)` to get the current timestamp.
15 | *
16 | * @returns guint32 type timestamp, for example 75176468
17 | */
18 | export const get_current_time = function() {
19 | return global.get_current_time() || global.display.get_current_time_roundtrip();
20 | }
--------------------------------------------------------------------------------
/utils/fileUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Gio from 'gi://Gio';
4 | import GLib from 'gi://GLib';
5 |
6 | import * as Log from './log.js';
7 |
8 |
9 | export let current_extension_path = null;
10 | export let current_extension_dir = null;
11 |
12 | export const default_sessionName = 'defaultSession';
13 | export const data_dir = GLib.get_user_data_dir();
14 | export const user_config = GLib.get_user_config_dir();
15 | // This extension can restore `xsm`'s session file,
16 | // but desktop_file_id is missing in that file, so can't move them. Will be fixed in the future.
17 | export const config_path_base = GLib.build_filenamev([user_config, 'another-window-session-manager']);
18 | // The session list
19 | export const sessions_path = GLib.build_filenamev([config_path_base, 'sessions']);
20 | export const sessions_backup_folder_name = 'backups';
21 | const sessions_backup_path = GLib.build_filenamev([sessions_path, sessions_backup_folder_name]);
22 |
23 | export let desktop_template_path = null;
24 | export let desktop_template_path_restore_at_autostart = null;
25 | export let desktop_template_path_restore_previous_at_autostart = null;
26 | export let desktop_template_launch_app_shell_script = null;
27 |
28 | export const desktop_file_store_path_base = GLib.build_filenamev([data_dir, '/applications']);
29 | export const desktop_file_store_path = `${desktop_file_store_path_base}/__another-window-session-manager`;
30 |
31 | export const recently_closed_session_name = 'Recently Closed Session';
32 | export const recently_closed_session_path = GLib.build_filenamev([sessions_path, recently_closed_session_name]);
33 | export const recently_closed_session_file = Gio.File.new_for_path(recently_closed_session_path);
34 |
35 | export const current_session_path = `${config_path_base}/currentSession`;
36 |
37 | export const current_session_summary_name = 'summary.json';
38 | export const current_session_summary_path = GLib.build_filenamev([current_session_path, 'summary.json']);
39 |
40 | export const autostart_restore_desktop_file_path = GLib.build_filenamev([user_config, '/autostart/_gnome-shell-extension-another-window-session-manager.desktop']);
41 | export const autostart_restore_previous_desktop_file_path = GLib.build_filenamev([user_config, '/autostart/_awsm-restore-previous-session.desktop']);
42 |
43 | export let desktop_template_path_ydotool_uinput_rules;
44 | export const system_udev_rules_path_ydotool_uinput_rules = '/etc/udev/rules.d/60-awsm-ydotool-uinput.rules';
45 |
46 | // Some constants rely on extension metadata,
47 | // we put them all here and initialize them from extension.js
48 | export function init(extensionObject) {
49 | current_extension_dir = extensionObject.dir;
50 | current_extension_path = extensionObject.path;
51 | desktop_template_path = GLib.build_filenamev([extensionObject.path, '/template/template.desktop']);
52 | desktop_template_path_restore_at_autostart = GLib.build_filenamev([extensionObject.path, '/template/_gnome-shell-extension-another-window-session-manager.desktop']);
53 | desktop_template_path_restore_previous_at_autostart = GLib.build_filenamev([extensionObject.path, '/template/_awsm-restore-previous-session.desktop']);
54 | desktop_template_launch_app_shell_script = GLib.build_filenamev([extensionObject.path, '/template/launch-app.sh']);
55 | desktop_template_path_ydotool_uinput_rules = GLib.build_filenamev([extensionObject.path, '/template/60-awsm-ydotool-uinput.rules']);
56 |
57 | }
58 |
59 | export async function loadSummary() {
60 | try {
61 | return await loadFile(current_session_summary_path);
62 | } catch (error) {
63 | Log.Log.getDefault().error(error);
64 | }
65 | }
66 |
67 | export async function loadFile(path) {
68 | try {
69 | return new Promise((resolve, reject) => {
70 | const file = Gio.File.new_for_path(path);
71 | file.load_contents_async(
72 | null,
73 | (file, asyncResult) => {
74 | try {
75 | const [success, contents, _] = file.load_contents_finish(asyncResult);
76 | resolve([getJsonObj(contents), path]);
77 | } catch (error) {
78 | Log.Log.getDefault().error(error);
79 | reject(error);
80 | }
81 | });
82 | });
83 | } catch (error) {
84 | Log.Log.getDefault().error(error);
85 | }
86 | }
87 |
88 | /**
89 | * Get the absolute session path which contains sessions,
90 | * it's `~/.config/another-window-session-manager` by default.
91 | *
92 | * @param {string} baseDir base directory, `~/.config/another-window-session-manager/sessions` by default
93 | * @returns {string} the absolute session path which contains sessions
94 | */
95 | export function get_sessions_path(baseDir = null) {
96 | if (baseDir) {
97 | return baseDir;
98 | } else {
99 | return sessions_path;
100 | }
101 | }
102 |
103 | export function get_sessions_backups_path() {
104 | return sessions_backup_path;
105 | }
106 |
107 | export function getJsonObj(contents) {
108 | let session_config;
109 | // Fix Gnome 3 crash due to: Some code called array.toString() on a Uint8Array instance. Previously this would have interpreted the bytes of the array as a string, but that is nonstandard. In the future this will return the bytes as comma-separated digits. For the time being, the old behavior has been preserved, but please fix your code anyway to explicitly call new TextDecoder().decode(array).
110 | if (contents instanceof Uint8Array) {
111 | const contentsConverted = new TextDecoder().decode(contents);
112 | session_config = JSON.parse(contentsConverted);
113 | } else {
114 | // Unreachable code
115 | session_config = JSON.parse(contents);
116 | }
117 | return session_config;
118 | }
119 |
120 | export async function listAllSessions(sessionPath, recursion, callback) {
121 | try {
122 | if (!sessionPath) {
123 | sessionPath = get_sessions_path();
124 | }
125 | if (!GLib.file_test(sessionPath, GLib.FileTest.EXISTS)) {
126 | Log.Log.getDefault().warn(`${sessionPath} not exist`);
127 | return;
128 | }
129 |
130 | Log.Log.getDefault().debug(`Scanning ${sessionPath}`);
131 |
132 | const sessionPathFile = Gio.File.new_for_path(sessionPath);
133 | let fileEnumerator = await new Promise((resolve, reject) => {
134 | sessionPathFile.enumerate_children_async(
135 | [Gio.FILE_ATTRIBUTE_STANDARD_NAME,
136 | Gio.FILE_ATTRIBUTE_STANDARD_TYPE,
137 | Gio.FILE_ATTRIBUTE_TIME_MODIFIED,
138 | Gio.FILE_ATTRIBUTE_STANDARD_CONTENT_TYPE].join(','),
139 | Gio.FileQueryInfoFlags.NONE,
140 | GLib.PRIORITY_DEFAULT,
141 | null,
142 | (file, asyncResult) => {
143 | try {
144 | resolve(file.enumerate_children_finish(asyncResult));
145 | } catch (e) {
146 | Log.Log.getDefault().error(e, `Failed to list directory ${sessionPath}`);
147 | reject(e);
148 | }
149 | });
150 | });
151 |
152 | const nextFilesFunc = async () => {
153 | return new Promise((resolve, reject) => {
154 | fileEnumerator.next_files_async(
155 | // num_files. Just set a random value, because I don't know which value is better yet
156 | 10,
157 | GLib.PRIORITY_DEFAULT,
158 | null,
159 | (iter, asyncResult) => {
160 | try {
161 | resolve(iter.next_files_finish(asyncResult));
162 | } catch (e) {
163 | reject(e);
164 | }
165 | }
166 | );
167 | });
168 | };
169 |
170 | let infos = await nextFilesFunc();
171 | while (infos && infos.length) {
172 | for (const info of infos) {
173 | const file = fileEnumerator.get_child(info);
174 | if (recursion && info.get_file_type() === Gio.FileType.DIRECTORY) {
175 | await listAllSessions(file.get_path(), recursion, callback);
176 | }
177 |
178 | if (callback) {
179 | callback(file, info);
180 | }
181 | }
182 |
183 | infos = await nextFilesFunc();
184 | }
185 | } catch (e) {
186 | Log.Log.getDefault().error(e);
187 | }
188 | }
189 |
190 | export function sessionExists(sessionName, baseDir = null) {
191 | const sessionsPath = get_sessions_path(baseDir);
192 | const sessionFilePath = GLib.build_filenamev([sessionsPath, sessionName]);
193 | if (GLib.file_test(sessionFilePath, GLib.FileTest.EXISTS)) {
194 | return [true, sessionFilePath];
195 | }
196 | return [false];
197 | }
198 |
199 | /**
200 | * Remove files. And also remove its parent if it's empty.
201 | *
202 | * @param {String} path The path of a file or a directory
203 | */
204 | export function removeFileAndParent(path) {
205 | if (!GLib.file_test(path, GLib.FileTest.EXISTS)) {
206 | throw new Error(`Cannot remove '${path}': No such file or directory`);
207 | }
208 |
209 | const file = Gio.File.new_for_path(path);
210 | try {
211 | const info = file.query_info(
212 | [Gio.FILE_ATTRIBUTE_STANDARD_TYPE].join(','),
213 | Gio.FileQueryInfoFlags.NONE,
214 | null);
215 |
216 | const fileType = info.get_file_type();
217 | const isDir = fileType === Gio.FileType.DIRECTORY;
218 |
219 | file.delete(null);
220 | Log.Log.getDefault().debug(`Removed ${isDir ? 'directory' : ''} ${path}`);
221 |
222 | const parent = file.get_parent();
223 | if (parent && isEmpty(parent)) {
224 | parent.delete(null);
225 | Log.Log.getDefault().debug(`Removed directory ${parent.get_path()}`);
226 | }
227 |
228 | } catch (e) {
229 | Log.Log.getDefault().error(e);
230 | }
231 | }
232 |
233 | export function isEmpty(directory) {
234 | const fileEnumerator = directory.enumerate_children(
235 | [Gio.FILE_ATTRIBUTE_STANDARD_NAME,
236 | Gio.FILE_ATTRIBUTE_STANDARD_TYPE].join(','),
237 | Gio.FileQueryInfoFlags.NONE,
238 | null);
239 | return !fileEnumerator.next_file(null);
240 | }
241 |
242 | /**
243 | * Remove files or directories
244 | *
245 | * @param {String} path The path of a file or a directory
246 | * @param {Boolean} recursively true if remove all files or directories in `path`
247 | */
248 | export function removeFile(path, recursively = false) {
249 | if (!GLib.file_test(path, GLib.FileTest.EXISTS)) {
250 | throw new Error(`Cannot remove '${path}': No such file or directory`);
251 | }
252 |
253 | const file = Gio.File.new_for_path(path);
254 | try {
255 | const info = file.query_info(
256 | [Gio.FILE_ATTRIBUTE_STANDARD_TYPE].join(','),
257 | Gio.FileQueryInfoFlags.NONE,
258 | null);
259 |
260 | const fileType = info.get_file_type();
261 | if (fileType === Gio.FileType.DIRECTORY) {
262 | if (!recursively) {
263 | throw new Error(`Cannot remove '${path}': Is a directory`);
264 | }
265 | const fileEnumerator = file.enumerate_children(
266 | [Gio.FILE_ATTRIBUTE_STANDARD_NAME,
267 | Gio.FILE_ATTRIBUTE_STANDARD_TYPE].join(','),
268 | Gio.FileQueryInfoFlags.NONE,
269 | null);
270 |
271 | let fileInfo = null;
272 | while (fileInfo = fileEnumerator.next_file(null)) {
273 | const childFile = fileEnumerator.get_child(fileInfo);
274 | if (info.get_file_type() === Gio.FileType.DIRECTORY) {
275 | removeFile(childFile.get_path(), recursively);
276 | }
277 | }
278 |
279 | file.delete(null);
280 | Log.Log.getDefault().debug(`Removed directory ${path}`);
281 | } else {
282 | file.delete(null);
283 | Log.Log.getDefault().debug(`Removed ${path}`);
284 | }
285 | } catch (e) {
286 | Log.Log.getDefault().error(e);
287 | }
288 | }
289 |
290 | export function trashSession(sessionName) {
291 | const [exists, sessionFilePath] = sessionExists(sessionName);
292 | if (!exists) {
293 | return true;
294 | }
295 |
296 | let trashed = false;
297 | try {
298 | const sessionPathFile = Gio.File.new_for_path(sessionFilePath);
299 | trashed = sessionPathFile.trash(null);
300 | if (!trashed) {
301 | Log.Log.getDefault().error(new Error(`Failed to trash file ${sessionFilePath}. Reason: Unknown.`));
302 | }
303 | return trashed;
304 | } catch (e) {
305 | Log.Log.getDefault().error(e, `Failed to trash file ${sessionFilePath}`);
306 | return false;
307 | }
308 | }
309 |
310 | export function isDirectory(sessionName) {
311 | const sessionFilePath = GLib.build_filenamev([sessions_path, sessionName]);
312 | if (GLib.file_test(sessionFilePath, GLib.FileTest.IS_DIR)) {
313 | return true;
314 | }
315 |
316 | return false;
317 | }
318 |
319 | export function loadAutostartDesktopTemplate() {
320 | return loadTemplate(desktop_template_path_restore_at_autostart);
321 | }
322 |
323 | export function loadDesktopTemplate(cancellable = null) {
324 | return loadTemplate(desktop_template_path, cancellable);
325 | }
326 |
327 | export function loadTemplate(path, cancellable = null) {
328 | const desktop_template_file = Gio.File.new_for_path(path);
329 | let [success, contents] = desktop_template_file.load_contents(cancellable);
330 | if (success) {
331 | if (contents instanceof Uint8Array) {
332 | return new TextDecoder().decode(contents);
333 | } else {
334 | // Unreachable code
335 | return contents;
336 | }
337 | }
338 |
339 | return '';
340 | }
341 |
342 | /**
343 | * Find the default app to open session file
344 | *
345 | * @param {string} filePath
346 | */
347 | export function findDefaultApp(filePath) {
348 | const session_file = Gio.File.new_for_path(filePath);
349 | return new Promise((resolve, reject) => {
350 | session_file.query_default_handler_async(
351 | GLib.PRIORITY_DEFAULT,
352 | null,
353 | (file, asyncResult) => {
354 | try {
355 | const app = session_file.query_default_handler_finish(asyncResult);
356 | if (app) {
357 | resolve([app, session_file]);
358 | } else {
359 | reject(new Error(`Cannot find the default application to ${filePath}`));
360 | }
361 | } catch (error) {
362 | reject(error);
363 | }
364 | });
365 | });
366 | }
367 |
--------------------------------------------------------------------------------
/utils/function.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 |
4 | export const callFunc = function (thisObj, func, param) {
5 | try {
6 | if (!(param instanceof Array)) {
7 | if (param) {
8 | return func.call(thisObj, param);
9 | }
10 | return func.call(thisObj);
11 | }
12 | return func.call(thisObj, ...param);
13 | } catch (error) {
14 | logError(error);
15 | }
16 | }
17 |
--------------------------------------------------------------------------------
/utils/iconFinder.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Gio from 'gi://Gio';
4 | import GLib from 'gi://GLib';
5 |
6 | import * as FileUtils from './fileUtils.js';
7 |
8 | // import {Extension} from 'resource:///org/gnome/shell/extensions/extension.js';
9 | let Extension;
10 | try {
11 | let extensionObj = await import('resource:///org/gnome/shell/extensions/extension.js');
12 | Extension = extensionObj.Extension;
13 | } catch (e) {
14 | let extensionPrefsObj = await import('resource:///org/gnome/Shell/Extensions/js/extensions/prefs.js');
15 | Extension = extensionPrefsObj.ExtensionPreferences;
16 | }
17 |
18 | export function find(iconName) {
19 | let iconPath = `${FileUtils.current_extension_path}/icons/${iconName}`;
20 | if (GLib.file_test(iconPath, GLib.FileTest.EXISTS)) {
21 | return Gio.icon_new_for_string(`${iconPath}`);
22 | }
23 |
24 | return Gio.ThemedIcon.new_from_names([iconName]);
25 |
26 | }
27 |
28 | export function findPath(iconName) {
29 | let iconPath = `${FileUtils.current_extension_path}/icons/${iconName}`;
30 | if (GLib.file_test(iconPath, GLib.FileTest.EXISTS)) {
31 | return iconPath;
32 | }
33 |
34 | return null;
35 | }
36 |
--------------------------------------------------------------------------------
/utils/log.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import {PrefsUtils} from './prefsUtils.js';
4 |
5 |
6 | export const Log = class {
7 |
8 | constructor() {
9 | }
10 |
11 | isDebug() {
12 | return PrefsUtils.isDebug();
13 | }
14 |
15 | isVerboseLogging() {
16 | return PrefsUtils.isVerboseLogging();
17 | }
18 |
19 | debug(logContent) {
20 | if (this.isDebug()) {
21 | log(`[DEBUG ][Another window session manager] ${logContent}`);
22 | }
23 | }
24 |
25 | error(e, logContent) {
26 | if (!(e instanceof Error)) {
27 | e = new Error(e);
28 | }
29 | logError(e, `[ERROR ][Another window session manager] ${logContent}`);
30 | }
31 |
32 | info(logContent) {
33 | if (this.isVerboseLogging()) {
34 | log(`[INFO ][Another window session manager] ${logContent}`);
35 | }
36 | }
37 |
38 | warn(logContent) {
39 | log(`[WARNING][Another window session manager] ${logContent}`);
40 | }
41 |
42 | destroy() {
43 |
44 | }
45 |
46 | // Return a singleton instance
47 | static getDefault() {
48 | if (!Log._default) {
49 | Log._default = new Log();
50 | }
51 | return Log._default;
52 | }
53 |
54 | static destroyDefault() {
55 | if (Log._default) {
56 | Log._default.destroy();
57 | delete Log._default;
58 | }
59 | }
60 |
61 | }
62 |
--------------------------------------------------------------------------------
/utils/metaWindowUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Meta from 'gi://Meta';
4 |
5 |
6 | /**
7 | * Get the stable window id, don't change even after gnome shell is restarted
8 | *
9 | * On X11, return xid; On Wayland, return id
10 | *
11 | * @returns stable window id
12 | */
13 | export const getStableWindowId = function(metaWindow) {
14 | return Meta.is_wayland_compositor() ? metaWindow.get_id() : metaWindow.get_description();
15 | }
16 |
17 | export const isSurfaceActor = function(clutterActor) {
18 | const className = clutterActor.constructor.$gtype.name;
19 | // Excepted MetaSurfaceActorX11 and MetaSurfaceActorWayland on X11 and Wayland, respectively
20 | return className.startsWith('MetaSurfaceActor');
21 | }
--------------------------------------------------------------------------------
/utils/prefsUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | /**
4 | * The instance of the PrefsUtilsClass
5 | */
6 | export let PrefsUtils = null;
7 |
8 | /**
9 | * Initialize the PrefsUtilsClass from extension.js or prefs.js so that it can be used.
10 | *
11 | * @param {*} extensionObject
12 | * @param {*} settings
13 | */
14 | export function prefsUtilsInit(extensionObject, settings) {
15 | if (PrefsUtils) {
16 | return;
17 | }
18 |
19 | const prefsUtilsClass = new PrefsUtilsClass();
20 | prefsUtilsClass._init(extensionObject, settings);
21 | PrefsUtils = prefsUtilsClass;
22 | }
23 |
24 | export function prefsUtilsDestroy() {
25 | if (PrefsUtils) {
26 | PrefsUtils.destroy();
27 | PrefsUtils = null;
28 | }
29 | }
30 |
31 | /**
32 | * This class must be initialized using `prefsUtilsInit()` from extension.js or prefs.js before it can be used.
33 | */
34 | const PrefsUtilsClass = class {
35 |
36 | constructor() {
37 | }
38 |
39 | _init(extensionObject, settings) {
40 | this.extensionObject = extensionObject;
41 | this.settings = settings;
42 | }
43 |
44 | getSettingString(settingName) {
45 | return this.settings.get_string(settingName);
46 | }
47 |
48 | getSettings() {
49 | return this.settings;
50 | }
51 |
52 | getExtensionPath() {
53 | return this.extensionObject.path;
54 | }
55 |
56 | isDebug() {
57 | return this.settings.get_boolean('debugging-mode');
58 | }
59 |
60 | isVerboseLogging() {
61 | return this.settings.get_boolean('verbose-logging');
62 | }
63 |
64 | destroy() {
65 | this.settings = null;
66 | this.extensionObject = null;
67 | }
68 | }
69 |
70 |
--------------------------------------------------------------------------------
/utils/signal.js:
--------------------------------------------------------------------------------
1 | import GObject from 'gi://GObject';
2 |
3 |
4 | export const Signal = class {
5 |
6 | constructor() {
7 |
8 | }
9 |
10 | /**
11 | * Disconnect signal from an object without the below error / warning in `journalctl`:
12 | *
13 | * ../gobject/gsignal.c:2732: instance '0x55629xxxxxx' has no handler with id '11000'
14 | */
15 | disconnectSafely(obj, signalId) {
16 | if (!signalId) {
17 | return;
18 | }
19 |
20 | // https://gjs-docs.gnome.org/gobject20~2.66p/gobject.signal_handler_find
21 | // Fix ../gobject/gsignal.c:2732: instance '0x55629xxxxxx' has no handler with id '11000' in some case, see two callers for more info
22 | const matchedId = GObject.signal_handler_find(
23 | obj, // GObject.Object
24 | GObject.SignalMatchType.ID,
25 | signalId,
26 | null, null, null, null);
27 | if (matchedId) {
28 | obj.disconnect(signalId);
29 | }
30 | }
31 |
32 | }
33 |
--------------------------------------------------------------------------------
/utils/stringUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict'
2 |
3 | /**
4 | * Format string like this and fill variables in ${} with real data:
5 | *
6 | * Name=${appName} Comment=${appName} Type=Application Exec=${commandLine} Icon=${icon}
7 | *
8 | */
9 | export const format = function(stringTemplate, argumentsObj) {
10 | const obj = argumentsObj;
11 | if (typeof obj !== 'object') {
12 | throw(new Error('Wrong arguments, only supports object'));
13 | }
14 |
15 | for (const key in obj) {
16 | stringTemplate = stringTemplate.replaceAll("${" + key + "}", obj[key]);
17 | }
18 | return stringTemplate;
19 | }
20 |
--------------------------------------------------------------------------------
/utils/subprocessUtils.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Gio from 'gi://Gio';
4 | import GLib from 'gi://GLib';
5 |
6 | import * as Log from './log.js';
7 |
8 |
9 | export async function getProcessInfo(apps /*ShellApp*/, ignoreWindowsCb) {
10 | try {
11 | const pidSet = new Set();
12 | for (const app of apps) {
13 | let metaWindows = app.get_windows();
14 | for (const metaWindow of metaWindows) {
15 | if (ignoreWindowsCb && ignoreWindowsCb(metaWindow)) {
16 | continue;
17 | }
18 |
19 | const pid = metaWindow.get_pid();
20 | // pid is `0` if not known
21 | // Note that pass `0` or negative value to `ps -p` will get `error: process ID out of range`
22 | if (pid > 0) pidSet.add(pid);
23 | }
24 | }
25 |
26 | if (!pidSet.size) return;
27 |
28 | // Separated with comma
29 | const pids = Array.from(pidSet).join(',');
30 | // TODO get_sandboxed_app_id() Gets an unique id for a sandboxed app (currently flatpaks and snaps are supported).
31 | const psCmd = ['ps', '--no-headers', '-p', `${pids}`, '-o', 'lstart,%cpu,%mem,pid,command'];
32 |
33 | return new Promise((resolve, reject) => {
34 | try {
35 | let proc = Gio.Subprocess.new(
36 | psCmd,
37 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
38 | );
39 | proc.communicate_utf8_async(null, null, (proc, res) => {
40 | try {
41 | const processInfoMap = new Map();
42 | let [, stdout, stderr] = proc.communicate_utf8_finish(res);
43 | let status = proc.get_exit_status();
44 | if (status === 0 && stdout) {
45 | const lines = stdout.trim();
46 | for (const line of lines.split('\n')) {
47 | const processInfoArray = line.split(' ').filter(a => a);
48 | const pid = processInfoArray.slice(7, 8).join();
49 | processInfoMap.set(Number(pid), processInfoArray);
50 | }
51 | return resolve(processInfoMap);
52 | }
53 |
54 | Log.Log.getDefault().error(new Error(`Failed to query process info. status: ${status}, stdout: ${stdout}, stderr: ${stderr}`));
55 | resolve(processInfoMap);
56 | } catch(e) {
57 | Log.Log.getDefault().error(e);
58 | reject(e);
59 | }
60 | });
61 | } catch (e) {
62 | Log.Log.getDefault().error(e);
63 | reject(e);
64 | }
65 |
66 | })
67 | } catch (e) {
68 | Log.Log.getDefault().error(e);
69 | }
70 | }
71 |
72 | // A simple asynchronous read loop
73 | function readOutput(stream, lineBuffer) {
74 | stream.read_line_async(0, null, (stream, res) => {
75 | try {
76 | let line = stream.read_line_finish_utf8(res)[0];
77 |
78 | if (line !== null) {
79 | lineBuffer.push(line);
80 | readOutput(stream, lineBuffer);
81 | }
82 | } catch (e) {
83 | logError(e);
84 | }
85 | });
86 | }
87 |
88 | /**
89 | * We can get the pid after `proc.wait_finish(res)`, but note that the
90 | * subprocess might exit later with failure.
91 | *
92 | */
93 | export const trySpawnCmdstr = function(commandLineString, callBackOnSuccess, callBackOnFailure) {
94 | let success_, argv;
95 |
96 | try {
97 | [success_, argv] = GLib.shell_parse_argv(commandLineString);
98 | } catch (err) {
99 | // Replace "Error invoking GLib.shell_parse_argv: " with
100 | // something nicer
101 | err.message = err.message.replace(/[^:]*: /, `${_('Could not parse command:')}\n`);
102 | throw err;
103 | }
104 |
105 | let proc = Gio.Subprocess.new(
106 | argv,
107 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
108 | );
109 | return new Promise((resolve, reject) => {
110 | proc.wait_async(null, (proc, res) => {
111 | try {
112 | let successful = proc.wait_finish(res);
113 | let status = proc.get_exit_status();
114 | let stdoutInputStream = proc.get_stdout_pipe();
115 | let stderrInputStream = proc.get_stderr_pipe();
116 | if (!(stdoutInputStream instanceof Gio.DataInputStream)) {
117 | stdoutInputStream = new Gio.DataInputStream({
118 | base_stream: stdoutInputStream,
119 | });
120 | }
121 |
122 | if (!(stderrInputStream instanceof Gio.DataInputStream)) {
123 | stderrInputStream = new Gio.DataInputStream({
124 | base_stream: stderrInputStream,
125 | });
126 | }
127 |
128 | resolve([status === 0, status, stdoutInputStream, stderrInputStream]);
129 | } catch(e) {
130 | Log.Log.getDefault().error(e);
131 | reject(e);
132 | }
133 | });
134 | });
135 | }
136 |
137 | /**
138 | * Deprecated. Use `trySpawnCmdstr()` instead.
139 | *
140 | * Since `proc.communicate_utf8_finish(res)` only returns value
141 | * after the subprocess (created by `commandLineString`)
142 | * exits, we cannot get the pid right after the subprocess launches.
143 | * So there will be some kind of blocking here.
144 | */
145 | export const trySpawnCmdstrWithBlocking = function(commandLineString, callBackOnSuccess, callBackOnFailure) {
146 | let success_, argv;
147 |
148 | try {
149 | [success_, argv] = GLib.shell_parse_argv(commandLineString);
150 | } catch (err) {
151 | // Replace "Error invoking GLib.shell_parse_argv: " with
152 | // something nicer
153 | err.message = err.message.replace(/[^:]*: /, `${_('Could not parse command:')}\n`);
154 | throw err;
155 | }
156 |
157 | let proc = Gio.Subprocess.new(
158 | argv,
159 | Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE
160 | );
161 | return new Promise((resolve, reject) => {
162 | proc.communicate_utf8_async(null, null, (proc, res) => {
163 | try {
164 | let [, stdout, stderr] = proc.communicate_utf8_finish(res);
165 | let status = proc.get_exit_status();
166 | resolve([status === 0, status, stdout, stderr]);
167 | } catch(e) {
168 | Log.Log.getDefault().error(e);
169 | reject(e);
170 | }
171 | });
172 | });
173 | }
174 |
175 | export const trySpawn = async function(commandLineArray, callBackOnSuccess, callBackOnFailure) {
176 | try {
177 | return await new Promise((resolve, reject) => {
178 | trySpawnAsync(commandLineArray,
179 | (output) => {
180 | if (callBackOnSuccess) {
181 | callBackOnSuccess(output);
182 | }
183 | resolve(output);
184 | },
185 | (output, status) => {
186 | if (callBackOnFailure) {
187 | callBackOnFailure(output, status);
188 | }
189 | reject(new Error(output));
190 | });
191 | });
192 | } catch (e) {
193 | Log.Log.getDefault().error(e);
194 | }
195 | }
196 | /**
197 | * Based on:
198 | * 1. https://gjs.guide/guides/gio/subprocesses.html#asynchronous-communication
199 | * 2. https://gitlab.gnome.org/GNOME/gnome-shell/blob/8fda3116f03d95fabf3fac6d082b5fa268158d00/js/misc/util.js:L111
200 | *
201 | * This implement will return the `stderr` and `stdout` to caller via two callback
202 | * `callBackOnFailure` and `callBackOnFailure`
203 | *
204 | */
205 | export const trySpawnAsync = function(commandLineArray, callBackOnSuccess, callBackOnFailure) {
206 | try {
207 | let [, pid, stdin, stdout, stderr] = GLib.spawn_async_with_pipes(
208 | // Working directory, passing %null to use the parent's
209 | null,
210 | // An array of arguments
211 | commandLineArray,
212 | // Process ENV, passing %null to use the parent's
213 | null,
214 | // Flags; we need to use PATH so `ls` can be found and also need to know
215 | // when the process has finished to check the output and status.
216 | GLib.SpawnFlags.SEARCH_PATH | GLib.SpawnFlags.DO_NOT_REAP_CHILD,
217 | // Child setup function
218 | () => {
219 | try {
220 | global.context.restore_rlimit_nofile();
221 | } catch (err) {
222 | }
223 | }
224 | );
225 |
226 | // Any unsused streams still have to be closed explicitly, otherwise the
227 | // file descriptors may be left open
228 | GLib.close(stdin);
229 |
230 | // Okay, now let's get output stream for `stdout`
231 | let stdoutStream = new Gio.DataInputStream({
232 | base_stream: new Gio.UnixInputStream({
233 | fd: stdout,
234 | close_fd: true
235 | }),
236 | close_base_stream: true
237 | });
238 |
239 | // We'll read the output asynchronously to avoid blocking the main thread
240 | let stdoutLines = [];
241 | readOutput(stdoutStream, stdoutLines);
242 |
243 | // We want the real error from `stderr`, so we'll have to do the same here
244 | let stderrStream = new Gio.DataInputStream({
245 | base_stream: new Gio.UnixInputStream({
246 | fd: stderr,
247 | close_fd: true
248 | }),
249 | close_base_stream: true
250 | });
251 |
252 | let stderrLines = [];
253 | readOutput(stderrStream, stderrLines);
254 |
255 | // Watch for the process to finish, being sure to set a lower priority than
256 | // we set for the read loop, so we get all the output
257 | GLib.child_watch_add(GLib.PRIORITY_DEFAULT_IDLE, pid, (pid, status) => {
258 | // TODO Note that this status is usually not equal to the integer passed to `exit()`
259 | // See: https://gitlab.gnome.org/GNOME/glib/-/blob/5d498f4d1ce0fd124cbfb065fb2155a2e964bf5f/glib/gmain.h#L244
260 | if (status === 0) {
261 | if (callBackOnSuccess) {
262 | callBackOnSuccess(stdoutLines.join('\n'));
263 | }
264 | } else {
265 | if (callBackOnFailure) {
266 | callBackOnFailure(stderrLines.join('\n'));
267 | }
268 | }
269 |
270 | // Ensure we close the remaining streams and process
271 | stdoutStream.close(null);
272 | stderrStream.close(null);
273 | GLib.spawn_close_pid(pid);
274 |
275 | });
276 | } catch (e) {
277 | logError(e);
278 | }
279 | }
280 |
281 |
282 | /**
283 | * Execute a command asynchronously and check the exit status.
284 | *
285 | * If given, @cancellable can be used to stop the process before it finishes.
286 | *
287 | * @param {string[]} argv - a list of string arguments
288 | * @param {Gio.Cancellable} [cancellable] - optional cancellable object
289 | * @returns {Promise} - The process success
290 | */
291 | export async function execCheck(argv, cancellable = null) {
292 | let cancelId = 0;
293 | let proc = new Gio.Subprocess({
294 | argv: argv,
295 | flags: Gio.SubprocessFlags.NONE
296 | });
297 | proc.init(cancellable);
298 |
299 | if (cancellable instanceof Gio.Cancellable) {
300 | cancelId = cancellable.connect(() => proc.force_exit());
301 | }
302 |
303 | return new Promise((resolve, reject) => {
304 | proc.wait_check_async(null, (proc, res) => {
305 | try {
306 | if (!proc.wait_check_finish(res)) {
307 | let status = proc.get_exit_status();
308 |
309 | throw new Gio.IOErrorEnum({
310 | code: Gio.io_error_from_errno(status),
311 | message: GLib.strerror(status)
312 | });
313 | }
314 |
315 | resolve();
316 | } catch (e) {
317 | reject(e);
318 | } finally {
319 | if (cancelId > 0) {
320 | cancellable.disconnect(cancelId);
321 | }
322 | }
323 | });
324 | });
325 | }
326 |
327 |
--------------------------------------------------------------------------------
/utils/tooltip.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Clutter from 'gi://Clutter';
4 | import Gio from 'gi://Gio';
5 | import GLib from 'gi://GLib';
6 | import Pango from 'gi://Pango';
7 | import St from 'gi://St';
8 |
9 | import * as Main from 'resource:///org/gnome/shell/ui/main.js';
10 |
11 | /**
12 | * Note: Adapted from: https://github.com/GSConnect/gnome-shell-extension-gsconnect/blob/master/src/shell/tooltip.js
13 | *
14 | * I may or may not modify it to fit the needs of this project.
15 | */
16 |
17 | /**
18 | * An StTooltip for ClutterActors
19 | *
20 | * Adapted from: https://github.com/RaphaelRochet/applications-overview-tooltip
21 | * See also: https://github.com/GNOME/gtk/blob/master/gtk/gtktooltip.c
22 | */
23 | var TOOLTIP_BROWSE_ID = 0;
24 | var TOOLTIP_BROWSE_MODE = false;
25 |
26 | export const Tooltip = class Tooltip {
27 |
28 | constructor(params) {
29 | Object.assign(this, params);
30 |
31 | this._bin = null;
32 | this._hoverTimeoutId = 0;
33 | this._showing = false;
34 |
35 | this._destroyId = this.parent.connect(
36 | 'destroy',
37 | this.destroy.bind(this)
38 | );
39 |
40 | this._hoverId = this.parent.connect(
41 | 'notify::hover',
42 | this._onHover.bind(this)
43 | );
44 |
45 | this._buttonPressEventId = this.parent.connect(
46 | 'button-press-event',
47 | this._hide.bind(this)
48 | );
49 | }
50 |
51 | get custom() {
52 | if (this._custom === undefined)
53 | this._custom = null;
54 |
55 | return this._custom;
56 | }
57 |
58 | set custom(actor) {
59 | this._custom = actor;
60 | this._markup = null;
61 | this._text = null;
62 |
63 | if (this._showing)
64 | this._show();
65 | }
66 |
67 | get gicon() {
68 | if (this._gicon === undefined)
69 | this._gicon = null;
70 |
71 | return this._gicon;
72 | }
73 |
74 | set gicon(gicon) {
75 | this._gicon = gicon;
76 |
77 | if (this._showing)
78 | this._show();
79 | }
80 |
81 | get icon() {
82 | return (this.gicon) ? this.gicon.name : null;
83 | }
84 |
85 | set icon(icon_name) {
86 | if (!icon_name)
87 | this.gicon = null;
88 | else
89 | this.gicon = new Gio.ThemedIcon({name: icon_name});
90 | }
91 |
92 | get markup() {
93 | if (this._markup === undefined)
94 | this._markup = null;
95 |
96 | return this._markup;
97 | }
98 |
99 | set markup(text) {
100 | this._markup = text;
101 | this._text = null;
102 |
103 | if (this._showing)
104 | this._show();
105 | }
106 |
107 | get text() {
108 | if (this._text === undefined)
109 | this._text = null;
110 |
111 | return this._text;
112 | }
113 |
114 | set text(text) {
115 | this._markup = null;
116 | this._text = text;
117 |
118 | if (this._showing)
119 | this._show();
120 | }
121 |
122 | get x_offset() {
123 | if (this._x_offset === undefined)
124 | this._x_offset = 0;
125 |
126 | return this._x_offset;
127 | }
128 |
129 | set x_offset(offset) {
130 | this._x_offset = (Number.isInteger(offset)) ? offset : 0;
131 | }
132 |
133 | get y_offset() {
134 | if (this._y_offset === undefined)
135 | this._y_offset = 0;
136 |
137 | return this._y_offset;
138 | }
139 |
140 | set y_offset(offset) {
141 | this._y_offset = (Number.isInteger(offset)) ? offset : 0;
142 | }
143 |
144 | _show() {
145 | if (this.text === null && this.markup === null)
146 | return this._hide();
147 |
148 | if (this._bin === null) {
149 | this._bin = new St.Bin({
150 | style_class: 'osd-window awsm-tooltip',
151 | opacity: 232,
152 | });
153 |
154 | if (this.custom) {
155 | this._bin.child = this.custom;
156 | } else {
157 | this._bin.child = new St.BoxLayout({vertical: false});
158 |
159 | if (this.gicon) {
160 | this._bin.child.icon = new St.Icon({
161 | gicon: this.gicon,
162 | y_align: St.Align.START,
163 | });
164 | this._bin.child.icon.set_y_align(Clutter.ActorAlign.START);
165 | this._bin.child.add_child(this._bin.child.icon);
166 | }
167 |
168 | this.label = new St.Label({text: this.markup || this.text});
169 | this.label.clutter_text.line_wrap = true;
170 | this.label.clutter_text.line_wrap_mode = Pango.WrapMode.WORD;
171 | this.label.clutter_text.ellipsize = Pango.EllipsizeMode.NONE;
172 | this.label.clutter_text.use_markup = (this.markup);
173 | this._bin.child.add_child(this.label);
174 | }
175 |
176 | Main.layoutManager.uiGroup.add_child(this._bin);
177 | Main.layoutManager.uiGroup.set_child_above_sibling(this._bin, null);
178 | } else if (this.custom) {
179 | this._bin.child = this.custom;
180 | } else {
181 | if (this._bin.child.icon)
182 | this._bin.child.icon.destroy();
183 |
184 | if (this.gicon) {
185 | this._bin.child.icon = new St.Icon({gicon: this.gicon});
186 | this._bin.child.insert_child_at_index(this._bin.child.icon, 0);
187 | }
188 |
189 | this.label.clutter_text.text = this.markup || this.text;
190 | this.label.clutter_text.use_markup = (this.markup);
191 | }
192 |
193 | // Position tooltip
194 | let [x, y] = this.parent.get_transformed_position();
195 | x = (x + (this.parent.width / 2)) - Math.round(this._bin.width / 2);
196 |
197 | x += this.x_offset;
198 | y += this.y_offset;
199 |
200 | // Show tooltip
201 | if (this._showing) {
202 | this._bin.ease({
203 | x: x,
204 | y: y,
205 | time: 0.15,
206 | transition: Clutter.AnimationMode.EASE_OUT_QUAD,
207 | });
208 | } else {
209 | this._bin.set_position(x, y);
210 | this._bin.ease({
211 | opacity: 232,
212 | time: 0.15,
213 | transition: Clutter.AnimationMode.EASE_OUT_QUAD,
214 | });
215 |
216 | this._showing = true;
217 | }
218 |
219 | // Enable browse mode
220 | TOOLTIP_BROWSE_MODE = true;
221 |
222 | if (TOOLTIP_BROWSE_ID) {
223 | GLib.source_remove(TOOLTIP_BROWSE_ID);
224 | TOOLTIP_BROWSE_ID = 0;
225 | }
226 |
227 | if (this._hoverTimeoutId) {
228 | GLib.source_remove(this._hoverTimeoutId);
229 | this._hoverTimeoutId = 0;
230 | }
231 | }
232 |
233 | _hide() {
234 | if (this._bin) {
235 | this._bin.ease({
236 | opacity: 0,
237 | time: 0.10,
238 | transition: Clutter.AnimationMode.EASE_OUT_QUAD,
239 | onComplete: () => {
240 | Main.layoutManager.uiGroup.remove_child(this._bin);
241 |
242 | if (this.custom)
243 | this._bin.remove_child(this.custom);
244 |
245 | this._bin.destroy();
246 | this._bin = null;
247 | },
248 | });
249 | }
250 |
251 | TOOLTIP_BROWSE_ID = GLib.timeout_add(GLib.PRIORITY_DEFAULT, 500, () => {
252 | TOOLTIP_BROWSE_MODE = false;
253 | TOOLTIP_BROWSE_ID = 0;
254 | return false;
255 | });
256 |
257 | if (this._hoverTimeoutId) {
258 | GLib.source_remove(this._hoverTimeoutId);
259 | this._hoverTimeoutId = 0;
260 | }
261 |
262 | this._showing = false;
263 | this._hoverTimeoutId = 0;
264 | }
265 |
266 | _onHover() {
267 | if (this.parent.hover) {
268 | if (!this._hoverTimeoutId) {
269 | if (this._showing) {
270 | this._show();
271 | } else {
272 | this._hoverTimeoutId = GLib.timeout_add(
273 | GLib.PRIORITY_DEFAULT,
274 | (TOOLTIP_BROWSE_MODE) ? 60 : 500,
275 | () => {
276 | this._show();
277 | this._hoverTimeoutId = 0;
278 | return false;
279 | }
280 | );
281 | }
282 | }
283 | } else {
284 | this._hide();
285 | }
286 | }
287 |
288 | destroy() {
289 | this.parent.disconnect(this._destroyId);
290 | this.parent.disconnect(this._hoverId);
291 | this.parent.disconnect(this._buttonPressEventId);
292 |
293 | if (this.custom)
294 | this.custom.destroy();
295 |
296 | if (this._bin) {
297 | Main.layoutManager.uiGroup.remove_child(this._bin);
298 | this._bin.destroy();
299 | }
300 |
301 | if (TOOLTIP_BROWSE_ID) {
302 | GLib.source_remove(TOOLTIP_BROWSE_ID);
303 | TOOLTIP_BROWSE_ID = 0;
304 | }
305 |
306 | if (this._hoverTimeoutId) {
307 | GLib.source_remove(this._hoverTimeoutId);
308 | this._hoverTimeoutId = 0;
309 | }
310 | }
311 | };
312 |
313 |
--------------------------------------------------------------------------------
/windowTilingSupport.js:
--------------------------------------------------------------------------------
1 | 'use strict';
2 |
3 | import Shell from 'gi://Shell';
4 | import Meta from 'gi://Meta';
5 | import GObject from 'gi://GObject';
6 |
7 | import * as Log from './utils/log.js';
8 | import {PrefsUtils} from './utils/prefsUtils.js';
9 |
10 |
11 | // Singleton class, all methods are `static`
12 | export class WindowTilingSupport {
13 |
14 | static initialize() {
15 | this._log = new Log.Log();
16 | this._settings = PrefsUtils.getSettings();
17 | this._defaultAppSystem = Shell.AppSystem.get_default();
18 |
19 | this._signals = new WindowTilingSupportSignals();
20 |
21 | // Used for getting another raised signal id to prevent 'too much recursion' due to raising each other.
22 | this._signalsConnectedMap = new Map();
23 |
24 | this._grabbedWindowsAboutToUntileMap = new Map();
25 |
26 | this._grabOpBeginId = global.display.connect('grab-op-begin', this._grabOpBegin.bind(this));
27 | this._grabOpEndId = global.display.connect('grab-op-end', this._grabOpEnd.bind(this));
28 |
29 | }
30 |
31 | static prepareToTile(metaWindow, window_tiling) {
32 | if (!window_tiling) return;
33 | if (!this._settings.get_boolean('restore-window-tiling')) return;
34 | const windowAboutToResize = this._getWindowAboutToResize(window_tiling);
35 | if (!windowAboutToResize) return;
36 |
37 | metaWindow._tile_match_awsm = windowAboutToResize;
38 | windowAboutToResize._tile_match_awsm = metaWindow;
39 |
40 | this._signals.emit('window-tiled', metaWindow, windowAboutToResize);
41 |
42 | // Connect `raised` only once and this will prevent `JS ERROR: too much recursion`
43 | if (!this._signalsConnectedMap.get(metaWindow)) {
44 | const raisedId = metaWindow.connect('raised', () => {
45 | const raisedTogether = this._settings.get_boolean('raise-windows-together');
46 | if (raisedTogether) {
47 | const anotherWindowRaisedId = this._signalsConnectedMap.get(windowAboutToResize);
48 | windowAboutToResize.block_signal_handler(anotherWindowRaisedId);
49 | windowAboutToResize.raise();
50 | windowAboutToResize.unblock_signal_handler(anotherWindowRaisedId);
51 | }
52 | });
53 | this._signalsConnectedMap.set(metaWindow, raisedId);
54 | }
55 | }
56 |
57 | static _grabOpBegin(display, grabbedWindow, grabOp) {
58 | // Fix `JS ERROR: TypeError: grabbedWindow is null` while `grab-op-begin` by `dash to panel`,
59 | // who emits nullish grabbedWindow.
60 | if (!grabbedWindow) return;
61 |
62 | // Check if the grabbed window has been in a tiling state with another window
63 | const windowAboutToResize = grabbedWindow._tile_match_awsm;
64 | if (!windowAboutToResize || windowAboutToResize._tile_match_awsm !== grabbedWindow)
65 | return;
66 |
67 | // When position changed
68 | if (grabOp === Meta.GrabOp.MOVING) {
69 | const oldGrabbedWindowRect = grabbedWindow.get_frame_rect().copy();
70 | this._grabbedWindowsAboutToUntileMap.set(grabbedWindow, oldGrabbedWindowRect);
71 | return;
72 | }
73 |
74 | if (!this._settings.get_boolean('restore-window-tiling')) return;
75 |
76 | this._sizeChangedId = grabbedWindow.connect('size-changed', () => {
77 | const grabbedWindowRect = grabbedWindow.get_frame_rect();
78 | const windowAboutToResizeRect = windowAboutToResize.get_frame_rect();
79 | const grabbedWindowOnLeftSide = grabbedWindowRect.x < windowAboutToResizeRect.x;
80 | let xywh = null;
81 | if (grabbedWindowOnLeftSide) {
82 | xywh = [
83 | grabbedWindowRect.width,
84 | windowAboutToResizeRect.y,
85 | windowAboutToResizeRect.width - (grabbedWindowRect.width - windowAboutToResizeRect.x),
86 | windowAboutToResizeRect.height];
87 | } else {
88 | xywh = [
89 | windowAboutToResizeRect.x,
90 | windowAboutToResizeRect.y,
91 | grabbedWindowRect.x,
92 | windowAboutToResizeRect.height];
93 | }
94 |
95 | if (xywh) {
96 | windowAboutToResize.move_resize_frame(false, ...xywh);
97 | }
98 |
99 | });
100 | }
101 |
102 | static _grabOpEnd(display, grabbedWindow, grabOp) {
103 | // grabbedWindow is null, tested on Fedora 35 with Gnome 41.6 and Wayland,
104 | // by clicking the indicator show and then hide the popup menu
105 | if (!grabbedWindow) return;
106 |
107 | const oldGrabbedWindowRect = this._grabbedWindowsAboutToUntileMap.get(grabbedWindow);
108 | const currentRect = grabbedWindow.get_frame_rect();
109 | // Untile if any of x, y, width and height changed
110 | if (oldGrabbedWindowRect &&
111 | (oldGrabbedWindowRect.x !== currentRect.x
112 | || oldGrabbedWindowRect.y !== currentRect.y
113 | || oldGrabbedWindowRect.width !== currentRect.width
114 | || oldGrabbedWindowRect.height !== currentRect.height))
115 | {
116 | const anotherTilingWindow = grabbedWindow._tile_match_awsm;
117 |
118 | this._log.debug(`Untiling ${grabbedWindow.get_title()}`);
119 | delete grabbedWindow._tile_match_awsm;
120 |
121 | if (anotherTilingWindow) {
122 | this._log.debug(`Untiling ${anotherTilingWindow.get_title()}`);
123 | delete anotherTilingWindow._tile_match_awsm;
124 | }
125 | this._grabbedWindowsAboutToUntileMap.delete(grabbedWindow);
126 |
127 | this._disconnectRaisedSignals();
128 |
129 | this._signals.emit('window-untiled', grabbedWindow, anotherTilingWindow);
130 | }
131 |
132 | if (this._sizeChangedId) {
133 | grabbedWindow.disconnect(this._sizeChangedId);
134 | this._sizeChangedId = 0;
135 | }
136 |
137 | }
138 |
139 | static _getWindowAboutToResize(window_tiling) {
140 | if (!window_tiling) return null;
141 | const window_tile_for = window_tiling.window_tile_for;
142 | const shellApp = this._defaultAppSystem.lookup_app(window_tile_for.desktop_file_id);
143 | if (!shellApp) return null;
144 | const windows = shellApp.get_windows();
145 | if (!windows || !windows.length) return null;
146 |
147 | let windowAboutToResize = null;
148 | if (windows.length === 1) {
149 | windowAboutToResize = windows[0];
150 | } else {
151 | // Get one window by matching title
152 | for (const win of windows) {
153 | if (win.get_title() === window_tile_for.window_title) {
154 | windowAboutToResize = win;
155 | break;
156 | }
157 | }
158 | }
159 |
160 | return windowAboutToResize;
161 | }
162 |
163 | static connect(signal, func) {
164 | this._signals.connect(signal, func);
165 | }
166 |
167 | static disconnect(id) {
168 | this._signals.disconnect(id);
169 | }
170 |
171 | static _disconnectRaisedSignals() {
172 | if (this._signalsConnectedMap) {
173 | this._signalsConnectedMap.forEach((id, obj) => {
174 | obj.disconnect(id);
175 | });
176 | this._signalsConnectedMap.clear();
177 | }
178 | }
179 |
180 | static destroy() {
181 |
182 | if (this._grabbedWindowsAboutToUntileMap) {
183 | this._grabbedWindowsAboutToUntileMap.clear();
184 | this._grabbedWindowsAboutToUntileMap = null;
185 | }
186 |
187 | this._disconnectRaisedSignals();
188 |
189 | this._signalsConnectedMap = null;
190 |
191 | if (this._grabOpBeginId) {
192 | global.display.disconnect(this._grabOpBeginId);
193 | this._grabOpBeginId = 0;
194 | }
195 |
196 | if (this._grabOpEndId) {
197 | global.display.disconnect(this._grabOpEndId);
198 | this._grabOpEndId = 0;
199 | }
200 | }
201 |
202 |
203 | }
204 |
205 | const WindowTilingSupportSignals = GObject.registerClass({
206 | Signals: {
207 | 'window-tiled': {
208 | param_types: [Meta.Window.$gtype, Meta.Window.$gtype],
209 | flags: GObject.SignalFlags.RUN_LAST,
210 | },
211 | 'window-untiled': {
212 | param_types: [Meta.Window.$gtype, Meta.Window.$gtype],
213 | flags: GObject.SignalFlags.RUN_LAST,
214 | },
215 | }
216 | }, class WindowTilingSupportSignals extends GObject.Object{
217 |
218 | _init() {
219 | super._init();
220 | }
221 |
222 |
223 | });
--------------------------------------------------------------------------------