From 0430dc502b3f83237004d830b371b82510b72dfe Mon Sep 17 00:00:00 2001 From: Raheman Vaiya Date: Sun, 9 Jan 2022 20:53:07 -0500 Subject: [PATCH] Improve application support --- CHANGELOG.md | 6 + README.md | 68 ++++---- keyd.1.gz | Bin 5861 -> 5918 bytes man.md | 49 +++--- scripts/keyd-application-mapper | 296 ++++++++++++++++++++------------ 5 files changed, 257 insertions(+), 162 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ef7db5..4a8c70e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,9 @@ +# v2.2.1-beta + + - Moved application bindings into ~/.config/keyd/app.conf. + - Added -d to keyd-application-mapper. + - Fixed broken gnome support. + # v2.2.0-beta - Added a new IPC mechanism for dynamically altering the keymap (-e). diff --git a/README.md b/README.md index 2296c4e..878575e 100644 --- a/README.md +++ b/README.md @@ -35,7 +35,7 @@ Some of the more interesting ones include: - Keyboard specific configuration. - Instantaneous remapping (no more flashing :)). - A client-server model that facilitates scripting and display server agnostic application remapping. (Currently ships with support for X, sway, and gnome). -- System wide config (work in a VT) +- System wide config (works in a VT) - First class support for modifier overloading. ### keyd is for people who: @@ -75,6 +75,33 @@ Some of the more interesting ones include: make && sudo make install sudo systemctl enable keyd && sudo systemctl start keyd +# Quickstart + +1. Install keyd + +2. Put the following in `/etc/keyd/default.conf`: + +``` +[ids] + +* + +[main] + +# Maps capslock to escape when pressed and control when held. +capslock = overload(control, esc) + +# Remaps the escape key to capslock +esc = capslock +``` + +3. Run `sudo systemctl restart keyd` to reload the config file. + +4. See the [man page](man.md) for a comprehensive list of config options. + +*Note*: It is possible to render your machine unusable with a bad config file. +Should you find yourself in this position, the special key sequence +`backspace+backslash+enter` should cause keyd to terminate. ## Application Specific Remapping (experimental) @@ -82,25 +109,28 @@ Some of the more interesting ones include: usermod -aG keyd -- Populate `~/.keyd-mappings`: +- Populate `~/.config/keyd/app.conf`: E.G - [Alacritty] + [alacritty] alt.] = macro(C-g n) alt.[ = macro(C-g p) - [Chromium] + [chromium] alt.[ = C-S-tab alt.] = macro(C-tab) - Run: + keyd-application-mapper +You will probably want to put `keyd-application-mapper -d` in your +initialization. -Class names are discoverable with `keyd-application-mapper -m`. +Window class names are discoverable with `keyd-application-mapper -m`. See the man page for more details. ## SBC support @@ -120,34 +150,6 @@ members, no personal responsibility is taken for them. [AUR](https://aur.archlinux.org/packages/keyd-git/) package maintained by eNV25. -# Quickstart - -1. Install keyd - -2. Put the following in `/etc/keyd/default.conf`: - -``` -[ids] - -* - -[main] - -# Maps capslock to escape when pressed and control when held. -capslock = overload(control, esc) - -# Remaps the escape key to capslock -esc = capslock -``` - -3. Run `sudo systemctl restart keyd` to reload the config file. - -4. See the [man page](man.md) for a comprehensive list of config options. - -*Note*: It is possible to render your machine unusable with a bad config file. -Should you find yourself in this position, the special key sequence -`backspace+backslash+enter` should cause keyd to terminate. - # Sample Config [ids] diff --git a/keyd.1.gz b/keyd.1.gz index 1d5167f86b82d93df1ccf67e056288f8cd42bd92..6ae9186bd792198ae456ceeb18a27f40df0ff95d 100644 GIT binary patch literal 5918 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 diff --git a/man.md b/man.md index 234ada5..d71a862 100644 --- a/man.md +++ b/man.md @@ -6,7 +6,7 @@ # SYNOPSIS -**keyd** [options] +**keyd** \[options\] # OPTIONS @@ -243,9 +243,8 @@ unless they are doing something unorthodox (e.g nesting hybrid layers). ## IPC -To facilitate extensibility, keyd employs a client-server model. Thus the -keymap can be conceived of as a 'living entity' that can be modified at -runtime. +To facilitate extensibility keyd employs a client-server model. The keymap can +thus be conceived of as a 'living entity' that can be modified at run time. In addition to allowing the user to try new bindings on the fly, this enables the user to fully leverage keyd's expressive power from other programs @@ -268,7 +267,7 @@ The `-e` flag accepts one or more *expressions*, each of which must have one of Where `` is the name of an (existing) layer in which the key is to be bound. -As a special case an expression may be the string 'reset' in which case the +As a special case, an expression may be the string 'reset', in which case the current keymap will revert to its original state (all dynamically applied bindings will be dropped). @@ -282,9 +281,9 @@ By default expressions apply to the most recently active keyboard. ### Application Support -keyd ships with a python script called `keyd-application-mapper` which -reads a file called *~/.keyd-mappings* and applies the supplied mappings -whenever a window of the relevant class comes into focus. +keyd ships with a python script called `keyd-application-mapper` that +reads a file called *~/.config/keyd/app.conf* and applies the supplied bindings +whenever a window with a matching class comes into focus. The file has the following form: @@ -293,37 +292,43 @@ The file has the following form: -Where each expression is a valid argument to `-e`. +Where each expression is a valid argument to `-e` (see *Expressions*). For example: - [Alacritty] + [kitty] - control.1 = macro(Inside space alacritty) + control.1 = macro(Inside space kitty!) + + [alacritty] + + control.1 = macro(Inside space alacritty!) [chromium] - control.1 = macro(Inside space chrome) + control.1 = macro(Inside space chrome!) Will remap `C-1` to the the string 'Inside alacritty' when a window with class -'Alacritty' is active and 'Inside chrome' when a window with class 'chromium' -is active. +`alacritty` is active. -Application classes can be obtained by running `keyd-application-mapper -m`. -At the moment X, sway and gnome are supported. +In order for this to work keyd must be running and the user must have access to +*/var/run/keyd.socket* (i.e be a member of the *keyd* group). Application +classes can be obtained with `keyd-application-mapper -m`. -In order for this script to work the user must have access to */var/run/keyd.socket* -(i.e be a member of the *keyd* group). +You will probably want to put `keyd-application-mapper -d` somewhere in the +initialization path of your display server (e.g `~/.xinitrc`). + +At the moment X, sway and gnome are supported. ### A note on security Any user which can interact with a program that directly controls input devices should be assumed to have full access to the system. -While keyd is slightly better at providing some degree of isolation than other -remappers (by dint of mediating access through an IPC mechanism rather than -granting users blanket access to /dev/input/* and /dev/uinput), it still -provides the opportunity for abuse and should be treated with due deference. +While keyd offers slightly better isolation compared to other remappers by dint +of mediating access through an IPC mechanism (rather than granting users +blanket access to /dev/input/* and /dev/uinput), it still provides an +opportunity for abuse and should be treated with due deference. Specifically, access to */var/run/keyd.socket* should only be granted to trusted users and the group `keyd` should be regarded with the same reverence diff --git a/scripts/keyd-application-mapper b/scripts/keyd-application-mapper index 47c1784..a5e5a8a 100755 --- a/scripts/keyd-application-mapper +++ b/scripts/keyd-application-mapper @@ -3,17 +3,40 @@ import subprocess import argparse import os +import re +import sys +import fcntl # Good enough for now :/. -# TODO(ish): +# TODO(ish): # -# Make assorted detection hacks cleaner. +# Make assorted detection hacks cleaner. # Profile and optimize. # Consider reimplmenting in perl or C. # Produce more useful error messages :P. -CONFIG_PATH = os.getenv('HOME') + '/.keyd-mappings' +CONFIG_PATH = os.getenv('HOME')+'/.config/keyd/app.conf' +LOCKFILE = os.getenv('HOME')+'/.config/keyd/lockfile' +LOGFILE = os.getenv('HOME')+'/.config/keyd/log' + +def die(msg): + sys.stderr.write('ERROR: ') + sys.stderr.write(msg) + sys.stderr.write('\n') + exit(0) + +def assert_env(var): + if not os.getenv(var): + raise Exception(f'Missing environment variable {var}') + +def run_or_die(cmd, msg=''): + rc = subprocess.run(['/bin/sh', '-c', cmd], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL).returncode + + if rc != 0: + die(msg) def parse_config(path): map = {} @@ -35,15 +58,14 @@ def parse_config(path): return map - -class SwayWindowChangeDetector(): +class SwayMonitor(): def __init__(self, on_window_change): - import os + assert_env('SWAYSOCK') self.on_window_change = on_window_change - if not os.getenv('SWAYSOCK'): - raise Exception('SWAYSOCK not found, is sway running?') + def init(self): + pass def run(self): import json @@ -51,11 +73,11 @@ class SwayWindowChangeDetector(): swayproc = subprocess.Popen( ['swaymsg', - '--type', - 'subscribe', - '--monitor', - '--raw', - '["window"]'], stdout=subprocess.PIPE) + '--type', + 'subscribe', + '--monitor', + '--raw', + '["window"]'], stdout=subprocess.PIPE) for ev in swayproc.stdout: data = json.loads(ev) @@ -63,141 +85,201 @@ class SwayWindowChangeDetector(): try: if data['container']['focused'] == True: cls = data['container']['window_properties']['class'] - self.on_window_change([cls]) + self.on_window_change(cls) except: - self.on_window_change([data['container']['app_id']]) + cls = data['container']['app_id'] + self.on_window_change(cls) pass -class XWindowChangeDetector(): +class XMonitor(): def __init__(self, on_window_change): - import os + assert_env('DISPLAY') self.on_window_change = on_window_change - if not os.getenv('DISPLAY'): - raise Exception('DISPLAY not set, is X running?') - - # TODO: make this less kludgy - def run(self): - import time + def init(self): import Xlib import Xlib.display - dpy = Xlib.display.Display() - - class_cache = {} - - def get_class(win): - hsh = str(win) - if hsh not in class_cache: - try: - cls = win.get_wm_class() - if not cls: - return [] - class_cache[hsh] = cls - except: - return [] - - return class_cache[hsh] + self.dpy = Xlib.display.Display() + self.dpy.screen().root.change_attributes( + event_mask = Xlib.X.SubstructureNotifyMask|Xlib.X.PropertyChangeMask) - last = [] + def run(self): + last_active_class = "" while True: - win = dpy.get_input_focus().focus - - classes = get_class(win) - - if classes != last: - self.on_window_change(classes) - - last = classes - time.sleep(0.1) - + self.dpy.next_event() -class GnomeWindowChangeDetector(): - def __init__(self, on_window_change): - import dbus - import dbus.mainloop.glib + try: + cls = self.dpy.get_input_focus().focus.get_wm_class()[1] + if cls != last_active_class: + last_active_class = cls + self.on_window_change(cls) + except: + import traceback + traceback.print_exc() + pass - dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) - self.con = dbus.SessionBus() +# :( +class GnomeMonitor(): + def __init__(self, on_window_change): + assert_env('GNOME_SETUP_DISPLAY') - self.introspect = self.get_dbus_object("org.gnome.Shell.Introspect", - "/org/gnome/Shell/Introspect") + self.on_window_change = on_window_change - self.shell = self.get_dbus_object( - "org.gnome.Shell", "/org/gnome/Shell") + self.extension_dir = os.getenv('HOME') + '/.local/share/gnome-shell/extensions/keyd' + self.fifo_path = self.extension_dir + '/keyd.fifo' + + def _install(self): + os.makedirs(self.extension_dir, exist_ok=True) + + extension = ''' + const Shell = imports.gi.Shell; + + // We have to keep an explicit reference around to prevent garbage collection :/. + let file = imports.gi.Gio.File.new_for_path('%s'); + let pipe = file.append_to_async(0, 0, null, on_pipe_open); + + function send(msg) { + if (!pipe) + return; + + try { + pipe.write(msg, null); + } catch { + log('pipe closed, reopening...'); + pipe = null; + file.append_to_async(0, 0, null, on_pipe_open); + } + } + + function on_pipe_open(file, res) { + log('pipe opened'); + pipe = file.append_to_finish(res); + } + + function init() { + Shell.WindowTracker.get_default().connect('notify::focus-app', () => { + send(`${global.display.focus_window.get_wm_class()}\n`); + }); + + return { enable: ()=>{}, disable: ()=>{} }; + } + ''' % (self.fifo_path) + + metadata = ''' + { + "name": "keyd", + "description": "Used by keyd to obtain active window information.", + "uuid": "keyd", + "shell-version": [ "41" ] + } + ''' + + open(self.extension_dir + '/metadata.json', 'w').write(metadata) + open(self.extension_dir + '/extension.js', 'w').write(extension) + os.mkfifo(self.fifo_path) + + def init(self): + if not os.path.exists(self.extension_dir): + print('keyd extension not found, installing...') + self._install() + print('Success! Please restart Gnome and rerun this script.') + exit(0) + + run_or_die('gsettings set org.gnome.shell disable-user-extensions false'); + run_or_die('gnome-extensions enable keyd', 'Failed to enable keyd extension.') - self.on_window_change = on_window_change - self.introspect.connect_to_signal( - "RunningApplicationsChanged", lambda: self._on_window_change()) + def run(self): + for cls in open(self.fifo_path): + cls = cls.strip() + self.on_window_change(cls) + +def get_monitor(on_window_change): + monitors = [ + ('Sway', SwayMonitor), + ('Gnome', GnomeMonitor), + ('X', XMonitor), + ] - def get_dbus_object(self, interface, path): - import dbus - return dbus.Interface(self.con.get_object(interface, path), interface) + for name, mon in monitors: + try: + m = mon(on_window_change) + print(f'{name} detected') + return m + except: + pass - def get_window_class(self): - return self.shell.Eval('global.display.focus_window.get_wm_class()')[1].strip('"') + print('Could not detect app environment :(.') + sys.exit(-1) - def _on_window_change(self): - self.on_window_change(self.get_window_class()) +def lock(): + global lockfh + lockfh = open(LOCKFILE, 'w') + try: + fcntl.flock(lockfh, fcntl.LOCK_EX | fcntl.LOCK_NB) + except: + die('only one instance may run at a time') - def run(self): - import dbus - from gi.repository import GLib +def ping_keyd(): + run_or_die('keyd -e ping', +'could not connect to keyd instance, make sure it is running and you are a member of `keyd`') - loop = GLib.MainLoop() - loop.run() +def daemonize(): + print('Daemonizing...') + fh = open(LOGFILE, 'w') -def get_detector(on_window_change): - detectors = [ - ('Sway', SwayWindowChangeDetector), - ('Gnome', GnomeWindowChangeDetector), - ('X', XWindowChangeDetector), - ] + os.close(1) + os.close(2) + os.dup2(fh.fileno(), 1) + os.dup2(fh.fileno(), 2) - for name,detector in detectors: - try: - d = detector(on_window_change) - print(f'{name} detected') - return d - except: - pass + if os.fork(): exit(0) + if os.fork(): exit(0) - print('Could not detect app environment :(.') - exit(-1) +opt = argparse.ArgumentParser() +opt.add_argument('-m', '--monitor', default=False, action='store_true', help='print window class names in real time') +opt.add_argument('-d', '--daemonize', default=False, action='store_true', help='fork and run in the background') +args = opt.parse_args() +if not os.path.exists(CONFIG_PATH): + die('could not find app.conf, make sure it is in ~/.config/keyd/app.conf') -parser = argparse.ArgumentParser() -parser.add_argument('-m', '--monitor', default=False, action='store_true') -args = parser.parse_args() +bindings = parse_config(CONFIG_PATH) +ping_keyd() +lock() -if subprocess.run(['keyd', '-e', 'ping'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: - print('Could not connect to keyd instance, make sure it is running and you are a member of `keyd`') - exit(-1) +def normalize_class(s): + return re.sub('[^A-Za-z0-9]', '-', s).strip('-').lower() -app_bindings = parse_config(CONFIG_PATH) last_mtime = os.path.getmtime(CONFIG_PATH) - -def on_window_change(classes): +def on_window_change(cls): global last_mtime - global app_bindings + global bindings + cls = normalize_class(cls) mtime = os.path.getmtime(CONFIG_PATH) if mtime != last_mtime: print(CONFIG_PATH + ': Updated, reloading config...') - app_bindings = parse_config(CONFIG_PATH) + bindings = parse_config(CONFIG_PATH) last_mtime = mtime if args.monitor: - print(', '.join(classes)) + print(cls) return - for cls in classes: - if cls in app_bindings: - # Apply the bindings. - subprocess.run(['keyd', '-e', *app_bindings[cls]]) + if cls in bindings: + # Apply the bindings. + subprocess.run(['keyd', '-e', *bindings[cls]]) + + +mon = get_monitor(on_window_change) +mon.init() + +if args.daemonize: + daemonize() -get_detector(on_window_change).run() +mon.run()