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:
keyd-application-mapper
`keyd-application-mapper`
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.

Binary file not shown.

@ -295,20 +295,34 @@ By default expressions apply to the most recently active keyboard.
### 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
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 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]
@ -316,9 +330,9 @@ For example:
alt.[ = C-S-tab
alt.t = C-S-t
[alacritty]
[st-*]
alt.1 = macro(Inside space alacritty!)
alt.1 = macro(Inside space st)
[chromium]
@ -327,22 +341,24 @@ For example:
alt.[ = C-S-tab
alt.t = C-t
Will remap `A-1` to the the string 'Inside alacritty' when a window with class
`alacritty` is active. You can think of each section as an application specific mask
applied over the default configuration.
Will remap `A-1` to the the string 'Inside st' when a window with a class
that begins with 'st-' (e.g st-256color) is active.
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`).
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.
Note: 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).
#### Installation
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

@ -7,6 +7,7 @@ import shutil
import re
import sys
import fcntl
from fnmatch import fnmatch
# Good enough for now :/.
@ -31,6 +32,9 @@ def assert_env(var):
if not os.getenv(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=''):
rc = subprocess.run(['/bin/sh', '-c', cmd],
stdout=subprocess.DEVNULL,
@ -40,16 +44,23 @@ def run_or_die(cmd, msg=''):
die(msg)
def parse_config(path):
map = {}
config = []
for line in open(path):
line = line.strip()
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 = []
map[window_identifier] = bindings
config.append((cls, title, bindings))
elif line == '':
continue
elif line.startswith('#'):
@ -57,7 +68,7 @@ def parse_config(path):
else:
bindings.append(line)
return map
return config
class SwayMonitor():
def __init__(self, on_window_change):
@ -71,6 +82,8 @@ class SwayMonitor():
def run(self):
import json
import subprocess
last_cls = ''
last_title = ''
swayproc = subprocess.Popen(
['swaymsg',
@ -83,14 +96,27 @@ class SwayMonitor():
for ev in swayproc.stdout:
data = json.loads(ev)
title = ''
cls = ''
try:
if data['container']['focused'] == True:
props = data['container']['window_properties']
self.on_window_change(props['class'], props['title'])
cls = props['class']
title = props['title']
except:
title = ''
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():
@ -109,21 +135,29 @@ class XMonitor():
def run(self):
last_active_class = ""
last_active_name = ""
last_active_title = ""
WM_NAME = self.dpy.intern_atom('WM_NAME')
while True:
self.dpy.next_event()
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'
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
self.on_window_change(cls)
last_active_title = title
self.on_window_change(cls, title)
except:
pass
@ -134,7 +168,7 @@ class GnomeMonitor():
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.fifo_path = self.extension_dir + '/keyd.fifo'
@ -172,7 +206,9 @@ class GnomeMonitor():
Shell.WindowTracker.get_default().connect('notify::focus-app', () => {
const win = global.display.focus_window;
const cls = win ? win.get_wm_class() : 'root';
send(`${cls}\n`);
const title = win ? win.get_title() : '';
send(`${cls}\\t${title}\\n`);
});
return {
@ -207,16 +243,21 @@ class GnomeMonitor():
if not self._is_installed():
print('keyd extension not found, installing...')
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)
run_or_die('gsettings set org.gnome.shell disable-user-extensions false');
run_or_die('gnome-extensions enable keyd', 'Failed to enable keyd extension.')
if 'DISABLED' in run('gnome-extensions show keyd'):
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):
for line in open(self.fifo_path):
[cls, name] = line.strip().split('\t')
self.on_window_change(cls, name)
(cls, title) = line.strip('\n').split('\t')
self.on_window_change(cls, title)
def get_monitor(on_window_change):
monitors = [
@ -269,31 +310,42 @@ 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')
map = parse_config(CONFIG_PATH)
config = parse_config(CONFIG_PATH)
ping_keyd()
lock()
def normalize_identifier(s):
return re.sub('[^A-Za-z0-9]', '-', s).strip('-').lower()
def lookup_bindings(cls, title):
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)
def on_window_change(cls, name):
def on_window_change(cls, title):
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)
if mtime != last_mtime:
print(CONFIG_PATH + ': Updated, reloading config...')
map = parse_config(CONFIG_PATH)
config = parse_config(CONFIG_PATH)
last_mtime = mtime
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])

Loading…
Cancel
Save