Skip to content

Commit

Permalink
Merge pull request #44 from nlpsuge/experiments-close-multiple-window…
Browse files Browse the repository at this point in the history
…s-GtkListBox

Experimental feature: Close windows according to shortcuts using `ydotool`

It supports X11 and Wayland.

This PR is part of #9
  • Loading branch information
nlpsuge authored Jun 27, 2022
2 parents dc30a7a + 23ae35c commit eb73d0e
Show file tree
Hide file tree
Showing 26 changed files with 1,798 additions and 105 deletions.
44 changes: 41 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,37 @@ To modify the delay, timer, and how to restore a session:
1. Search saved session by the session name fuzzily
1. ...

## How to `Restore a session at startup`?
## Close windows

### How to make `Close by rules` work

```bash
# 1. Install `ydotool` using the package manager and make sure the version is greater than v1.0.0
sudo dnf install ydotool
#Or install it from the source code: https://github.com/ReimuNotMoe/ydotool

#Check the permission of `/dev/uinput`, if it's `crw-rw----+`, you can skip step 2
# 2. Get permission to access to `/dev/uinput` as the normal user
sudo touch /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
sudo echo '# See:
# https://github.com/ValveSoftware/steam-devices/blob/master/60-steam-input.rules
# https://github.com/ReimuNotMoe/ydotool/issues/25
# ydotool udev write access
KERNEL=="uinput", SUBSYSTEM=="misc", TAG+="uaccess", OPTIONS+="static_node=uinput"' > /etc/udev/rules.d/60-awsm-ydotool-uinput.rules
#Remove executable permission (a.k.a. x)
sudo chmod 644 /etc/udev/rules.d/60-awsm-ydotool-uinput.rules

# 3. Autostart the ydotoold service under the normal user
sudo cp /usr/lib/systemd/system/ydotool.service /usr/lib/systemd/user
sudo systemctl --user enable ydotool.service
```

And then reboot the system to take effect. Relogin maybe work too.

## Restore sessions

### How to `Restore a session at startup`?

To make it work, you must enable it through `Restore sessions -> Restore at startup` in the Preferences AND active a session by clicking <img src=https://user-images.githubusercontent.com/2271720/162792222-0fc7e6ca-1382-49cf-975a-f53d878d0479.png width="24" height="13"> in the popup menu.

Expand Down Expand Up @@ -91,9 +121,17 @@ Please do not modify `_gnome-shell-extension-another-window-session-manager.desk


# Dependencies
This project uses `ps` and `pwdx` to get some information from a process, install it via `dnf install procps-ng` if you don't have.
* procps-ng

Use `ps` and `pwdx` to get some information from a process, install it via `dnf install procps-ng` if you don't have.

* glib2

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`.

* ydotool

And it uses `gdbus` to call the remote method, which is provided by this exension, to implement the `restore at start` feature. `gdbus` is part of `glib2`.
Send keys to close the application with multiple windows.

# Known issues

Expand Down
6 changes: 6 additions & 0 deletions bin/install-udev-rules-for-ydotool.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
# $1: FileUtils.desktop_template_path_ydotool_uinput_rules
# $2: FileUtils.system_udev_rules_path_ydotool_uinput_rules

cp "$1" "$2" && chmod 644 "$2"

239 changes: 230 additions & 9 deletions closeSession.js
Original file line number Diff line number Diff line change
@@ -1,17 +1,36 @@
'use strict';

const { Shell } = imports.gi;
const { Meta, Shell, Gio, GLib } = imports.gi;

const Main = imports.ui.main;
const Scripting = imports.ui.scripting;

const Util = imports.misc.util;

const ExtensionUtils = imports.misc.extensionUtils;
const Me = ExtensionUtils.getCurrentExtension();
const Log = Me.imports.utils.log;
const PrefsUtils = Me.imports.utils.prefsUtils;
const SubprocessUtils = Me.imports.utils.subprocessUtils;

const OpenWindowsInfoTracker = Me.imports.openWindowsInfoTracker;

const Constants = Me.imports.constants;


var CloseSession = class {
constructor() {
this._log = new Log.Log();
this._prefsUtils = new PrefsUtils.PrefsUtils();
this._settings = this._prefsUtils.getSettings();

this._skip_app_with_multiple_windows = true;
this._defaultAppSystem = Shell.AppSystem.get_default();

this._subprocessLauncher = new Gio.SubprocessLauncher({
flags: (Gio.SubprocessFlags.STDOUT_PIPE |
Gio.SubprocessFlags.STDERR_PIPE)});

// TODO Put into Settings
// All apps in the whitelist should be closed safely, no worrying about lost data
this.whitelist = ['org.gnome.Terminal.desktop', 'org.gnome.Nautilus.desktop', 'smplayer.desktop'];
Expand All @@ -26,23 +45,225 @@ var CloseSession = class {
workspaceManager.get_workspace_by_index(i)._keepAliveId = false;
}

let [running_apps_closing_by_rules, new_running_apps] = this._getRunningAppsClosingByRules();
this._tryCloseAppsByRules(running_apps_closing_by_rules);

for (const app of new_running_apps) {
this._closeOneApp(app);
}

}

_closeOneApp(app) {
if (this._skip_multiple_windows(app)) {
this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`);
} else {
this._log.debug(`Closing ${app.get_name()}`);
app.get_windows().forEach(w => w._aboutToClose = true);
app.request_quit();
}
}

_tryCloseAppsByRules(running_apps_closing_by_rules) {
if (!running_apps_closing_by_rules || running_apps_closing_by_rules.length === 0) {
return;
}

const app = running_apps_closing_by_rules.shift();

const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules');
const closeWindowsRulesObj = JSON.parse(closeWindowsRules);
const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()];

if (rules?.type === 'shortcut') {
let keycodesSegments = [];
let shortcutsOriginal = [];
let keycodes = [];
for (const order in rules.value) {
const rule = rules.value[order];
let shortcut = rule.shortcut;
let state = rule.state;
let keycode = rule.keycode;
const linuxKeycodes = this._convertToLinuxKeycodes(state, keycode);
const translatedLinuxKeycodes = linuxKeycodes.slice()
// Press keys
.map(k => k + ':1')
.concat(linuxKeycodes.slice()
// Release keys
.reverse().map(k => k + ':0'))
// keycodes = keycodes.concat(translatedLinuxKeycodes);
keycodesSegments.push(translatedLinuxKeycodes);
shortcutsOriginal.push(shortcut);
}

// Leave the overview first, so the keys can be sent to the activated windows
if (Main.overview.visible) {
Main.overview.hide();
const hiddenId = Main.overview.connect('hidden',
() => {
Main.overview.disconnect(hiddenId);
this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules);
});
} else {
this._activateAndCloseWindows(app, keycodesSegments, shortcutsOriginal, running_apps_closing_by_rules);
}

}

}

_convertToLinuxKeycodes(state, keycode) {
let keycodes = [];
// Convert to key codes defined in /usr/include/linux/input-event-codes.h
if (state & Constants.GDK_SHIFT_MASK) {
// KEY_LEFTSHIFT
keycodes.push(42);
}
if (state & Constants.GDK_CONTROL_MASK) {
// KEY_LEFTCTRL
keycodes.push(29);
}
if (state & Constants.GDK_ALT_MASK) {
// KEY_LEFTALT
keycodes.push(56);
}
if (state & Constants.GDK_META_MASK) {
// KEY_LEFTMETA
keycodes.push(125);
}
// The Xorg keycodes are 8 larger than the Linux keycodes.
// See https://wiki.archlinux.org/title/Keyboard_input#Identifying_keycodes_in_Xorg
keycodes.push(keycode - 8);
return keycodes;
}

_activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules) {
if (!linuxKeyCodesSegments || linuxKeyCodesSegments.length === 0) {
// Proceed the next rule
this._tryCloseAppsByRules(running_apps_closing_by_rules);
return;
}
const linuxKeyCodes = linuxKeyCodesSegments.shift();
const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules');
const closeWindowsRulesObj = JSON.parse(closeWindowsRules);
const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()];
const keyDelay = rules?.keyDelay;
const cmd = ['ydotool', 'key', '--key-delay', !keyDelay ? '0' : keyDelay + ''].concat(linuxKeyCodes);
const cmdStr = cmd.join(' ');

this._log.info(`Closing the app ${app.get_name()} by sending: ${cmdStr} (${shortcutsOriginal.join(' ')})`);

this._activateAndFocusWindow(app);
SubprocessUtils.trySpawnAsync(cmd,
(output) => {
this._log.info(`Succeed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`);
this._activateAndCloseWindows(app, linuxKeyCodesSegments, shortcutsOriginal, running_apps_closing_by_rules);
}, (output) => {
this._log.error(new Error(`Failed to send keys to close the windows of the previous app ${app.get_name()}. output: ${output}`));
});
}

_getRunningAppsClosingByRules() {
if (!this._settings.get_boolean('enable-close-by-rules')) {
return [[], this._defaultAppSystem.get_running()];
}

let running_apps_closing_by_rules = [];
let new_running_apps = [];
let running_apps = this._defaultAppSystem.get_running();
for (const app of running_apps) {
const app_name = app.get_name();
if (this._skip_multiple_windows(app)) {
this._log.debug(`Skipping ${app.get_name()} because it has more than one windows`);
continue;
const closeWindowsRules = this._prefsUtils.getSettingString('close-windows-rules');
const closeWindowsRulesObj = JSON.parse(closeWindowsRules);
const rules = closeWindowsRulesObj[app.get_app_info()?.get_filename()];
if (!rules || !rules.enabled || !rules.value) {
new_running_apps.push(app);
} else {
running_apps_closing_by_rules.push(app);
}
this._log.debug(`Closing ${app_name}`);
app.request_quit();
}

return [running_apps_closing_by_rules, new_running_apps];
}

_activateAndFocusWindow(app) {
let windows;
if (Meta.is_wayland_compositor()) {
windows = this._sortWindowsOnWayland(app);
} else {
windows = this._sortWindowsOnX11(app);
}

const topLevelWindow = windows[windows.length - 1];
if (topLevelWindow) {
this._log.info(`Activating the running window ${topLevelWindow.get_title()} of ${app.get_name()}`);
Main.activateWindow(topLevelWindow);
}
}

_sortWindowsOnWayland(app) {
const windows = app.get_windows();
windows.sort((w1, w2) => {
const windowStableSequence1 = w1.get_stable_sequence();
const windowStableSequence2 = w2.get_stable_sequence();
return this._compareWindowStableSequence(windowStableSequence1, windowStableSequence2);
});
return windows;
}

_sortWindowsOnX11(app) {
const savedWindowsMappingJsonStr = this._settings.get_string('windows-mapping');
const savedWindowsMapping = new Map(JSON.parse(savedWindowsMappingJsonStr));

const app_info = app.get_app_info();
const desktopFullPath = app_info.get_filename();
const xidObj = savedWindowsMapping.get(desktopFullPath);
const windows = app.get_windows();
windows.sort((w1, w2) => {
const xid1 = w1.get_description();
const value1 = xidObj[xid1];
let windowStableSequence1;
if (value1) {
windowStableSequence1 = value1.windowStableSequence;
} else {
windowStableSequence1 = w1.get_stable_sequence();
this._log.warn(`Mapping for this xid ${xid1} and stable sequence does not exist, use sequence ${windowStableSequence1} instead. app name: ${app.get_name()}, window title: ${w1.get_title()}`);
}

const xid2 = w2.get_description();
const value2 = xidObj[xid2];
let windowStableSequence2;
if (value2) {
windowStableSequence2 = value2.windowStableSequence;
} else {
windowStableSequence2 = w2.get_stable_sequence();
this._log.warn(`Mapping for this xid ${xid2} and stable sequence does not exist, use sequence ${windowStableSequence2} instead. app name: ${app.get_name()}, window title: ${w2.get_title()}`);
}

return this._compareWindowStableSequence(windowStableSequence1, windowStableSequence2);
});
return windows;
}

_compareWindowStableSequence(windowStableSequence1, windowStableSequence2) {
const diff = windowStableSequence1 - windowStableSequence2;
if (diff === 0) {
return 0;
}

if (diff > 0) {
return 1;
}

if (diff < 0) {
return -1;
}
}

_skip_multiple_windows(shellApp) {
if (shellApp.get_n_windows() > 1 && this._skip_app_with_multiple_windows) {
const app_id = shellApp.get_id();
if (this.whitelist.includes(app_id)) {
this._log.debug(`${shellApp.get_name()} / ${app_id} in the whitelist.`);
this._log.debug(`${shellApp.get_name()} (${app_id}) in the whitelist. Closing it anyway.`);
return false;
}
return true;
Expand All @@ -61,4 +282,4 @@ var CloseSession = class {
}
}

}
}
12 changes: 12 additions & 0 deletions constants.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
/**
* GdkModifierType
*
* See: https://gitlab.gnome.org/GNOME/gtk/blob/d726ecdb5d1ece870585c7be89eb6355b2482544/gdk/gdkenums.h:L74
*/
var GDK_SHIFT_MASK = 1 << 0;
var GDK_CONTROL_MASK = 1 << 2;
var GDK_ALT_MASK = 1 << 3;

var GDK_META_MASK = 1 << 28;


26 changes: 26 additions & 0 deletions dbus-interfaces/org.freedesktop.login1.Manager.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<node>
<interface name="org.freedesktop.login1.Manager">
<signal name="UserNew">
<arg type="u" direction="out"/>
<arg type="o" direction="out"/>
</signal>
<signal name="SessionRemoved">
<arg type="s" direction="out"/>
<arg type="o" direction="out"/>
</signal>
<signal name="UserRemoved">
<arg type="u" direction="out"/>
<arg type="o" direction="out"/>
</signal>
<signal name="PrepareForShutdown">
<arg type="b" direction="out"/>
</signal>
<method name="Inhibit">
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="s" direction="in"/>
<arg type="h" direction="out"/>
</method>
</interface>
</node>
Loading

0 comments on commit eb73d0e

Please sign in to comment.