├── .gitignore ├── Makefile ├── README.md ├── ammd.c ├── bitcmds ├── 31.do └── 31.undo └── config.h /.gitignore: -------------------------------------------------------------------------------- 1 | orig/ 2 | orig/* 3 | obj/ 4 | obj/* 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC ?= gcc 2 | CFLAGS ?= -Wall -g 3 | OPTFLAGS ?= -O3 4 | DEBUGFLAGS ?= -DDEBUG 5 | 6 | default: obj/ammd obj/winclass 7 | debug: obj/ammd obj/dammd obj/winclass 8 | clean: 9 | rm -rf obj 10 | 11 | $(HOME)/.appmodmaps/.created: 12 | mkdir "$(HOME)/.appmodmaps" || echo "Never mind." 13 | cp bitcmds/* "$(HOME)/.appmodmaps" 14 | touch "$(HOME)/.appmodmaps/.created" 15 | 16 | $(HOME)/.appmodmaps/config.h: $(HOME)/.appmodmaps/.created 17 | cp config.h "$(HOME)/.appmodmaps/config.h" 18 | 19 | obj: 20 | mkdir obj || echo "Never mind." 21 | 22 | obj/ammd: $(HOME)/.appmodmaps/config.h ammd.c obj 23 | $(CC) $(CFLAGS) $(OPTFLAGS) "-DUSER_CONFIG=\"$(HOME)/.appmodmaps/config.h\"" -o obj/ammd ammd.c -lX11 24 | 25 | obj/dammd: obj/ammd ammd.c obj 26 | $(CC) $(CFLAGS) $(DEBUGFLAGS) "-DUSER_CONFIG=\"$(HOME)/.appmodmaps/config.h\"" -o obj/dammd ammd.c -lX11 27 | 28 | obj/winclass: obj/ammd ammd.c obj 29 | $(CC) $(CFLAGS) $(DEBUGFLAGS) -o obj/winclass ammd.c -lX11 30 | 31 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # appmodmap 2 | 3 | **appmodmap** is a daemon that watches the currently active window and dynamically adjusts your system settings (such as keyboard, etc.) as the active window class changes. You create a set of primitive operations and then you can select any combination of them for any particular X11 window class. It was written for Linux but should work on any POSIXy thing with recent X11 libraries. 4 | 5 | **appmodmap** was originally written as a better way of dynamically enabling the Command key "like it oughta work" by enabling and disabling mapping it to the Control key based on application support and the system environment. This original usage is included as a set of demo primitives which can be used "out of the box." 6 | 7 | ## Setting it up 8 | 9 | **appmodmap** comes as a collection of shell scripts, a `Makefile`, a single C source file, and a header file that is compiled into that source file called `config.h` with configuration information. The main daemon has no dependencies other than X11 itself, though the included "demo" shell scripts it uses as primitives call `setxkbmap` and manipulate GNOME settings. 10 | 11 | When you run the `Makefile` with `make`, a directory `~/.appmodmaps` will be created. This folder contains the demo `.do` and `.undo` primitives (see below), which are shell scripts, and the local copy of `config.h` which you modify for your own settings. Don't change the ones actually in the repository unless you intend to generate a PR to make your changes the default. 12 | 13 | The `Makefile` will then compile two binaries: `obj/ammd`, the main daemon, which can be run from any Terminal window or as part of your X startup scripts, and `obj/winclass`, which is a debugging tool to tell you the class hint of the currently active window (it displays the window ID, class hint window name and the window class as you move between windows). You can move these binaries anywhere convenient. 14 | 15 | Whenever you run `make` again, your local copy of `config.h` in `~/.appmodmaps` is used, not the `config.h` that comes with the repository. If you want to reset it or update it with later changes such as updated primitives, either merge your directory together with the new copy of `appmodmap`, or remove the `~/.appmodmaps` directory entirely and it will be recreated. 16 | 17 | ## Running it 18 | 19 | Just start `ammd` (in the background if you like with `&` or as your shell requires). When the daemon is terminated, killed or otherwise, it will reset everything back to the default before exiting. 20 | 21 | Similarly, to watch windows and class hints without running primitives, just start `winclass` in the same fashion, and Control-C to quit it. 22 | 23 | ## Configuring your own settings 24 | 25 | The default "demo" files that comes with **appmodmap** implement toggling Command-key remapping with certain apps. We'll use this here to illustrate how to create more complex situations, or add more applications. 26 | 27 | In `~/.appmodmaps/config.h` is a mapping of window names (as specified in `XClassHint`, which you can obtain from `winclass`) to bit values. The demo files have a single bit (`1<<31`) which is set for those apps that need it, but you can use up to any combination of 32 individual primitive states by just `or`ing bits together. Window classes that do _not_ appear in `~/.appmodmaps/config.h` are considered to have a bit value of 0. 28 | 29 | When `ammd` sees a new window accept input, it retrieves the name from the class hint and its required new bit value (or 0). If the current bit value does not match what the new window requires, it will iterate through each bit of the new value to make the current value match. Bits that need to be set will trigger calling the "do" primitive for that bit value; bits that need to be unset will trigger calling the "undo" primitive. 30 | 31 | Let's show how this works with the included Command key demo. We switch from GNOME terminal, which allows you to remap its keyboard shortcuts, to Nautilus, which doesn't. In the terminal, the bit value is 0 (use no settings), because it does not appear in `~/.appmodmaps/config.h`. Nautilus, however, has a value of `1<<31`. When we switch to Nautilus, `ammd` computes that we need to turn on bit 31 to get from 0, and runs that primitive by calling the shell script `~/.appmodmaps/31.do` to set the necessary keyboard changes. If we switch from Nautilus to any other app with the same bit value, `ammd` does nothing, since the new value is unchanged. But if we switch back to GNOME terminal, `ammd` computes we now need to turn off bit 31, and calls the shell script `~/.appmodmaps/31.undo` to undo those keyboard changes. 32 | 33 | If we terminate `ammd` while bits are still set, `ammd` will call the "undo" primitive for each set bit to reset your system back to the original state. In this case, if bit 31 was still on, then it will call `~/.appmodmaps/31.undo` to undo those keyboard changes. 34 | 35 | To add new bits, just put them into `~/.appmodmaps/config.h` and then create corresponding "do" and "undo" primitives. For example, to use bit 3, just `|(1<<3)` (or the `#define` you set for that bit) with the bit value in the required mapping entries in `~/.appmodmaps/config.h`, and create `~/.appmodmaps/3.do` and `~/.appmodmaps/3.undo`. The shell scripts for the "do" and "undo" primitives should be mirror images: what one does, the other should exactly undo, and vice versa, without side effects if possible. Make sure these scripts are executable (`chmod +x`). Then run a `make` to rebuild `ammd` and stop and restart `ammd` if it was already running. 36 | 37 | For local use, to avoid conflict with future built-in primitives in future versions of **appmodmap**, we advise starting from bit 0 and working your way up for local primitives you implement. 38 | 39 | To add new application window name types, obtain the window name from the `XClassHint` using `winclass` (or any similar tool), and then insert a new entry into the mappings in `~/.appmodmaps/config.h` with the needed bit value. Then run a `make` to rebuild `ammd` and stop and restart `ammd` if it was already running. 40 | 41 | ## To-do 42 | 43 | It would be nice to get configuration information from an XDG path as well (currently XDG paths are only used for lockfiles, and only if they are defined). 44 | 45 | `ammd` and `winclass` don't check the current front window; they only react on changes. If you start it from a terminal app that needs its services, the bitmap won't change until you switch to something else and then switch back. 46 | 47 | Make a friendlier way of configuring the daemon, maybe even allowing live changes. However, this would require coming up with some sort of configuration file format and having a dependency on it, which makes this more complex than I'd like. 48 | 49 | Include additional typical system primitives. (If you have some ideas, file a PR.) 50 | 51 | We use the window name as given in the class hint because that's usually what you want, but sometimes we actually do want the window class. (And the window name given in the class hint doesn't usually match what's in the titlebar, which can be confusing.) 52 | 53 | ## License 54 | 55 | **appmodmap** is offered to you under the BSD license. 56 | 57 | Copyright (c) 2018-2020, Cameron Kaiser. 58 | All rights reserved. 59 | 60 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 61 | 62 | * Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 63 | * Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 64 | * The names of its contributors may not be used to endorse or promote products derived from this software without specific prior written permission. 65 | 66 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 67 | 68 | 69 | -------------------------------------------------------------------------------- /ammd.c: -------------------------------------------------------------------------------- 1 | /* Copyright 2018-2020 Cameron Kaiser. 2 | All rights reserved. 3 | BSD license. */ 4 | 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #define MAX_STRING_SIZE 1024 18 | 19 | char currentmap[MAX_STRING_SIZE]; 20 | char path[MAX_STRING_SIZE], basepath[MAX_STRING_SIZE]; 21 | 22 | #define LOCK_FILE "%s/ammd.lock" 23 | #define LOCK_FILE_PID "%s/ammd.lock.%u" 24 | int lock_fd; 25 | char lockfile[MAX_STRING_SIZE], lockfile_pid[MAX_STRING_SIZE]; 26 | 27 | uint32_t bitset; 28 | struct sigaction action; 29 | 30 | typedef struct mapping { 31 | char *wclass; 32 | unsigned int statemask; 33 | } mappings; 34 | 35 | #ifndef USER_CONFIG 36 | #warning Creating daemon with no user configuration settings. 37 | #else 38 | #include USER_CONFIG 39 | #endif 40 | 41 | /* The below are not useful without locking or reconfiguring. */ 42 | #ifdef USER_CONFIG 43 | 44 | void update_keymappings_for_bits(uint32_t newbitset) { 45 | uint32_t i, j = 1; 46 | int l; 47 | 48 | if (newbitset == bitset) return; /* nothing to do */ 49 | 50 | /* iterate through the new bitset, running do and undo scripts 51 | as needed to equal the new bit state */ 52 | for (i=0; i<32; i++, j<<=1) { 53 | if ((bitset & j) == (newbitset & j)) continue; 54 | 55 | if (newbitset & j) 56 | l = snprintf(path, MAX_STRING_SIZE, "%s/%u.do", basepath, i); 57 | else 58 | l = snprintf(path, MAX_STRING_SIZE, "%s/%u.undo", basepath, i); 59 | if (l < 1 || l >= (MAX_STRING_SIZE-1)) { 60 | fprintf(stderr, "failure: oversized command line for bit %i\n", i); 61 | continue; 62 | } 63 | 64 | #if DEBUG 65 | fprintf(stderr, "executing: %s\n", path); 66 | #endif 67 | system(path); 68 | } 69 | bitset = newbitset; 70 | } 71 | 72 | void find_keymappings(unsigned long wid, XClassHint *c) { 73 | mappings m; 74 | uint32_t i, newbitset = 0; 75 | 76 | if (!strcmp(c->res_name, currentmap)) 77 | return; /* nothing to do */ 78 | 79 | /* leave null termination */ 80 | (void)strncpy(currentmap, c->res_name, (MAX_STRING_SIZE - 1)); 81 | for(i=0; ; i++) { 82 | m = keymaps[i]; 83 | if (!m.statemask || !m.wclass) 84 | break; 85 | 86 | if (!strcmp(c->res_name, m.wclass)) { 87 | if ((newbitset & m.statemask) != m.statemask) 88 | newbitset |= m.statemask; 89 | break; 90 | } 91 | } 92 | #if DEBUG 93 | fprintf(stderr, "new bit set: 0x%08x\n", newbitset); 94 | #endif 95 | update_keymappings_for_bits(newbitset); 96 | } 97 | 98 | void reset_daemon() { 99 | #if DEBUG 100 | fprintf(stderr, "terminating\n"); 101 | #endif 102 | update_keymappings_for_bits(0); 103 | if(close(lock_fd) || unlink(lockfile_pid) || unlink(lockfile)) { 104 | perror("unable to cleanup lock"); 105 | } 106 | } 107 | 108 | static void bye(int for_great_justice_take_off_every_sig) { 109 | #if DEBUG 110 | fprintf(stderr, "signal\n"); 111 | #endif 112 | exit(0); 113 | } 114 | 115 | #endif 116 | 117 | static int xerrorh(Display *d, XErrorEvent *e) { 118 | #if DEBUG 119 | fprintf(stderr, "Caught Xlib error=%d code=%d\n", 120 | e->error_code, e->request_code); 121 | #endif 122 | return 0; 123 | } 124 | 125 | int main(int argc, char **argv) { 126 | Display *d; 127 | Window w; 128 | XEvent e; 129 | int i, j; 130 | char *xdgrd; 131 | 132 | if (!getenv("HOME")) { 133 | fprintf(stderr, "unable to determine home directory\n"); 134 | return 1; 135 | } 136 | i = snprintf(basepath, MAX_STRING_SIZE, "%s/.appmodmaps", getenv("HOME")); 137 | if (i < 1 || i >= (MAX_STRING_SIZE-1)) { 138 | fprintf(stderr, "unable to compute base path\n"); 139 | return 1; 140 | } 141 | 142 | d = XOpenDisplay(NULL); 143 | if (!d) { 144 | fprintf(stderr, "Failed to open X display\n"); 145 | return 1; 146 | } 147 | 148 | w = DefaultRootWindow(d); 149 | XSelectInput(d, w, PropertyChangeMask); 150 | 151 | #ifdef USER_CONFIG 152 | /* Only worth doing this work for locking and unwinding if we actually 153 | do something worth locking and unwinding for. */ 154 | 155 | if ((xdgrd = getenv("XDG_RUNTIME_DIR"))) { 156 | i = snprintf(lockfile_pid, MAX_STRING_SIZE, LOCK_FILE_PID, xdgrd, getpid()); 157 | j = snprintf(lockfile, MAX_STRING_SIZE, LOCK_FILE, xdgrd); 158 | } else { 159 | i = snprintf(lockfile_pid, MAX_STRING_SIZE, LOCK_FILE_PID, 160 | "/tmp", getpid()); 161 | j = snprintf(lockfile, MAX_STRING_SIZE, LOCK_FILE, "/tmp"); 162 | } 163 | if (i < 1 || i >= (MAX_STRING_SIZE-1) || j < 1 || j >= (MAX_STRING_SIZE-1)) { 164 | fprintf(stderr, "unable to compute lock path\n"); 165 | return 1; 166 | } 167 | 168 | lock_fd = open(lockfile_pid, O_CREAT); 169 | if (link(lockfile_pid, lockfile)) { 170 | (void)close(lock_fd); 171 | (void)unlink(lockfile_pid); 172 | perror("unable to lock: ammd already running?"); 173 | return 1; 174 | } 175 | 176 | atexit(reset_daemon); 177 | (void)memset(&action, 0, sizeof(action)); 178 | action.sa_handler = bye; 179 | if (sigaction(SIGINT, &action, 0) || sigaction(SIGTERM, &action, 0)) { 180 | perror("sigaction failed"); 181 | return 1; 182 | } 183 | #else 184 | (void)xdgrd; /* suppress unused warnings */ 185 | (void)j; 186 | #endif 187 | 188 | (void)memset(currentmap, 0, sizeof(currentmap)); 189 | (void)XSetErrorHandler(xerrorh); 190 | for (;;) { 191 | XNextEvent(d, &e); 192 | if (e.type == PropertyNotify) { 193 | if (!strcmp(XGetAtomName(e.xproperty.display, e.xproperty.atom), "_NET_ACTIVE_WINDOW")) { 194 | Atom f, a; 195 | int af, status; 196 | unsigned long ni, ba; 197 | unsigned long *prop; /* native endian and bit length */ 198 | 199 | /* get the new active window */ 200 | f = XInternAtom(d, "_NET_ACTIVE_WINDOW", True); 201 | status = XGetWindowProperty( 202 | d, 203 | e.xproperty.window, 204 | f, 0, 1000, False, AnyPropertyType, 205 | &a, &af, &ni, &ba, (unsigned char **)&prop); 206 | if (status == Success) { 207 | unsigned long wid = *prop; 208 | XClassHint *c; 209 | 210 | /* not at all unusual to get an XBadWindow now and then */ 211 | if (!wid) 212 | continue; 213 | 214 | /* get class */ 215 | c = XAllocClassHint(); 216 | if (c) { 217 | status = XGetClassHint(d, (Window)wid, c); 218 | if (status) { 219 | #if DEBUG 220 | #if __LP64__ 221 | fprintf(stdout, "0x%08lx \"%s\" \"%s\"\n", 222 | wid, c->res_name, c->res_class); 223 | #else 224 | fprintf(stdout, "0x%08x \"%s\" \"%s\"\n", 225 | wid, c->res_name, c->res_class); 226 | #endif 227 | #endif 228 | #ifdef USER_CONFIG 229 | find_keymappings(wid, c); 230 | #endif 231 | } 232 | #if DEBUG 233 | else { 234 | fprintf(stderr, "failure with class hint: %i\n", status); 235 | } 236 | #endif 237 | XFree(c); 238 | } else { 239 | /* fatal */ 240 | fprintf(stderr, "out of memory!!!!\n"); 241 | return 1; 242 | } 243 | } 244 | #if DEBUG 245 | else { 246 | fprintf(stderr, "failed to get window property: %i\n", status); 247 | } 248 | #endif 249 | } 250 | } 251 | } 252 | 253 | return 0; 254 | } 255 | 256 | -------------------------------------------------------------------------------- /bitcmds/31.do: -------------------------------------------------------------------------------- 1 | # This script sets the Command ("Super") key to be equivalent to Control, 2 | # and also sets the GNOME keybindings so that Super-Tab still switches. 3 | 4 | #Uncomment these lines if you use xmodmap instead of setxkbmap. 5 | #This is uncommon. 6 | # xmodmap -e "remove mod4 = Super_L" 7 | # xmodmap -e "add control = Super_L" 8 | 9 | setxkbmap -option altwin:ctrl_win & 10 | 11 | gsettings set org.gnome.desktop.wm.keybindings switch-applications "['Tab', 'Tab']" & 12 | gsettings set org.gnome.desktop.wm.keybindings switch-applications-backward "['Tab', 'Tab']" & 13 | gsettings set org.gnome.desktop.wm.keybindings switch-group-backward "['Above_Tab', 'Above_Tab']" & 14 | gsettings set org.gnome.desktop.wm.keybindings switch-group "['Above_Tab', 'Above_Tab']" & 15 | 16 | -------------------------------------------------------------------------------- /bitcmds/31.undo: -------------------------------------------------------------------------------- 1 | # This reverses 31.do. See that file. 2 | 3 | #Uncomment if you uncommented the analogous lines in 1.do. 4 | # xmodmap -e "remove control = Super_L" 5 | # xmodmap -e "add mod4 = Super_L" 6 | 7 | # setxkbmap -option altwin: does not work. This does. 8 | setxkbmap -option & 9 | 10 | gsettings set org.gnome.desktop.wm.keybindings switch-applications "['Tab', 'Tab']" & 11 | gsettings set org.gnome.desktop.wm.keybindings switch-applications-backward "['Tab', 'Tab']" & 12 | gsettings set org.gnome.desktop.wm.keybindings switch-group-backward "['Above_Tab', 'Above_Tab']" & 13 | gsettings set org.gnome.desktop.wm.keybindings switch-group "['Above_Tab', 'Above_Tab']" & 14 | 15 | -------------------------------------------------------------------------------- /config.h: -------------------------------------------------------------------------------- 1 | /* Define command bits. */ 2 | 3 | /* Demo bit included: map Command key as an alias to Control. */ 4 | #define CTRLCMD (1<<31) 5 | 6 | /* Define additional local bits here in this format. Work up from 0 to 7 | avoid conflict with included default ones. */ 8 | /* #define BIT1CMD (1<<0) */ 9 | /* #define BIT2CMD (1<<1) */ 10 | /* etc. */ 11 | 12 | /* For each window name specified, include the bit value of the keyboard 13 | settings to be enabled when that window name is active. */ 14 | 15 | mappings keymaps[] = { 16 | { "epiphany", CTRLCMD }, 17 | { "soffice", CTRLCMD }, 18 | { "libreoffice", CTRLCMD }, 19 | { "gedit", CTRLCMD }, 20 | { "org.gnome.gedit", CTRLCMD }, 21 | { "nautilus", CTRLCMD }, 22 | { "org.gnome.Nautilus", CTRLCMD }, 23 | { "krita", CTRLCMD }, 24 | { "vlc", CTRLCMD }, 25 | { "Pinta", CTRLCMD }, 26 | { "inkscape", CTRLCMD }, 27 | { "gimp-2.8", CTRLCMD }, 28 | { "retext", CTRLCMD }, 29 | { "evince", CTRLCMD }, 30 | { "ghex", CTRLCMD }, 31 | 32 | /* Terminate the list with this couplet. */ 33 | 34 | { NULL, 0 }, 35 | }; 36 | 37 | --------------------------------------------------------------------------------