Fix title matching + add wildcards

master
Raheman Vaiya 4 years ago
parent 06b39432fb
commit ac27378217
  1. 2
      README.md
  2. BIN
      keyd.1.gz
  3. 52
      man.md
  4. 112
      scripts/keyd-application-mapper

@ -125,7 +125,7 @@ E.G
- Run: - Run:
keyd-application-mapper `keyd-application-mapper`
You will probably want to put `keyd-application-mapper -d` somewhere in your You will probably want to put `keyd-application-mapper -d` somewhere in your
display server initialization logic (e.g ~/.xinitrc) unless you are running Gnome. display server initialization logic (e.g ~/.xinitrc) unless you are running Gnome.

Binary file not shown.

@ -295,20 +295,34 @@ By default expressions apply to the most recently active keyboard.
### Application Support ### Application Support
keyd ships with a python script called `keyd-application-mapper` that keyd ships with a python script called **keyd-application-mapper** which
reads a file called *~/.config/keyd/app.conf* and applies the supplied bindings reads a file called *~/.config/keyd/app.conf* and applies the supplied bindings
whenever a window with a matching class comes into focus. whenever a window with a matching class comes into focus.
The file has the following form: You can think of each section as a set of application specific masks.
[<application class>] The config file has the following form:
[<filter>]
<expression 1> <expression 1>
<expression 2...> <expression 2...>
Where each expression is a valid argument to `-e` (see *Expressions*). Where `<filter>` has one of the following forms:
[<class exp>] # Match by window class
[<class exp>|<title exp>] # Match by class and title
For example: and each `<expression>` is a valid argument to `-e` (see *Expressions*).
`<class exp>` and `<title exp>` are strings which describe window class and title names
to be matched and may optionally contain unix style wildcards (*). For example,
'`[gnome|*find*]`' will match any window with a class of 'gnome' and a title
which contains 'find'.
applied over the global rules defined in `/etc/keyd/*.conf`.
E.G
[kitty] [kitty]
@ -316,9 +330,9 @@ For example:
alt.[ = C-S-tab alt.[ = C-S-tab
alt.t = C-S-t alt.t = C-S-t
[alacritty] [st-*]
alt.1 = macro(Inside space alacritty!) alt.1 = macro(Inside space st)
[chromium] [chromium]
@ -327,22 +341,24 @@ For example:
alt.[ = C-S-tab alt.[ = C-S-tab
alt.t = C-t alt.t = C-t
Will remap `A-1` to the the string 'Inside alacritty' when a window with class Will remap `A-1` to the the string 'Inside st' when a window with a class
`alacritty` is active. You can think of each section as an application specific mask that begins with 'st-' (e.g st-256color) is active.
applied over the default configuration.
Application classes can be obtained by inspecting the log output while the Window class and title names can be obtained by inspecting the log output while the
daemon is running (e.g `tail -f ~/.config/keyd/app.log`). daemon is running (e.g `tail -f ~/.config/keyd/app.log`).
If the script is run under Gnome, the extension will manage the lifecycle of
the application remapper after it has been run for the first time. Otherwise
you will need to put `keyd-application-mapper -d` somewhere in your display
server initialization logic (e.g ~/.xinitrc or ~/.xsession)
At the moment X, Sway and Gnome are supported. At the moment X, Sway and Gnome are supported.
Note: In order for this to work, keyd must be running and the user must have access to #### Installation
*/var/run/keyd.socket* (i.e be a member of the *keyd* group).
Installation is a simple matter of running the daemon `keyd-application-mapper -d`
somewhere in your display server initialization logic (e.g ~/.xinitrc or
~/.xsession). The exception to this is if you are running Gnome, in which case
running `keyd-application-mapper` for the first time will install an extension
which manages the script lifecycle.
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).
### A note on security ### A note on security

@ -7,6 +7,7 @@ import shutil
import re import re
import sys import sys
import fcntl import fcntl
from fnmatch import fnmatch
# Good enough for now :/. # Good enough for now :/.
@ -31,6 +32,9 @@ def assert_env(var):
if not os.getenv(var): if not os.getenv(var):
raise Exception(f'Missing environment variable {var}') raise Exception(f'Missing environment variable {var}')
def run(cmd):
return subprocess.check_output(['/bin/sh', '-c', cmd]).decode('utf8')
def run_or_die(cmd, msg=''): def run_or_die(cmd, msg=''):
rc = subprocess.run(['/bin/sh', '-c', cmd], rc = subprocess.run(['/bin/sh', '-c', cmd],
stdout=subprocess.DEVNULL, stdout=subprocess.DEVNULL,
@ -40,16 +44,23 @@ def run_or_die(cmd, msg=''):
die(msg) die(msg)
def parse_config(path): def parse_config(path):
map = {} config = []
for line in open(path): for line in open(path):
line = line.strip() line = line.strip()
if line.startswith('[') and line.endswith(']'): if line.startswith('[') and line.endswith(']'):
window_identifier = line[1:-1] a = line[1:-1].split('|')
if len(a) < 2:
cls = a[0]
title = '*'
else:
cls = a[0]
title = a[1]
bindings = [] bindings = []
map[window_identifier] = bindings config.append((cls, title, bindings))
elif line == '': elif line == '':
continue continue
elif line.startswith('#'): elif line.startswith('#'):
@ -57,7 +68,7 @@ def parse_config(path):
else: else:
bindings.append(line) bindings.append(line)
return map return config
class SwayMonitor(): class SwayMonitor():
def __init__(self, on_window_change): def __init__(self, on_window_change):
@ -71,6 +82,8 @@ class SwayMonitor():
def run(self): def run(self):
import json import json
import subprocess import subprocess
last_cls = ''
last_title = ''
swayproc = subprocess.Popen( swayproc = subprocess.Popen(
['swaymsg', ['swaymsg',
@ -83,14 +96,27 @@ class SwayMonitor():
for ev in swayproc.stdout: for ev in swayproc.stdout:
data = json.loads(ev) data = json.loads(ev)
title = ''
cls = ''
try: try:
if data['container']['focused'] == True: if data['container']['focused'] == True:
props = data['container']['window_properties'] props = data['container']['window_properties']
self.on_window_change(props['class'], props['title'])
cls = props['class']
title = props['title']
except: except:
title = ''
cls = data['container']['app_id'] cls = data['container']['app_id']
self.on_window_change(cls, "")
pass if title == '' and cls == '':
continue
if last_cls != cls or last_title != title:
last_cls = cls
last_title = title
self.on_window_change(cls, title)
class XMonitor(): class XMonitor():
@ -109,21 +135,29 @@ class XMonitor():
def run(self): def run(self):
last_active_class = "" last_active_class = ""
last_active_name = "" last_active_title = ""
WM_NAME = self.dpy.intern_atom('WM_NAME')
while True: while True:
self.dpy.next_event() self.dpy.next_event()
try: try:
wm_class = self.dpy.get_input_focus().focus.get_wm_class() win = self.dpy.get_input_focus().focus
if wm_class == None: classes = win.get_wm_class()
title = win.get_full_property(WM_NAME, 0).value.decode('utf8')
if classes == None:
cls = 'root' cls = 'root'
else: else:
cls = wm_class[1] cls = classes[1]
if cls != last_active_class: if cls != last_active_class or title != last_active_title:
last_active_class = cls last_active_class = cls
self.on_window_change(cls) last_active_title = title
self.on_window_change(cls, title)
except: except:
pass pass
@ -134,7 +168,7 @@ class GnomeMonitor():
self.on_window_change = on_window_change self.on_window_change = on_window_change
self.version = '1' self.version = '1.1'
self.extension_dir = os.getenv('HOME') + '/.local/share/gnome-shell/extensions/keyd' self.extension_dir = os.getenv('HOME') + '/.local/share/gnome-shell/extensions/keyd'
self.fifo_path = self.extension_dir + '/keyd.fifo' self.fifo_path = self.extension_dir + '/keyd.fifo'
@ -172,7 +206,9 @@ class GnomeMonitor():
Shell.WindowTracker.get_default().connect('notify::focus-app', () => { Shell.WindowTracker.get_default().connect('notify::focus-app', () => {
const win = global.display.focus_window; const win = global.display.focus_window;
const cls = win ? win.get_wm_class() : 'root'; const cls = win ? win.get_wm_class() : 'root';
send(`${cls}\n`); const title = win ? win.get_title() : '';
send(`${cls}\\t${title}\\n`);
}); });
return { return {
@ -207,16 +243,21 @@ class GnomeMonitor():
if not self._is_installed(): if not self._is_installed():
print('keyd extension not found, installing...') print('keyd extension not found, installing...')
self._install() self._install()
print('Success! Please restart Gnome.') run_or_die('gsettings set org.gnome.shell disable-user-extensions false');
print('Success! Please restart Gnome and rerun this script one more time.')
exit(0) exit(0)
run_or_die('gsettings set org.gnome.shell disable-user-extensions false'); if 'DISABLED' in run('gnome-extensions show keyd'):
run_or_die('gnome-extensions enable keyd', 'Failed to enable keyd extension.') run_or_die('gnome-extensions enable keyd', 'Failed to enable keyd extension.')
print(f'Successfully enabled keyd extension :). Output will be stored in {LOGFILE}')
exit(0)
def run(self): def run(self):
for line in open(self.fifo_path): for line in open(self.fifo_path):
[cls, name] = line.strip().split('\t') (cls, title) = line.strip('\n').split('\t')
self.on_window_change(cls, name)
self.on_window_change(cls, title)
def get_monitor(on_window_change): def get_monitor(on_window_change):
monitors = [ monitors = [
@ -269,31 +310,42 @@ args = opt.parse_args()
if not os.path.exists(CONFIG_PATH): if not os.path.exists(CONFIG_PATH):
die('could not find app.conf, make sure it is in ~/.config/keyd/app.conf') die('could not find app.conf, make sure it is in ~/.config/keyd/app.conf')
map = parse_config(CONFIG_PATH) config = parse_config(CONFIG_PATH)
ping_keyd() ping_keyd()
lock() lock()
def normalize_identifier(s): def lookup_bindings(cls, title):
return re.sub('[^A-Za-z0-9]', '-', s).strip('-').lower() for cexp, texp, bindings in config:
if fnmatch(cls, cexp) and fnmatch(title, texp):
return bindings
return []
def normalize_class(s):
return re.sub('[^A-Za-z0-9]+', '-', s).strip('-').lower()
def normalize_title(s):
return re.sub('[^A-Za-z0-9@:/?.]+', '-', s).strip('-').lower()
last_mtime = os.path.getmtime(CONFIG_PATH) last_mtime = os.path.getmtime(CONFIG_PATH)
def on_window_change(cls, name): def on_window_change(cls, title):
global last_mtime global last_mtime
global map global config
cls = normalize_class(cls)
title = normalize_title(title)
cls = normalize_identifier(cls)
name = normalize_identifier(name)
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...')
map = parse_config(CONFIG_PATH) config = parse_config(CONFIG_PATH)
last_mtime = mtime last_mtime = mtime
if not args.quiet: if not args.quiet:
print(f'Active window class: {cls}') print(f'Active window: {cls}|{title}')
bindings = map.get(cls, []) bindings = lookup_bindings(cls, title)
subprocess.run(['keyd', '-e', 'reset', *bindings]) subprocess.run(['keyd', '-e', 'reset', *bindings])

Loading…
Cancel
Save