{"id":16665014,"url":"https://github.com/snyball/Hawck","last_synced_at":"2025-10-30T17:31:15.267Z","repository":{"id":54803642,"uuid":"140916513","full_name":"snyball/hawck","owner":"snyball","description":"Key-rebinding daemon for Linux (Wayland/X11/Console)","archived":false,"fork":false,"pushed_at":"2024-05-03T15:16:19.000Z","size":10121,"stargazers_count":574,"open_issues_count":20,"forks_count":15,"subscribers_count":16,"default_branch":"master","last_synced_at":"2024-10-31T04:34:57.585Z","etag":null,"topics":["keyboard-shortcuts","lua","wayland"],"latest_commit_sha":null,"homepage":"","language":"C++","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"bsd-2-clause","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/snyball.png","metadata":{"files":{"readme":"README.md","changelog":"changelogs/changelog_0.6.0.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2018-07-14T04:45:09.000Z","updated_at":"2024-10-29T21:51:53.000Z","dependencies_parsed_at":"2024-01-08T09:27:16.489Z","dependency_job_id":"181b488f-c53e-48d7-b082-20d974bb5b51","html_url":"https://github.com/snyball/hawck","commit_stats":null,"previous_names":[],"tags_count":4,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snyball%2Fhawck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snyball%2Fhawck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snyball%2Fhawck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/snyball%2Fhawck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/snyball","download_url":"https://codeload.github.com/snyball/hawck/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":239011366,"owners_count":19567650,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["keyboard-shortcuts","lua","wayland"],"created_at":"2024-10-12T11:01:27.696Z","updated_at":"2025-10-30T17:31:14.534Z","avatar_url":"https://github.com/snyball.png","language":"C++","readme":"\u003cimg src=\"./icons/hawck_logo_v3_with_text.svg\"/\u003e\n\n![License: BSD-2](https://img.shields.io/badge/license-BSD--2-brightgreen.svg)\n![Version: 0.7](https://img.shields.io/badge/version-0.7-blue.svg)\n\n## A key-rebinding daemon\n\nLinux with all it's combinations of window managers, display servers and\ndesktop environments needs a key-rebinding system that works everywhere.\n\nHawck intercepts key presses and lets you write Lua scripts to perform\nactions or modify keys depending on your needs.\n\nYour Lua scripts will work on Wayland, X11, and every WM/DE you throw at\nthem, as well as console ttys.\n\nCommon concrete use cases:\n\n- Rebind caps lock to ctrl or escape:\n\n```lua\nkey \"Caps\" =\u003e replace \"Escape\"\n-- , or\nkey \"Caps\" =\u003e replace \"Control\"\n```\n\n- Conditionally replace caps lock (or any other replacement)\n\n```lua\n-- Pressing F7 will activate the replacement, and pressing F7\n-- again will disable it.\nmode(\"Caps =\u003e Ctrl mode\", down + key \"F7\") =\u003e {\n    key \"Caps\" =\u003e replace \"Control\"\n}\n```\n\n- Replace a key, but only on a specific keyboard (not in `v0.6`, use master branch\n  for this)\n\n```lua\n-- Run `src/tools/lskbd.rb -k` and look for \"ID\" in the output to find keyboard IDs.\nfromkbd \"1:1:AT_Translated_Set_2_keyboard\" + key \"F7\" =\u003e say \"Hello\"\n```\n\n- Paste into a tty, or another program which does not support pasting\n\n```lua\nfunction getClipboard()\n    -- get-clipboard should be replaced with whatever works with your setup.\n    local p = io.popen(\"get-clipboard\")\n    local clip = p:read(\"*a\")\n    p:close()\n    return clip\nend\n\nshift + alt + key \"v\" =\u003e function ()\n  local clip_contents = getClipboard()\n  write(clip_contents)() -- Note the extra parens, write() returns a closure\nend\n```\n\n- Make non-us keyboards more convenient for programming:\n\n```lua\n-- Replace with your respective european characters\nshift + key \"ø\" =\u003e insert \"[\"\nkey \"ø\" =\u003e insert \"{\"\n-- , or\nshift + key \"æ\" =\u003e insert \"]\"\nkey \"æ\" =\u003e insert \"}\"\n```\n\n- Store a common phrase and activate it with a key-binding\n\n```lua\nlocal seals_pasta = \"What the **** did you just say about me you little *****? I'll have you know I graduated top of my class from…\"\nshift + alt + key \"p\" =\u003e write(seals_pasta)\n```\n\n- Run .desktop application actions, and generally launch programs\n\n```lua\nshift + alt + key \"f\" =\u003e app(\"firefox\"):new_window(\"http://man7.org/linux/man-pages/man1/yes.1.html\")\nshift + alt + key \"w\" =\u003e app(\"firefox\"):new_private_window(\"https://www.youtube.com/watch?v=V4MF2s6MLxY\")\n```\n\n\u003csmall\u003e\nThe name is a portmanteau of \"Hack\" and \"AWK\", as the program started out as a\nbit of a hack, and the scripts take inspiration from the pattern-matching style\nof the AWK programming language (plus, hawks are pretty cool I guess.)\n\u003c/small\u003e\n\n## Installation\n\nFor `v0.6`, click Branch\\\u003eTags\\\u003eMaster\\\u003e`v0.6.1` and follow the instructions in\nthe `README` files for that version. The rest of this section will apply to the\nmaster branch.\n\n### Arch/Manjaro\n\nInstall the [hawck-git](https://aur.archlinux.org/packages/hawck-git/) AUR package.\n\nRemember to run `/usr/share/hawck/bin/hawck-user-setup` after install (you\nshould run this script with your personal user account, not as root.)\n\n### Debian/Ubuntu\n\n```bash\n$ git clone --recurse-submodules -j8 https://github.com/snyball/Hawck.git\n$ cd Hawck\n$ pkexec xargs apt -y install \u003c bin/dependencies/debian-deps.txt\n$ ./install.sh\n```\n\nThe user setup script will prompt you for your password, it has to add your user\nto the `hawck-input-share` group. You'll have to log out and back in again after\nyour user has been added to the group (on some distributions, e.g Ubuntu, a\nreboot is required.)\n\nWhen you've started the computer back up again, run the following commands:\n\n### Other distributions\n\nInstall all the dependencies listed on the\n[AUR page](https://aur.archlinux.org/packages/hawck-git/).\nOnce you've found the appropriate packages for your distribution, run the\n`install.sh` script. Note that the install script has only been tested on Ubuntu\n19.10.\n\n## Testing it out\n\nAs Hawck is not able to determine your Wayland/X11 keymap dynamically, you must\nfirst set your keymap in `~/.local/share/hawck/cfg.lua`. This also applies to\nus-keymap users, as the default is stubbornly set to the more uncommon \"no\"\n(norwegian) because the author of Hawck is from there.\n[FAQ: Keyboard Layout](#faq-layout) below.\n\n```lua\n  keymap = \"no\", -- set to \"us\", \"de\", \"no-latin1\", etc. depending on your keyboard layout\n```\n\nNow you can test out a simple script, your `example.hwk` could look like this:\n\n```lua\n-- Pressing C-a should insert the character \"b\"\ndown + ctrl + key \"a\" =\u003e insert \"b\"\n```\n\n\u003e Tip: If you make a mistake, hold Escape and press the Spacebar (ESC-SPC)\n\u003e to deactivate all scripts. Repeat ESC-SPC to reactivate script processing.\n\n```bash\n# Work around issue with ACL (a better solution is WIP)\n$ sudo setfacl -m 'g:uinput:rw' /dev/uinput\n# Start services\n$ sudo systemctl start hawck-inputd\n$ hawck-macrod\n# Create a new script\n$ vim ~/.config/hawck/scripts/example.hwk\n# Whitelist keys from the script\n$ hawck-add ~/.config/hawck/scripts/example.hwk\n# Enable the script (note: macrod specifically requires mode 0755)\n$ chmod +x ~/.config/hawck/scripts/example.hwk\n# Disable the script\n$ chmod -x ~/.config/hawck/scripts/example.hwk\n```\n\n\u003e Warning: When editing example.hwk, the macro daemon will auto-reload, but\n\u003e because of the [security model](#security), new keys will be ignored until\n\u003e you rerun `hawck-add`. You can check what keycodes are intercepted by the\n\u003e input daemon for our example script with:\n\n```sh\n$ cat /var/lib/hawck-input/keys/example.csv\n```\n\n## Security\n\nAs you can imagine, with all this power comes great responsibility. Here's\nan explanation of the strategy used to make things a bit more secure.\n\nWhen Hawck starts up it splits up into two daemons that communicate with\neach other:\n\n- InputD\n  - Runs under the 'hawck-input' user and is part of the input group,\n    letting it read from /dev/input/ devices.\n  - Member of the hawck-uinput group, allowing it to use /dev/uinput\n  - Grabs keyboard input exclusively.\n  - Knows which keys to pass over to the macro daemon\n  - Controls a virtual keyboard that is used to emulate\n    keypresses, this includes re-pressing keys that did\n    not need to be handled by the macro daemon.\n- MacroD\n  - Runs under the desktop user.\n  - Listens for keypresses sent from the keyboard daemon.\n  - Passes received keypresses into Lua scripts in order to\n    perform actions or conditionally modify the keys.\n  - Potential output keys produced by the script are sent\n    back to the keyboard daemon.\n    - Inconsequential implementation details: Having two virtual\n      keyboards operated by both daemons opens up an entirely new\n      can of worms especially for modifier keys.\n\n\u003cimg src=\"./images/arch.svg\"/\u003e\n\nThe keyboard daemon contains a whitelist of keys that the macro daemon\nis allowed to see, this whitelist is derived from the Lua scripts used.\nThis means that the process run under the desktop user never sees all\nkeyboard input, this is important as the `/proc/` filesystem would\nallow any process launched by the user to see all keyboard input if\nit were not filtered.\n\nIf you are using X11 your key-presses can already be intercepted without any\nspecial permission, so all this extra security is unnecessary.\n\nRead the alternative strategies FAQ section below for more information and a\nmore thorough explanation of the drawbacks with the current model.\n\n### Scripting\n\nHawck is scripted with, you guessed it, hawk scripts. These scripts are\nessentially just Lua with some extra operators. The hwk2lua program is\nused to transpile `.hwk` files into `.lua` files (this happens automagically\nif you use Hawck-UI.)\n\nAs an example, here is a hawk script:\n\n```lua\n-- Programming mode is activated by pressing down the f7 key.\n-- It is only run when a key is not being released (-up)\nmode(\"Programming mode\", down + key \"f7\") + -up =\u003e {\n    -- When caps-lock is pressed, substitute with escape\n    key \"caps\" =\u003e insert \"escape\"\n    shift =\u003e {\n        key \"ø\" =\u003e insert \"[\"\n        key \"æ\" =\u003e insert \"]\"\n    }\n    key \"ø\" =\u003e insert \"{\"\n    key \"æ\" =\u003e insert \"}\"\n    -- Write \"Hello \" 10 times using the virtual keyboard,\n    -- then show a \"World!\" notification\n    key \"F12\" =\u003e (write \"Hello \") * 10 .. say \"World!\"\n}\n```\n\nWith this script the `ø` character produces `{`, similarly\nfor the other bindings. But only if `\"Programming mode\"` is enabled.\n\nThis transpiles to the following (without comments):\n\n```lua\n__match[mode(\"Programming mode\", down + key \"f7\") + -up ] = MatchScope.new(function (__match)\n    __match[key \"caps\"] = insert \"escape\"\n    __match[shift] = MatchScope.new(function (__match)\n        __match[key \"oslash\"] = insert \"bracketleft\"\n        __match[key \"ae\"    ] = insert \"bracketright\"\n    end)\n    __match[key \"oslash\"] = insert \"braceleft\"\n    __match[key \"ae\"    ] = insert \"braceright\"\n    __match[key \"F12\"   ] = (write \"Hello \") * 10 .. say \"World!\"\nend)\n```\n\nWhich is what the `hawck-macrod` daemon actually runs. Note\nthat the only differences between plain Lua syntax and the\nadded Hawck-specific syntax is the `=\u003e` operator and how it\nmodifies the behaviour of `{}` braces. Currently the \"transpiler\"\nonly consists of ~100 lines of Python code, the remaining\nsyntactic sugar is achieved using Lua operator overloading\n(see `match.lua` for details.)\n\n\u003csmall\u003e\nWriting DSLs can be fun, but can also be too much fun, resulting\nin an overengineered language requiring a lot of maintenance.\n\nThis is why Lua, with a single extra operator that was\neasy to implement is what I went with in Hawck.\n\u003c/small\u003e\n\nThe lua files are deleted on the fly, but you can open a lua shell with your\nscript loaded for investigation:\n\n```sh\n$ cd ~/.local/share/hawck/scripts/\n$ hwk2lua example.hwk \u003e! tmp.lua\n$ lua -l init -l tmp -i\n$ rm tmp.lua\n```\n\n## FAQ\n\n### Persisting the configuration\n\nThe **input daemon** should autostart, which you can enable/disable with\n\n```sh\n$ sudo systemctl enable hawck-inputd\n```\n\nThe **macro daemon** will not autostart (and your journal will contain\n`hawck-inputd: SystemError: Connection refused`). When you've got everything set\nup correctly, and you understand how it works and how to recover issues, you may\nwant autostart it. Depending on your distribution the provided _desktop_ file\nmight work:\n\n```sh\ncp bin/hawck-macrod.desktop \"${XDG_CONFIG_HOME:-~/.config}\"/autostart\n```\n\n#### \u003ca name=\"faq-layout\"\u003e\u003c/a\u003e Is my keyboard layout supported?\n\nIf you're using a Norwegian or US keyboard, yes.\nIf not, maybe.\n\nIf not you might have to do some tweaking, feel free to report\nan issue on GitHub for your keymap.\n\nThe layout support works like this:\n\n- Because the input daemon runs as root, what it intercepts are the raw kernel\n  keycodes (defined in [/usr/include/linux/input-event-codes.h][1]\n- The numeric keycodes that are listed in\n  _/usr/share/hawck/keymaps/default_linux.lua_ can be used in your scripts eg\n  `key(57)` for the space key (except numbers 1 to 9 which are aliased to the\n  numeric top row keys). Find the keycode for any key by switching to a\n  [Virtual console](https://wiki.archlinux.org/title/Linux_console) and running\n  `showkey`.\n- Using numeric codes is impractical, and using the name the kernel has for the\n  key is confusing as they are not aligned with the key label especially on\n  non-us layouts. This is why Hawck tries to provide localized key names.\n- There are 2 main mechanisms to transform the keycodes the kernel detects into\n  higher-level events like an actual character on screen:\n  1. [Kbd](https://kbd-project.org/), used by the virtual console\n  2. [Xkb](https://www.x.org/wiki/XKB/), used by both X11 and Wayland\n     (through libinput).\n- Both provide a way to define localized keymaps, and Hawck decided to hook into\n  the first, simpler console keymaps to provide support. Note, that their may be\n  discrepancies between the Kbd and Xkb definitions of a given keyboard layout.\n- So Hawck needs to parse a console keymap definition of your layout. It is not\n  necessarily built-into your system, for example on Debian/Ubuntu based distro\n  it is in the `console-data` package.\n- Based on the `keymap = \"foo\"` line _~/.local/share/hawck/cfg.lua_, Hawck will\n  search for a file named `foo.(k)map.gz` in a number of common directories like\n  _/usr/share/keymaps/_ (see [kbmap.getall()][2]).\n- **The symbol names present in those keymap files are the ones that can be\n  used**, with the addition of some [built-in aliases](keymaps/aliases.lua)\n  between unicode characters and key names, so that `insert \"/\"` is able to\n  insert a slash.\n\nFor example, the top-left key (under Esc) on a US keyboard has code 41 according\nto `showkey` and _us.kmap.gz_ has this line:\n\n```\n  keycode  41 = grave            asciitilde\n```\n\nSo `key(\"grave\")` can be used to refer to this key.\n\nIn a lua shell `cd ~/.local/share/hawck/scripts/ ; lua -l init -i`, you can see\nhow the keymap has been parsed with `u.puts(kbd.map)`\n\n[1]: https://github.com/torvalds/linux/blob/master/include/uapi/linux/input-event-codes.h\n[2]: https://github.com/snyball/Hawck/blob/master/src/Lua/Keymap.lua#L247\n\n#### Hotplugging keyboards\n\nWhen `hawck-inputd` starts up it finds the connected keyboards\nthen proceeds to lock them and listen for key events.\n\nYou can then plug the keyboards out and in as many times as\nyou please.\n\nBut, emphasis on out and in, not in and out. `Inputd` will\nonly use the keyboards that were available when it started.\nChanging this to support adding more keyboards while inputd\nis running is planned, it is mostly a relic of having to\ndebug `inputd` by plugging in another keyboard (which I\ndidn't want it locking on to.)\n\nCurrently, if you plug in another keyboard you can just\nrun `systemctl restart hawck-inputd` and it should find\nthe new keyboard, and automatically lock it the next time\nit is plugged in (before you restart hawck-inputd again.)\n\n#### Alternative security strategies and motivations\n\nThe current model is a compromise between ease of use, and security.\n\nThe issue with the current Hawck security model is that binding all keys\non your keyboard (or even 20-30% of them, depending on which keys they\nare and how common they are in your language) effectively renders the\nsecurity model useless.\n\nHere are some alternative models that have been considered:\n\n- Run all scripts inside inputd\n  - In this model there is only a single daemon, hawck-inputd\n  - This is planned as an optional alternative, and a lot of\n    the code has already been written, but it takes a lot of\n    work and consideration to make this work well, and securely.\n    - Work on this might just be scrapped for script-less\n      rebinding directly inside inputd.\n  - Scripts are sandboxed and are not allowed to communicate\n    with the outside world, this includes launching programs.\n    - Problem #1: Not being able to bind keys to launchers was\n      a deal breaker for me.\n    - Problem #2: Perfect sandboxing isn't easy to pull of, it\n      requires very careful programming and as such takes longer to\n      implement.\n- Don't use scripts\n  - This is obviously not as flexible.\n  - A common use case for something like this is very simple\n    rebindings, like caps =\u003e control. This system would work\n    very well for those cases.\n  - Although this is planned as an alternative, it can never\n    be the only supported mode, as Hawck aims to be as useful\n    when it comes to keyboard automation as something like\n    AutoHotkey.\n- \"Abandon all hope, ye who enter unsafe mode\"\n\n  - For a lot of users, especially the ones still on X11, these\n    issues are not a concern. They might want a model that just\n    let's them do whatever they want whenever they want without\n    any bothersome password prompts. This is not recommended,\n    but a perfectly valid stance.\n  - This is already supported, opt-in, by toggling unsafe mode\n    in the settings panel of hawck-ui.\n\n- What I'm currently aiming for is being able to support these\n  three modes:\n  - Sandboxed inside inputd\n    - Used for most simple key-replacement scripts.\n  - Whitelisted keys passed from inputd to macrod (current method)\n    - Used for anything that can't be done from the sandbox.\n  - Unsafe mode (optional current method)\n    - Used by X11 users, and users who just don't care.\n\n#### Synchronous and asynchronous keyboard locking explained\n\nIf you read the logs you might see something like this:\n\n```\nhawck-inputd[7672]: Running Hawck InputD ...\nhawck-inputd[7672]: Attempting to get lock on device: Logitech Logitech G710 Keyboard @ usb-0000:00:14.0-4/input0\nhawck-inputd[7672]: Locking keyboard: Logitech Logitech G710 Keyboard ...\nhawck-inputd[7672]: Preparing async lock on: Logitech Logitech G710 Keyboard @ usb-0000:00:14.0-4/input0\nhawck-inputd[7672]: Attempting to get lock on device: AT Translated Set 2 keyboard @ isa0060/serio0/input0\nhawck-inputd[7672]: Locking keyboard: AT Translated Set 2 keyboard ...\nhawck-inputd[7672]: Immediate lock on: AT Translated Set 2 keyboard @ isa0060/serio0/input0\nhawck-inputd[7672]: Acquired lock on keyboard: Logitech Logitech G710 Keyboard\n```\n\n`hawck-inputd` employs two different methods for locking keyboards.\n\nOne is the simple immediate lock, where an `ioctl` call is used to\ngain exclusive access to the keyboard immediately.\n\nThe other one is an asynchronous lock, which solves the following\nproblem, illustrated by using an immediate lock where it should\nnot be used:\n\n- hawck-inputd is not running\n- user holds down shift on G710 keyboard\n- GNOME desktop receives a [down + shift] event, from G710 keyboard\n- hawck-inputd starts up\n- hawck-inputd acquires an immediate lock on G710\n  - GNOME will now not see any more of the G710 keypresses\n- hawck-inputd registers a virtual keyboard device\n  - This device will now echo key events that come from G710,\n    and any other keyboard on the system.\n- user stops pressing the shift key on G710 keyboard\n- hawck-inputd receives the [up + shift] event\n- hawck-inputd will now echo the [up + shift] event through\n  its virtual keyboard.\n- GNOME desktop sees an [up + shift] event, from Hawck keyboard\n  - GNOME doesn't (and shouldn't) do anything with this event\n- GNOME desktop still considers the shift key to be held down,\n  because as far as it knows it is still held down on one of the\n  keyboards, Hawck stole the memo.\n- user presses w, but what comes out is \"W\"\n- No amount of pressing and releasing shift solves the problem.\n- user is having a bad time\n\nAsynchronous lock solves this problem by simply waiting until\nevery key has been released before the keyboard is locked.\n\n## Ports\n\n### BSD\n\nPorting to FreeBSD is possible, but requires the installation of shims, or\nporting of the following classes/functions.\n\n- `Keyboard`\n  - Access to raw keyboard input.\n  - shim: https://www.freshports.org/devel/evdev-proto/\n  - Requires: `ioctl(fd, EVIOCGRAB, ...)` for exclusive keyboard lock.\n- `UDevice`\n  - Virtual keyboard devices for outputting keys.\n  - shim: https://www.freshports.org/devel/evdev-proto/\n- `KBDInfo`/`KBDB`\n  - Gathers info about a keyboard from `sysfs`\n  - Can this be retrieved using `sysctl` on FreeBSD?\n- `pidexe(pid_t)`\n  - Used to find the executable behind a process id\n  - Single line of code change.\n- `FSWatcher.cpp`\n  - File system notifications\n  - shim: https://github.com/libinotify-kqueue/libinotify-kqueue\n\n## Known Bugs:\n\n- Outputting keys too quickly:\n  - GNOME Wayland has a bug where it will drop a lot of keys\n  - Workaround: run hawck-inputd with the --udev-event-delay flag set to 3800 (µs)\n","funding_links":[],"categories":["Linux"],"sub_categories":["Other"],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnyball%2FHawck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fsnyball%2FHawck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fsnyball%2FHawck/lists"}