Compare commits

..

1 Commits

Author SHA1 Message Date
Jacopo De Simoi 98c10fe212 Be more lenient with compose+shift 6 years ago
  1. 10
      Makefile
  2. 69
      README.md
  3. 96
      xcape.1
  4. 159
      xcape.c

@ -1,6 +1,5 @@
INSTALL=install INSTALL=install
PREFIX=/usr PREFIX=/usr
MANDIR?=/local/man/man1
TARGET := xcape TARGET := xcape
@ -9,18 +8,13 @@ CFLAGS += `pkg-config --cflags xtst x11`
LDFLAGS += `pkg-config --libs xtst x11` LDFLAGS += `pkg-config --libs xtst x11`
LDFLAGS += -pthread LDFLAGS += -pthread
all: $(TARGET)
$(TARGET): xcape.c $(TARGET): xcape.c
$(CC) $(CFLAGS) -o $@ $< $(LDFLAGS) $(CC) $(CFLAGS) -o $@ $< $(LDFLAGS)
install: install:
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)/bin $(INSTALL) -Dm 755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
$(INSTALL) -d -m 0755 $(DESTDIR)$(PREFIX)$(MANDIR)
$(INSTALL) -m 0755 $(TARGET) $(DESTDIR)$(PREFIX)/bin/$(TARGET)
$(INSTALL) -m 0644 xcape.1 $(DESTDIR)$(PREFIX)$(MANDIR)/xcape.1
clean: clean:
rm $(TARGET) rm $(TARGET)
.PHONY: all clean install .PHONY: clean

@ -12,38 +12,24 @@ editor ;)
Minimal building instructions Minimal building instructions
----------------------------- -----------------------------
First install the development dependencies. On Debian-based systems $ sudo apt-get install git gcc make libx11-dev libxtst-dev pkg-config
(including Ubuntu and Linux Mint), run: $ mkdir xcape
$ sudo apt-get install git gcc make pkg-config libx11-dev libxtst-dev libxi-dev
On Fedora-based systems, run:
$ sudo dnf install git gcc make pkgconfig libX11-devel libXtst-devel libXi-devel
Then run:
$ git clone https://github.com/alols/xcape.git
$ cd xcape $ cd xcape
$ git clone https://github.com/alols/xcape.git .
$ make $ make
$ sudo make install
Usage Usage
----- -----
$ xcape [-d] [-f] [-t <timeout ms>] [-e <map-expression>] $ xcape [-d] [-t <timeout ms>] [-e <map-expression>]
### `-d` ### `-d`
Debug mode. Does not fork into the background. Prints debug information. Debug mode. Does not fork into the background.
### `-f`
Foreground mode. Does not fork into the background.
### `-t <timeout ms>` ### `-t <timeout ms>`
If you hold a key longer than this timeout, xcape will not generate a key If you hold a key longer than this timeout, xcape will not generate a key
event. Default is 500 ms. event. Default is 50 ms.
### `-e <map-expression>` ### `-e <map-expression>`
@ -56,44 +42,25 @@ the actual name of the character. For example to generate "{" the
expression `'ModKey=Shift_L|bracketleft'` could be used (assuming that you expression `'ModKey=Shift_L|bracketleft'` could be used (assuming that you
have a key with "{" above "["). have a key with "{" above "[").
You can also specify keys in decimal (prefix `#`), octal (`#0`), or You can also specify ModKey in decimal (prefix `#`), octal (`#0`), or
hexadecimal (`#0x`). They will be interpreted as keycodes unless no corresponding hexadecimal (`#0x`). It will be interpreted as a keycode unless no corresponding
key name is found. key name is found.
#### Examples #### Examples
+ This will make Left Shift generate Escape when pressed and released on 1. This will make Left Shift generate Escape when pressed and released on
its own, and Left Control generate Ctrl-O combination when pressed and it's own, and Left Control generate Ctrl-O combination when pressed and
released on its own. released on it's own.
xcape -e 'Shift_L=Escape;Control_L=Control_L|O' xcape -e 'Shift_L=Escape;Control_L=Control_L|O'
+ In conjunction with xmodmap it is possible to make an ordinary key act 2. If your `s` key has the code `42` and your `l` key `43` and you have set both
as an extra modifier. First map the key to the modifier with xmodmap to `AltGr` (a.k.a. ISO_Level3_Shift) with xmodmap, then this will generate the
and then the modifier back to the key with xcape. However, this has ordinary letters when pressed and released on their own. But pressed together
several limitations: the key will not work as ordinary until it is with another key, the `s` or `l` key will produce `AltGr`. So, depending on your
released, and in particular, *it may act as a modifier unintentionally if keyboard layout, you can compose e.g. `@`, `{` or `³` easily when touch-typing.
you type too fast.* This is not a bug in xcape, but an unavoidable
consequence of using these two tools together in this way. xcape -e '#42=s;#43=l'
As an example, we can make the space bar work as an additional ctrl
key when held (similar to
[Space2ctrl](https://github.com/r0adrunner/Space2Ctrl)) with the
following sequence of commands.
# Map an unused modifier's keysym to the spacebar's keycode and make it a
# control modifier. It needs to be an existing key so that emacs won't
# spazz out when you press it. Hyper_L is a good candidate.
spare_modifier="Hyper_L"
xmodmap -e "keycode 65 = $spare_modifier"
xmodmap -e "remove mod4 = $spare_modifier" # hyper_l is mod4 by default
xmodmap -e "add Control = $spare_modifier"
# Map space to an unused keycode (to keep it around for xcape to
# use).
xmodmap -e "keycode any = space"
# Finally use xcape to cause the space bar to generate a space when tapped.
xcape -e "$spare_modifier=space"
Note regarding xmodmap Note regarding xmodmap

@ -1,96 +0,0 @@
.TH XCAPE 1 2017-07-03 "John Hill" "xcape Manual"
.SH NAME
xcape \- use a modifier key as another key
.SH SYNOPSIS
.B xcape
[\fB-d\fR]
[\fB-f\fR]
[\fB-t\fR \fItimeout\fR]
[\fB-e\fR \fImap-expression\fR]
.SH DESCRIPTION
\fBxcape\fR allows a modifier key to be used as another key when it is pressed
and released on its own. The default behaviour is to generate the \fIEscape\fR
key in place of \fIControl_L\fR (Left Control).
.SH OPTIONS
.TP
.BR \-d
Debug mode. Will run as a foreground process and print debug information.
.TP
.BR \-f
Foreground mode. Will run as a foreground process.
.TP
.BR \-t " " \fItimeout\fR
Give a \fItimeout\fR in milliseconds. If you hold a key longer than
\fItimeout\fR a key event will not be generated.
.TP
.BR \-e " " \fImap-expression\fR
Use \fImap-expression\fR as the expression(s).
.SH EXPRESSION SYNTAX
Expression syntax is \'\fBModKey\fR=\fBKey\fR[|\fBOtherKey\fR]\'. Multiple
expressions can be passed, delimited by semi-colons (;).
.PP
A list of keysyms can be found in the header file <\fIX11/keysymdef.h\fR>
(without the \fIXK_\fR prefix).
.PP
Note that shifted keys must be specified as a shift key followed by the key to
be pressed rather than the actual name of the character. For example to
generate "\fI{\fR" the expression
\'\fIModKey\fR=\fIShift_L\fR|\fIbracketleft\fR\' could be used
(assuming that you have a key with \'{\' above \'[\').
.PP
You can also specify \fBModKey\fR in decimal (prefix \fI#\fR), octal
(\fI#0\fR), or hexadecimal (\fI#0x\fR). It will be interpreted as a keycode
unless no corresponding key name is
found.
.SH EXAMPLES
.PP
Make Left Shift generate Escape when pressed and released on it's own, and Left
Control generate Ctrl\-O combination when pressed and released on it's own:
.RS
\fBxcape\fR \fB-e\fR '\fIShift_L\fR=\fIEscape\fR;\fIControl_L\fR=\fIControl_L\fR|\fIO\fR'
.RE
.PP
In conjugation with xmodmap it is possible to make an ordinary key act as an
extra modifier. First map the key to the modifier with xmodmap and then the
modifier back to the key with \fBxcape\fR. As an example, we can make the space
bar work as an additional ctrl key when held with the following sequence of
commands:
.PP
First, map an unused modifier's keysym to the spacebar's keycode and make it a
control modifier. It needs to be an existing key so that emacs won't spazz out
when you press it. Hyper_L is a good candidate.
.PP
.RS
.nf
\fBspare_modifier\fR="\fIHyper_L\fR"
\fBxmodmap\fR \fB-e\fR "\fBkeycode\fR \fI65\fR = \fI$spare_modifier\fR"
\fBxmodmap\fR \fB-e\fR "\fBremove\fR \fImod4\fR = \fI$spare_modifier\fR"
# hyper_l is mod4 by default
\fBxmodmap\fR \fB-e\fR "\fBadd\fR \fIControl\fR = \fI$spare_modifier\fR"
.fi
.RE
.PP
Next, map space to an unused keycode (to keep it around for \fBxcape\fR to use).
.PP
.RS
\fBxmodmap\fR \fR-e\fR "\fBkeycode\fR \fIany\fR = \fIspace\fR"
.RE
.PP
Finally use \fBxcape\fR to cause the space bar to generate a space when tapped.
.PP
.RS
\fBxcape\fR \fB-e\fR "\fI$spare_modifier\fR=\fIspace\fR"
.RE
.SH SEE ALSO
\fBxmodmap\fR(1), \fBxev\fR(1)
.SH AUTHOR
\fBxcape\fR was written by Albin Olsson
(albin dot olsson at gmail dot com)

@ -1,7 +1,7 @@
/************************************************************************ /************************************************************************
* xcape.c * xcape.c
* *
* Copyright 2015 Albin Olsson * Copyright 2012 Albin Olsson
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by * it under the terms of the GNU General Public License as published by
@ -44,7 +44,7 @@ typedef struct _Key_t
typedef struct _KeyMap_t typedef struct _KeyMap_t
{ {
Bool UseKeyCode; /* (for from) instead of KeySym; ignore latter */ Bool UseKeyCode; // (for from) instead of KeySym; ignore latter
KeySym from_ks; KeySym from_ks;
KeyCode from_kc; KeyCode from_kc;
Key_t *to_keys; Key_t *to_keys;
@ -62,11 +62,11 @@ typedef struct _XCape_t
XRecordContext record_ctx; XRecordContext record_ctx;
pthread_t sigwait_thread; pthread_t sigwait_thread;
sigset_t sigset; sigset_t sigset;
Bool foreground;
Bool debug; Bool debug;
KeyMap_t *map; KeyMap_t *map;
Key_t *generated; Key_t *generated;
struct timeval timeout; struct timeval timeout;
Bool timeout_valid;
} XCape_t; } XCape_t;
/************************************************************************ /************************************************************************
@ -78,47 +78,29 @@ void intercept (XPointer user_data, XRecordInterceptData *data);
KeyMap_t *parse_mapping (Display *ctrl_conn, char *mapping, Bool debug); KeyMap_t *parse_mapping (Display *ctrl_conn, char *mapping, Bool debug);
void delete_mapping (KeyMap_t *map);
Key_t *key_add_key (Key_t *keys, KeyCode key); Key_t *key_add_key (Key_t *keys, KeyCode key);
void delete_keys (Key_t *keys);
void print_usage (const char *program_name);
/************************************************************************ /************************************************************************
* Main function * Main function
***********************************************************************/ ***********************************************************************/
int main (int argc, char **argv) int main (int argc, char **argv)
{ {
XCape_t *self = malloc (sizeof (XCape_t)); XCape_t *self = malloc (sizeof (XCape_t));
int dummy, ch; int dummy, ch;
static char default_mapping[] = "Control_L=Escape"; static char default_mapping[] = "Control_L=Escape";
char *mapping = default_mapping; char *mapping = default_mapping;
XRecordRange *rec_range = XRecordAllocRange();
XRecordClientSpec client_spec = XRecordAllClients;
self->foreground = False;
self->debug = False; self->debug = False;
self->timeout.tv_sec = 0; self->timeout.tv_sec = 0;
self->timeout.tv_usec = 500000; self->timeout.tv_usec = 500000;
self->timeout_valid = True;
self->generated = NULL; self->generated = NULL;
rec_range->device_events.first = KeyPress; while ((ch = getopt (argc, argv, "de:t:")) != -1)
rec_range->device_events.last = ButtonRelease;
while ((ch = getopt (argc, argv, "dfe:t:")) != -1)
{ {
switch (ch) switch (ch)
{ {
case 'd': case 'd':
self->debug = True; self->debug = True;
/* imply -f (no break) */
case 'f':
self->foreground = True;
break; break;
case 'e': case 'e':
mapping = optarg; mapping = optarg;
@ -128,36 +110,24 @@ int main (int argc, char **argv)
int ms = atoi (optarg); int ms = atoi (optarg);
if (ms > 0) if (ms > 0)
{ {
self->timeout_valid = True;
self->timeout.tv_sec = ms / 1000; self->timeout.tv_sec = ms / 1000;
self->timeout.tv_usec = (ms % 1000) * 1000; self->timeout.tv_usec = (ms % 1000) * 1000;
} }
else else
{ {
fprintf (stderr, "Invalid argument for '-t': %s.\n", optarg); self->timeout_valid = False;
print_usage (argv[0]);
return EXIT_FAILURE;
} }
} }
break; break;
default: default:
print_usage (argv[0]); fprintf (stdout, "Usage: %s [-d] [-t timeout_ms] [-e <mapping>]\n", argv[0]);
fprintf (stdout,
"Runs as a daemon unless -d flag is set\n");
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
} }
if (optind < argc)
{
fprintf (stderr, "Not a command line option: '%s'\n", argv[optind]);
print_usage (argv[0]);
return EXIT_SUCCESS;
}
if (!XInitThreads ())
{
fprintf (stderr, "Failed to initialize threads.\n");
exit (EXIT_FAILURE);
}
self->data_conn = XOpenDisplay (NULL); self->data_conn = XOpenDisplay (NULL);
self->ctrl_conn = XOpenDisplay (NULL); self->ctrl_conn = XOpenDisplay (NULL);
@ -187,12 +157,9 @@ int main (int argc, char **argv)
self->map = parse_mapping (self->ctrl_conn, mapping, self->debug); self->map = parse_mapping (self->ctrl_conn, mapping, self->debug);
if (self->map == NULL) if (self->map == NULL)
{
fprintf (stderr, "Failed to parse_mapping\n");
exit (EXIT_FAILURE); exit (EXIT_FAILURE);
}
if (self->foreground != True) if (self->debug != True)
daemon (0, 0); daemon (0, 0);
sigemptyset (&self->sigset); sigemptyset (&self->sigset);
@ -203,6 +170,11 @@ int main (int argc, char **argv)
pthread_create (&self->sigwait_thread, pthread_create (&self->sigwait_thread,
NULL, sig_handler, self); NULL, sig_handler, self);
XRecordRange *rec_range = XRecordAllocRange();
rec_range->device_events.first = KeyPress;
rec_range->device_events.last = ButtonRelease;
XRecordClientSpec client_spec = XRecordAllClients;
self->record_ctx = XRecordCreateContext (self->ctrl_conn, self->record_ctx = XRecordCreateContext (self->ctrl_conn,
0, &client_spec, 1, &rec_range, 1); 0, &client_spec, 1, &rec_range, 1);
@ -221,23 +193,15 @@ int main (int argc, char **argv)
exit (EXIT_FAILURE); exit (EXIT_FAILURE);
} }
pthread_join (self->sigwait_thread, NULL);
if (!XRecordFreeContext (self->ctrl_conn, self->record_ctx)) if (!XRecordFreeContext (self->ctrl_conn, self->record_ctx))
{ {
fprintf (stderr, "Failed to free xrecord context\n"); fprintf (stderr, "Failed to free xrecord context\n");
} }
if (self->debug) fprintf (stdout, "main exiting\n");
XFree (rec_range);
XCloseDisplay (self->ctrl_conn); XCloseDisplay (self->ctrl_conn);
XCloseDisplay (self->data_conn); XCloseDisplay (self->data_conn);
delete_mapping (self->map); if (self->debug) fprintf (stdout, "main exiting\n");
free (self);
return EXIT_SUCCESS; return EXIT_SUCCESS;
} }
@ -257,8 +221,6 @@ void *sig_handler (void *user_data)
if (self->debug) fprintf (stdout, "Caught signal %d!\n", sig); if (self->debug) fprintf (stdout, "Caught signal %d!\n", sig);
XLockDisplay (self->ctrl_conn);
if (!XRecordDisableContext (self->ctrl_conn, if (!XRecordDisableContext (self->ctrl_conn,
self->record_ctx)) self->record_ctx))
{ {
@ -268,8 +230,6 @@ void *sig_handler (void *user_data)
XSync (self->ctrl_conn, False); XSync (self->ctrl_conn, False);
XUnlockDisplay (self->ctrl_conn);
if (self->debug) fprintf (stdout, "sig_handler exiting...\n"); if (self->debug) fprintf (stdout, "sig_handler exiting...\n");
return NULL; return NULL;
@ -307,6 +267,7 @@ void handle_key (XCape_t *self, KeyMap_t *key,
key->pressed = True; key->pressed = True;
if (self->timeout_valid)
gettimeofday (&key->down_at, NULL); gettimeofday (&key->down_at, NULL);
if (mouse_pressed) if (mouse_pressed)
@ -320,10 +281,13 @@ void handle_key (XCape_t *self, KeyMap_t *key,
if (key->used == False) if (key->used == False)
{ {
struct timeval timev = self->timeout; struct timeval timev = self->timeout;
if (self->timeout_valid)
{
gettimeofday (&timev, NULL); gettimeofday (&timev, NULL);
timersub (&timev, &key->down_at, &timev); timersub (&timev, &key->down_at, &timev);
}
if (timercmp (&timev, &self->timeout, <)) if (!self->timeout_valid || timercmp (&timev, &self->timeout, <))
{ {
for (k = key->to_keys; k != NULL; k = k->next) for (k = key->to_keys; k != NULL; k = k->next)
{ {
@ -355,8 +319,6 @@ void intercept (XPointer user_data, XRecordInterceptData *data)
static Bool mouse_pressed = False; static Bool mouse_pressed = False;
KeyMap_t *km; KeyMap_t *km;
XLockDisplay (self->ctrl_conn);
if (data->category == XRecordFromServer) if (data->category == XRecordFromServer)
{ {
int key_event = data->data[0]; int key_event = data->data[0];
@ -395,13 +357,9 @@ void intercept (XPointer user_data, XRecordInterceptData *data)
{ {
mouse_pressed = False; mouse_pressed = False;
} }
for (km = self->map; km != NULL; km = km->next) else
{ {
if ((km->UseKeyCode == False for (km = self->map; km != NULL; km = km->next)
&& XkbKeycodeToKeysym (self->ctrl_conn, key_code, 0, 0)
== km->from_ks)
|| (km->UseKeyCode == True
&& key_code == km->from_kc))
{ {
if ((km->UseKeyCode == False if ((km->UseKeyCode == False
&& XkbKeycodeToKeysym (self->ctrl_conn, key_code, 0, 0) && XkbKeycodeToKeysym (self->ctrl_conn, key_code, 0, 0)
@ -411,7 +369,7 @@ void intercept (XPointer user_data, XRecordInterceptData *data)
{ {
handle_key (self, km, mouse_pressed, key_event); handle_key (self, km, mouse_pressed, key_event);
} }
else if (km->pressed && (key_event == KeyPress || key_event == ButtonPress)) else if (km->pressed && key_event == KeyPress)
{ {
/* We should check if the pressed key is a modifier before marking the key as used. */ /* We should check if the pressed key is a modifier before marking the key as used. */
if (key_code != 50) /* hack; try if it works */ if (key_code != 50) /* hack; try if it works */
@ -424,7 +382,6 @@ void intercept (XPointer user_data, XRecordInterceptData *data)
} }
exit: exit:
XUnlockDisplay (self->ctrl_conn);
XRecordFreeData (data); XRecordFreeData (data);
} }
@ -434,8 +391,8 @@ KeyMap_t *parse_token (Display *dpy, char *token, Bool debug)
KeyMap_t *km = NULL; KeyMap_t *km = NULL;
KeySym ks; KeySym ks;
char *from, *to, *key; char *from, *to, *key;
KeyCode code; /* keycode */ KeyCode code; // keycode (to)
long parsed_code; /* parsed keycode value */ long fromcode; // keycode (from)
to = token; to = token;
from = strsep (&to, "="); from = strsep (&to, "=");
@ -447,17 +404,17 @@ KeyMap_t *parse_token (Display *dpy, char *token, Bool debug)
&& strsep (&from, "#") != NULL) && strsep (&from, "#") != NULL)
{ {
errno = 0; errno = 0;
parsed_code = strtoul (from, NULL, 0); /* dec, oct, hex automatically */ fromcode = strtoul (from, NULL, 0); // dec, oct, hex automatically
if (errno == 0 if (errno == 0
&& parsed_code <=255 && fromcode <=255
&& XkbKeycodeToKeysym (dpy, (KeyCode) parsed_code, 0, 0) != NoSymbol) && XkbKeycodeToKeysym (dpy, (KeyCode) fromcode, 0, 0) != NoSymbol)
{ {
km->UseKeyCode = True; km->UseKeyCode = True;
km->from_kc = (KeyCode) parsed_code; km->from_kc = (KeyCode) fromcode;
if (debug) if (debug)
{ {
KeySym ks_temp = XkbKeycodeToKeysym (dpy, (KeyCode) parsed_code, 0, 0); KeySym ks_temp = XkbKeycodeToKeysym (dpy, (KeyCode) fromcode, 0, 0);
fprintf(stderr, "Assigned mapping from \"%s\" ( keysym 0x%x, " fprintf(stderr, "Assigned mapping from from \"%s\" ( keysym 0x%x, "
"key code %d)\n", "key code %d)\n",
XKeysymToString(ks_temp), XKeysymToString(ks_temp),
(unsigned) ks_temp, (unsigned) ks_temp,
@ -498,23 +455,6 @@ KeyMap_t *parse_token (Display *dpy, char *token, Bool debug)
if (key == NULL) if (key == NULL)
break; break;
if (!strncmp (key, "#", 1)
&& strsep (&key, "#") != NULL)
{
errno = 0;
parsed_code = strtoul (key, NULL, 0); /* dec, oct, hex automatically */
if (!(errno == 0
&& parsed_code <=255
&& XkbKeycodeToKeysym (dpy, (KeyCode) parsed_code, 0, 0) != NoSymbol))
{
fprintf (stderr, "Invalid keycode: %s\n", key);
return NULL;
}
code = (KeyCode) parsed_code;
}
else
{
if ((ks = XStringToKeysym (key)) == NoSymbol) if ((ks = XStringToKeysym (key)) == NoSymbol)
{ {
fprintf (stderr, "Invalid key: %s\n", key); fprintf (stderr, "Invalid key: %s\n", key);
@ -529,15 +469,13 @@ KeyMap_t *parse_token (Display *dpy, char *token, Bool debug)
"mapping.\n", key, (unsigned int)ks, token); "mapping.\n", key, (unsigned int)ks, token);
return NULL; return NULL;
} }
}
km->to_keys = key_add_key (km->to_keys, code); km->to_keys = key_add_key (km->to_keys, code);
if (debug) if (debug)
{ {
KeySym ks_temp = XkbKeycodeToKeysym (dpy, code, 0, 0);
fprintf(stderr, "to \"%s\" (keysym 0x%x, key code %d)\n", fprintf(stderr, "to \"%s\" (keysym 0x%x, key code %d)\n",
XKeysymToString(ks_temp), key,
(unsigned) ks_temp, (unsigned) XStringToKeysym (key),
(unsigned) code); (unsigned) code);
} }
} }
@ -578,28 +516,3 @@ KeyMap_t *parse_mapping (Display *ctrl_conn, char *mapping, Bool debug)
return rval; return rval;
} }
void delete_mapping (KeyMap_t *map)
{
while (map != NULL) {
KeyMap_t *next = map->next;
delete_keys (map->to_keys);
free (map);
map = next;
}
}
void delete_keys (Key_t *keys)
{
while (keys != NULL) {
Key_t *next = keys->next;
free (keys);
keys = next;
}
}
void print_usage (const char *program_name)
{
fprintf (stdout, "Usage: %s [-d] [-f] [-t timeout_ms] [-e <mapping>]\n", program_name);
fprintf (stdout, "Runs as a daemon unless -d or -f flag is set\n");
}

Loading…
Cancel
Save