Improve application support

master
Raheman Vaiya 4 years ago
parent 6c5a75f3be
commit 0430dc502b
  1. 6
      CHANGELOG.md
  2. 68
      README.md
  3. BIN
      keyd.1.gz
  4. 49
      man.md
  5. 274
      scripts/keyd-application-mapper

@ -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 # v2.2.0-beta
- Added a new IPC mechanism for dynamically altering the keymap (-e). - Added a new IPC mechanism for dynamically altering the keymap (-e).

@ -35,7 +35,7 @@ Some of the more interesting ones include:
- Keyboard specific configuration. - Keyboard specific configuration.
- Instantaneous remapping (no more flashing :)). - 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). - 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. - First class support for modifier overloading.
### keyd is for people who: ### keyd is for people who:
@ -75,6 +75,33 @@ Some of the more interesting ones include:
make && sudo make install make && sudo make install
sudo systemctl enable keyd && sudo systemctl start keyd 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) ## Application Specific Remapping (experimental)
@ -82,25 +109,28 @@ Some of the more interesting ones include:
usermod -aG keyd <user> usermod -aG keyd <user>
- Populate `~/.keyd-mappings`: - Populate `~/.config/keyd/app.conf`:
E.G E.G
[Alacritty] [alacritty]
alt.] = macro(C-g n) alt.] = macro(C-g n)
alt.[ = macro(C-g p) alt.[ = macro(C-g p)
[Chromium] [chromium]
alt.[ = C-S-tab alt.[ = C-S-tab
alt.] = macro(C-tab) alt.] = macro(C-tab)
- Run: - Run:
keyd-application-mapper 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. See the man page for more details.
## SBC support ## 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. [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 # Sample Config
[ids] [ids]

Binary file not shown.

@ -6,7 +6,7 @@
# SYNOPSIS # SYNOPSIS
**keyd** [options] **keyd** \[options\]
# OPTIONS # OPTIONS
@ -243,9 +243,8 @@ unless they are doing something unorthodox (e.g nesting hybrid layers).
## IPC ## IPC
To facilitate extensibility, keyd employs a client-server model. Thus the To facilitate extensibility keyd employs a client-server model. The keymap can
keymap can be conceived of as a 'living entity' that can be modified at thus be conceived of as a 'living entity' that can be modified at run time.
runtime.
In addition to allowing the user to try new bindings on the fly, this 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 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 `<layer>` is the name of an (existing) layer in which the key is to be bound. Where `<layer>` 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 current keymap will revert to its original state (all dynamically applied
bindings will be dropped). bindings will be dropped).
@ -282,9 +281,9 @@ By default expressions apply to the most recently active keyboard.
### Application Support ### Application Support
keyd ships with a python script called `keyd-application-mapper` which keyd ships with a python script called `keyd-application-mapper` that
reads a file called *~/.keyd-mappings* and applies the supplied mappings reads a file called *~/.config/keyd/app.conf* and applies the supplied bindings
whenever a window of the relevant class comes into focus. whenever a window with a matching class comes into focus.
The file has the following form: The file has the following form:
@ -293,37 +292,43 @@ The file has the following form:
<expression 1> <expression 1>
<expression 2...> <expression 2...>
Where each expression is a valid argument to `-e`. Where each expression is a valid argument to `-e` (see *Expressions*).
For example: For example:
[Alacritty] [kitty]
control.1 = macro(Inside space alacritty) control.1 = macro(Inside space kitty!)
[alacritty]
control.1 = macro(Inside space alacritty!)
[chromium] [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 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' `alacritty` is active.
is active.
Application classes can be obtained by running `keyd-application-mapper -m`. In order for this to work keyd must be running and the user must have access to
At the moment X, sway and gnome are supported. */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* You will probably want to put `keyd-application-mapper -d` somewhere in the
(i.e be a member of the *keyd* group). initialization path of your display server (e.g `~/.xinitrc`).
At the moment X, sway and gnome are supported.
### A note on security ### A note on security
Any user which can interact with a program that directly controls input devices Any user which can interact with a program that directly controls input devices
should be assumed to have full access to the system. should be assumed to have full access to the system.
While keyd is slightly better at providing some degree of isolation than other While keyd offers slightly better isolation compared to other remappers by dint
remappers (by dint of mediating access through an IPC mechanism rather than of mediating access through an IPC mechanism (rather than granting users
granting users blanket access to /dev/input/* and /dev/uinput), it still blanket access to /dev/input/* and /dev/uinput), it still provides an
provides the opportunity for abuse and should be treated with due deference. opportunity for abuse and should be treated with due deference.
Specifically, access to */var/run/keyd.socket* should only be granted to 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 trusted users and the group `keyd` should be regarded with the same reverence

@ -3,6 +3,9 @@
import subprocess import subprocess
import argparse import argparse
import os import os
import re
import sys
import fcntl
# Good enough for now :/. # Good enough for now :/.
@ -13,7 +16,27 @@ import os
# Consider reimplmenting in perl or C. # Consider reimplmenting in perl or C.
# Produce more useful error messages :P. # 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): def parse_config(path):
map = {} map = {}
@ -35,15 +58,14 @@ def parse_config(path):
return map return map
class SwayMonitor():
class SwayWindowChangeDetector():
def __init__(self, on_window_change): def __init__(self, on_window_change):
import os assert_env('SWAYSOCK')
self.on_window_change = on_window_change self.on_window_change = on_window_change
if not os.getenv('SWAYSOCK'): def init(self):
raise Exception('SWAYSOCK not found, is sway running?') pass
def run(self): def run(self):
import json import json
@ -63,141 +85,201 @@ class SwayWindowChangeDetector():
try: try:
if data['container']['focused'] == True: if data['container']['focused'] == True:
cls = data['container']['window_properties']['class'] cls = data['container']['window_properties']['class']
self.on_window_change([cls]) self.on_window_change(cls)
except: except:
self.on_window_change([data['container']['app_id']]) cls = data['container']['app_id']
self.on_window_change(cls)
pass pass
class XWindowChangeDetector(): class XMonitor():
def __init__(self, on_window_change): def __init__(self, on_window_change):
import os assert_env('DISPLAY')
self.on_window_change = on_window_change self.on_window_change = on_window_change
if not os.getenv('DISPLAY'): def init(self):
raise Exception('DISPLAY not set, is X running?')
# TODO: make this less kludgy
def run(self):
import time
import Xlib import Xlib
import Xlib.display import Xlib.display
dpy = Xlib.display.Display() self.dpy = Xlib.display.Display()
self.dpy.screen().root.change_attributes(
event_mask = Xlib.X.SubstructureNotifyMask|Xlib.X.PropertyChangeMask)
class_cache = {} def run(self):
last_active_class = ""
while True:
self.dpy.next_event()
def get_class(win):
hsh = str(win)
if hsh not in class_cache:
try: try:
cls = win.get_wm_class() cls = self.dpy.get_input_focus().focus.get_wm_class()[1]
if not cls: if cls != last_active_class:
return [] last_active_class = cls
class_cache[hsh] = cls self.on_window_change(cls)
except: except:
return [] import traceback
traceback.print_exc()
return class_cache[hsh] pass
last = []
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)
class GnomeWindowChangeDetector(): # :(
class GnomeMonitor():
def __init__(self, on_window_change): def __init__(self, on_window_change):
import dbus assert_env('GNOME_SETUP_DISPLAY')
import dbus.mainloop.glib
dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
self.con = dbus.SessionBus()
self.introspect = self.get_dbus_object("org.gnome.Shell.Introspect",
"/org/gnome/Shell/Introspect")
self.shell = self.get_dbus_object(
"org.gnome.Shell", "/org/gnome/Shell")
self.on_window_change = on_window_change self.on_window_change = on_window_change
self.introspect.connect_to_signal(
"RunningApplicationsChanged", lambda: self._on_window_change())
def get_dbus_object(self, interface, path):
import dbus
return dbus.Interface(self.con.get_object(interface, path), interface)
def get_window_class(self):
return self.shell.Eval('global.display.focus_window.get_wm_class()')[1].strip('"')
def _on_window_change(self): self.extension_dir = os.getenv('HOME') + '/.local/share/gnome-shell/extensions/keyd'
self.on_window_change(self.get_window_class()) 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.')
def run(self): def run(self):
import dbus for cls in open(self.fifo_path):
from gi.repository import GLib cls = cls.strip()
self.on_window_change(cls)
loop = GLib.MainLoop()
loop.run() def get_monitor(on_window_change):
monitors = [
('Sway', SwayMonitor),
def get_detector(on_window_change): ('Gnome', GnomeMonitor),
detectors = [ ('X', XMonitor),
('Sway', SwayWindowChangeDetector),
('Gnome', GnomeWindowChangeDetector),
('X', XWindowChangeDetector),
] ]
for name,detector in detectors: for name, mon in monitors:
try: try:
d = detector(on_window_change) m = mon(on_window_change)
print(f'{name} detected') print(f'{name} detected')
return d return m
except: except:
pass pass
print('Could not detect app environment :(.') print('Could not detect app environment :(.')
exit(-1) sys.exit(-1)
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')
parser = argparse.ArgumentParser() def ping_keyd():
parser.add_argument('-m', '--monitor', default=False, action='store_true') run_or_die('keyd -e ping',
args = parser.parse_args() 'could not connect to keyd instance, make sure it is running and you are a member of `keyd`')
if subprocess.run(['keyd', '-e', 'ping'], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL).returncode != 0: def daemonize():
print('Could not connect to keyd instance, make sure it is running and you are a member of `keyd`') print('Daemonizing...')
exit(-1)
app_bindings = parse_config(CONFIG_PATH) fh = open(LOGFILE, 'w')
last_mtime = os.path.getmtime(CONFIG_PATH)
os.close(1)
os.close(2)
os.dup2(fh.fileno(), 1)
os.dup2(fh.fileno(), 2)
def on_window_change(classes): if os.fork(): exit(0)
if os.fork(): exit(0)
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')
bindings = parse_config(CONFIG_PATH)
ping_keyd()
lock()
def normalize_class(s):
return re.sub('[^A-Za-z0-9]', '-', s).strip('-').lower()
last_mtime = os.path.getmtime(CONFIG_PATH)
def on_window_change(cls):
global last_mtime global last_mtime
global app_bindings global bindings
cls = normalize_class(cls)
mtime = os.path.getmtime(CONFIG_PATH) mtime = os.path.getmtime(CONFIG_PATH)
if mtime != last_mtime: if mtime != last_mtime:
print(CONFIG_PATH + ': Updated, reloading config...') print(CONFIG_PATH + ': Updated, reloading config...')
app_bindings = parse_config(CONFIG_PATH) bindings = parse_config(CONFIG_PATH)
last_mtime = mtime last_mtime = mtime
if args.monitor: if args.monitor:
print(', '.join(classes)) print(cls)
return return
for cls in classes: if cls in bindings:
if cls in app_bindings:
# Apply the bindings. # Apply the bindings.
subprocess.run(['keyd', '-e', *app_bindings[cls]]) 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()

Loading…
Cancel
Save