├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── example └── keydoublerc ├── kdkill ├── kdlaunch ├── keydouble.c └── logo └── keydouble_logo.png /.gitignore: -------------------------------------------------------------------------------- 1 | keydouble 2 | *.o 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011, Bastien Dejean 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | 1. Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 2. Redistributions in binary form must reproduce the above copyright notice, 10 | this list of conditions and the following disclaimer in the documentation 11 | and/or other materials provided with the distribution. 12 | 13 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 14 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 15 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 16 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR 17 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 18 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 19 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND 20 | ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 21 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 22 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 23 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | CC = gcc 2 | CFLAGS = -ansi -Wall -O2 3 | LIBS = -lX11 -lXtst 4 | PREFIX = /usr/local 5 | BINPREFIX = $(PREFIX)/bin 6 | 7 | SRC = keydouble.c 8 | 9 | all: options keydouble 10 | 11 | options: 12 | @echo "keydouble build options:" 13 | @echo "CC = $(CC)" 14 | @echo "CFLAGS = $(CFLAGS)" 15 | @echo "PREFIX = $(PREFIX)" 16 | 17 | keydouble: $(SRC) Makefile 18 | $(CC) -o $@ $(SRC) $(CFLAGS) $(LIBS) 19 | 20 | clean: 21 | @echo "cleaning" 22 | rm -f keydouble 23 | 24 | install: all 25 | @echo "installing executable files to $(DESTDIR)$(BINPREFIX)" 26 | @install -D -m 755 keydouble $(DESTDIR)$(BINPREFIX)/keydouble 27 | @install -D -m 755 kdlaunch $(DESTDIR)$(BINPREFIX)/kdlaunch 28 | @install -D -m 755 kdkill $(DESTDIR)$(BINPREFIX)/kdkill 29 | 30 | uninstall: 31 | @echo "removing executable files from $(DESTDIR)$(BINPREFIX)" 32 | @rm -f $(DESTDIR)$(BINPREFIX)/{keydouble,kdlaunch,kdkill} 33 | 34 | .PHONY: all options clean install uninstall 35 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![logo](https://github.com/baskerville/keydouble/raw/master/logo/keydouble_logo.png) 2 | 3 | --- 4 | 5 | ## Warning 6 | 7 | This is a quick hack and you should use [xcape](https://github.com/alols/xcape) instead. 8 | 9 | ## Description 10 | 11 | This little X utility enables the use of keys simultaneously as modifiers and ordinary keys, 12 | 13 | The original key behavior is maintained under simple key press/release circumstances; but when a key is chorded, it can act as a modifier. 14 | 15 | E.g.: *left shift + b* produces *B*, but *left shift* tapped on its own might produce *parenleft*. 16 | 17 | ## Install 18 | 19 | Run 20 | 21 | make && make install 22 | 23 | By default, this will install in `/usr/local/bin`. If you'd like it somewhere else, then copy the executables to your `bin` directory of choice. 24 | 25 | ## Configuration 26 | 27 | `kdlaunch` honors a configuration file at `~/.keydoublerc`. 28 | 29 | This file contains *both* xmodmap configuration and keydouble configuration. 30 | 31 | An example configuration file is provided at `example/keydoublerc`. 32 | 33 | To write your own, use `xev` to discover the natural keycode fo the key you'd like to alter. Then pick an unused keycode (working backwards from 255 is recommended). In `.keydoublerc`'s `natart_pairs`, add a pair `NATURAL_KEYCODE:UNUSED_KEYCODE` and then add two xmodmap commands: one mapping the natural keycode to the modifier of your choice, and the other mapping the unused keycode to the keysym you'd like to appear when you tap. You may also need to add the keysym to the appropriate modifier (i.e. `add control = Control_L`). See xmodmap's docs for details. 34 | 35 | If you have a personal `xmodmap` configuration, copy it into this file, put it at `~/.xmodmaprc` (or change the relevant path in `kdlaunch`). 36 | 37 | Once you've tested (try `kdlaunch`) and you're satisfied, add 38 | 39 | kdlaunch & 40 | 41 | to `~/.xinitrc`. 42 | 43 | To kill `keydouble` run `kdkill`. 44 | 45 | ## Dependencies 46 | 47 | - libxtst 48 | - xorg-xmodmap 49 | - dash 50 | 51 | ## Strategy 52 | 53 | The default keycode (called *natural*) of the original key is mapped to the modifier keysym and the keycode generated by `keydouble` (called *artificial*) under isolated key press/release is mapped to the original keysym. 54 | 55 | The mapping is done by `xmodmap` and the *artificial* keycode generation by `keydouble`. 56 | 57 | ## Troubleshooting. 58 | 59 | ```Error! Option "-query" not recognized``` 60 | 61 | Either upgrade your x utils, or (what may be easier) hardcode your keyboard layout in kdlaunch. There's a comment pointing out where to put it. 62 | 63 | 64 | If needed (i.e. if you encounter record module errors), the `record` X module can be loaded with: 65 | 66 | Section "Module" 67 | Load "record" 68 | EndSection 69 | 70 | in `/etc/X11/xorg.conf`. 71 | -------------------------------------------------------------------------------- /example/keydoublerc: -------------------------------------------------------------------------------- 1 | ! keydouble configuration file. 2 | ! 3 | ! 1. Use `xev` to discover the natural keycode fo the key you'd like to 4 | ! alter. 5 | ! 6 | ! 2. Pick an unused keycode (working backwards from 255 is 7 | ! recommended). 8 | ! 9 | ! 3. In natart pairs, add a pair `NATURAL_KEYCODE:UNUSED_KEYCODE` 10 | ! 11 | ! 4. Add two xmodmap commands: one mapping the natural keycode to the 12 | ! modifier of your choice, and the other mapping the unused keycode to 13 | ! the keysym you'd like to appear when you tap. 14 | ! 15 | ! 5. You may also need to add the keysym to the appropriate modifier 16 | ! (i.e. `add control = Control_L`). See xmodmap's docs for details. 17 | ! 18 | ! ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 19 | ! The following line SHOULD REMAIN COMMENTED. It'll still do it's job. 20 | ! ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 21 | ! 22 | ! natart_pairs: 66:255 23:254 50:253 62:252 23 | 24 | keycode 66 = Control_L 25 | keycode 255 = F18 26 | 27 | keycode 23 = Alt_L 28 | keycode 254 = Tab 29 | 30 | keycode 50 = Shift_L 31 | keycode 253 = parenleft 32 | 33 | keycode 62 = Shift_R 34 | keycode 252 = parenright 35 | 36 | keycode 64 = Alt_L 37 | 38 | add control = Control_L 39 | add mod1 = Alt_L 40 | 41 | remove Lock = Caps_Lock -------------------------------------------------------------------------------- /kdkill: -------------------------------------------------------------------------------- 1 | #! /bin/dash 2 | 3 | kdpid=$(pgrep -x keydouble) 4 | 5 | if [ -n "$kdpid" ] ; then 6 | kill -TERM "$kdpid" 7 | else 8 | printf "%s\n" "keydouble is not running" 9 | fi 10 | -------------------------------------------------------------------------------- /kdlaunch: -------------------------------------------------------------------------------- 1 | #! /bin/dash 2 | 3 | XMODMAP_CONF="$HOME/.xmodmaprc" 4 | KEYDOUBLE_CONF="$HOME/.keydoublerc" 5 | 6 | die() { 7 | printf "%s\n" "$@" >&2 8 | exit 1 9 | } 10 | 11 | [ -f "$KEYDOUBLE_CONF" ] || die "found no configuration file\n" 12 | natart_pairs=$(grep -i '^![[:space:]]*natart_pairs:' "$KEYDOUBLE_CONF" | sed 's/^[^:]\+:[[:space:]]*//') 13 | [ -z "$natart_pairs" ] && die "please define a natart_pairs header\n" 14 | 15 | restore_map() { 16 | 17 | # If setxkbmap has no query option, change lyt to your local layout; 18 | # e.g. "us", "fr", etc. 19 | lyt=$(setxkbmap -query | grep -i '^layout:' | awk '{print $2}') 20 | 21 | if [ -n "$lyt" ] ; then 22 | setxkbmap -layout "$lyt" 23 | else 24 | die "couldn't guess the current keyboard layout" 25 | fi 26 | [ -f "$XMODMAP_CONF" ] && xmodmap "$XMODMAP_CONF" 27 | exit 0 28 | } 29 | 30 | trap restore_map INT 31 | 32 | xmodmap "$KEYDOUBLE_CONF" 33 | 34 | # don't use quotes here 35 | keydouble $natart_pairs 36 | 37 | restore_map 38 | -------------------------------------------------------------------------------- /keydouble.c: -------------------------------------------------------------------------------- 1 | #define _BSD_SOURCE 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | #include 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #define ARTIFICIAL_TIMEOUT 600 17 | #define SLEEP_MICROSEC 100*1000 18 | #define MAX_CODE 256 19 | #define CODE_UNDEF -1 20 | #define PAIR_SEP ":" 21 | 22 | typedef enum { 23 | false, 24 | true 25 | } bool; 26 | 27 | void setup(void); 28 | void loop(void); 29 | void stop(int signum); 30 | void evtcallback(XPointer priv, XRecordInterceptData *hook); 31 | void die(const char *errstr, ...); 32 | int deltamsec(struct timeval t1, struct timeval t2); 33 | 34 | Display *ctldpy, *datdpy; 35 | XRecordContext reccontext; 36 | XRecordRange *recrange; 37 | XRecordClientSpec reccspec; 38 | 39 | /* maps a natural to an artificial keycode */ 40 | int natart[MAX_CODE]; 41 | bool running = true; 42 | 43 | /* from libxnee */ 44 | typedef union { 45 | unsigned char type; 46 | xEvent event; 47 | xResourceReq req; 48 | xGenericReply reply; 49 | xError error; 50 | xConnSetupPrefix setup; 51 | } XRecordDatum; 52 | 53 | void setup(void) 54 | { 55 | int event, error, major, minor; 56 | /* 57 | We're gonna fetch two display objects; one, we'll be stealing events 58 | from, and the other, we'll be sending events to. This prevents an 59 | infinite loop. 60 | */ 61 | if (!(ctldpy = XOpenDisplay(NULL)) || !(datdpy = XOpenDisplay(NULL))) 62 | die("cannot open display\n"); 63 | 64 | /* 65 | We have to synchronize the control display to ensure that the 66 | events we *send* get sent immediately; because we're not doing 67 | anything but sending key events, it should not result in a 68 | significant reduction in speed. 69 | */ 70 | XSynchronize(ctldpy, true); 71 | 72 | /* 73 | Now we have to fetch the XRecord context; some sanity checking, 74 | first, then grab a context off of the 'from' display. 75 | */ 76 | if (!XTestQueryExtension(ctldpy, &event, &error, &major, &minor)) 77 | die("the xtest extension is not loaded\n"); 78 | 79 | if (!XRecordQueryVersion(ctldpy, &major, &minor)) 80 | die("the record extension is not loaded\n"); 81 | 82 | if (!(recrange = XRecordAllocRange())) 83 | die("could not alloc the record range object\n"); 84 | 85 | recrange->device_events.first = KeyPress; 86 | recrange->device_events.last = ButtonPress; 87 | reccspec = XRecordAllClients; 88 | 89 | if (!(reccontext = XRecordCreateContext(datdpy, 0, &reccspec, 1, &recrange, 1))) 90 | die("could not create a record context"); 91 | 92 | /* Finally, start listening for events. */ 93 | if (!XRecordEnableContextAsync(datdpy, reccontext, evtcallback, NULL)) 94 | die("cannot enable record context\n"); 95 | } 96 | 97 | void loop(void) 98 | { 99 | while (running) { 100 | XRecordProcessReplies(datdpy); 101 | usleep(SLEEP_MICROSEC); 102 | } 103 | } 104 | 105 | void evtcallback(XPointer priv, XRecordInterceptData *hook) 106 | { 107 | if (hook->category != XRecordFromServer) { 108 | XRecordFreeData(hook); 109 | return; 110 | } 111 | 112 | XRecordDatum *data = (XRecordDatum *) hook->data; 113 | 114 | static unsigned int numnat; 115 | static bool natdown[MAX_CODE], keycomb[MAX_CODE]; 116 | static struct timeval startwait[MAX_CODE], endwait[MAX_CODE]; 117 | 118 | int code = data->event.u.u.detail; 119 | int evttype = data->event.u.u.type; 120 | 121 | if (evttype == KeyPress) { 122 | /* a natural key was pressed */ 123 | if (!natdown[code] && natart[code] != CODE_UNDEF) { 124 | natdown[code] = true; 125 | numnat++; 126 | gettimeofday(&startwait[code], NULL); 127 | } else if (numnat > 0) { 128 | int i; 129 | for (i = 0; i < MAX_CODE; i++) 130 | keycomb[i] = natdown[i]; 131 | } 132 | } else if (evttype == KeyRelease) { 133 | /* a natural key was released */ 134 | if (natart[code] != CODE_UNDEF) { 135 | natdown[code] = false; 136 | numnat--; 137 | if (!keycomb[code]) { 138 | gettimeofday(&endwait[code], NULL); 139 | /* if the timeout wasn't reached since natural was pressed */ 140 | if (deltamsec(endwait[code], startwait[code]) < ARTIFICIAL_TIMEOUT ) { 141 | /* we send key Press/Release events for the artificial keycode */ 142 | XTestFakeKeyEvent(ctldpy, natart[code], true, CurrentTime); 143 | XTestFakeKeyEvent(ctldpy, natart[code], false, CurrentTime); 144 | } 145 | } 146 | keycomb[code] = false; 147 | } 148 | } else if (evttype == ButtonPress && numnat > 0) { 149 | int i; 150 | for (i = 0; i < MAX_CODE; i++) 151 | keycomb[i] = natdown[i]; 152 | } 153 | XRecordFreeData(hook); 154 | } 155 | 156 | void die(const char *errstr, ...) 157 | { 158 | va_list ap; 159 | va_start(ap, errstr); 160 | vfprintf(stderr, errstr, ap); 161 | va_end(ap); 162 | exit(EXIT_FAILURE); 163 | } 164 | 165 | int deltamsec(struct timeval t1, struct timeval t2) 166 | { 167 | return (((t1.tv_sec - t2.tv_sec) * 1000000) 168 | + (t1.tv_usec - t2.tv_usec)) / 1000; 169 | } 170 | 171 | void stop(int signum) 172 | { 173 | running = false; 174 | } 175 | 176 | void addpair(char *na) 177 | { 178 | char *natural, *artificial; 179 | int natcode, artcode; 180 | if (!(natural = strtok(na, PAIR_SEP)) || !(artificial = strtok(NULL, PAIR_SEP))) 181 | die("could not parse natart pair\n"); 182 | natcode = atoi(natural); 183 | artcode = atoi(artificial); 184 | natart[natcode] = artcode; 185 | } 186 | 187 | int main(int argc, char *argv[]) 188 | { 189 | int i; 190 | if (argc < 2) 191 | die("usage: %s NAT:ART ...\n", argv[0]); 192 | for (i = 0; i < MAX_CODE; i++) 193 | natart[i] = CODE_UNDEF; 194 | for (i = 1; i < argc; i++) 195 | addpair(argv[i]); 196 | 197 | signal(SIGINT, stop); 198 | signal(SIGTERM, stop); 199 | signal(SIGHUP, stop); 200 | 201 | setup(); 202 | loop(); 203 | 204 | if(!XRecordDisableContext(ctldpy, reccontext)) 205 | die("could not disable record context\n"); 206 | XRecordFreeContext(ctldpy, reccontext); 207 | XFlush(ctldpy); 208 | XFree(recrange); 209 | XCloseDisplay(datdpy); 210 | XCloseDisplay(ctldpy); 211 | 212 | return 0; 213 | } 214 | -------------------------------------------------------------------------------- /logo/keydouble_logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/baskerville/keydouble/8a03c9cee0d437206cc3ed3a661627a257f8c01b/logo/keydouble_logo.png --------------------------------------------------------------------------------