13 Commits

Author SHA1 Message Date
83fff50fa0 Added ligature-alpha-scrollback (w/o alpha) v0.9 patch 2023-06-20 23:59:05 +02:00
f2d4bbdbea Added scrollback-reflow v0.8.5 patch 2023-06-20 23:27:00 +02:00
9d89b6a5d0 Added scrollback v0.8.5 patch 2023-06-20 23:21:23 +02:00
1478e0d6ed Added .gitignore 2023-06-20 22:59:57 +02:00
211964d56e ignore C1 control characters in UTF-8 mode
Ignore processing and printing C1 control characters in UTF-8 mode.
These are in the range: 0x80 - 0x9f.

By default in st the mode is set to UTF-8.

This matches more the behaviour of xterm with the options -u8 or +u8 also.
Also see the xterm resource "allowC1Printable".

Let me know if this breaks something, in most cases I don't think so.

As usual a very good reference is:
https://invisible-island.net/xterm/ctlseqs/ctlseqs.html
2023-02-07 20:00:59 +01:00
f17abd25b3 Add support for DSR response "OK" escape sequence
"VT100 defines an escape sequence [1] called Device Status Report (DSR). When
the DSR sequence received is `csi 5n`, an "OK" response `csi 0n` is returned.
This patch adds that "OK" response.

I encountered this missing sequence when I noticed that fzf [2] would clobber
my prompt whenever completing a find.

To test that ST doesn't currently respond to `csi 5n`, use fzf's shell
extension in ST's repo to complete the path for a file.

    my-fancy-prompt $ vim **<tab>
    <select a file>
    st.c

Select a file with <enter>, and notice that fzf clobbers some or all of your
prompt.

After applying this patch, do the same test as above and notice that fzf has no
longer clobbered your prompt by placing the file name in the correct position
in your command.

    my-fancy-prompt $ vim **<tab>
    <select a file>
    my-fancy prompt $ vim st.c

Thank you for considering my first patch submission.

[1] https://www.xfree86.org/current/ctlseqs.html#VT100%20Mode
[2] https://github.com/junegunn/fzf
"

Patch slightly adapted with input from the mailinglist,
2023-02-07 19:57:34 +01:00
7e8050cc62 Fixed OSC color reset without parameter->resets all colors
Adapted from (garbled) patch by wim <wim@thinkerwim.org>

Additional notes: it should reset all the colors using xloadcols().
To reproduce: set a different (theme) color using some escape code, then reset
it:

	printf '\x1b]104\x07'
2023-02-05 13:29:35 +01:00
e5e959835b fix buffer overflow when handling long composed input
To reproduce the issue:

"
If you already have the multi-key enabled on your system, then add this line
to your ~/.XCompose file:

[...]
<question> <T> <E> <S> <T> <question> :
"1234567890123456789012345678901234567890123456789012345678901234567890"
"

Reported by and an initial patch by Andy Gozas <andy@gozas.me>, thanks!

Adapted the patch, for now st (like dmenu) handles a fixed amount of composed
characters, or otherwise ignores it. This is done for simplicity sake.
2022-10-25 17:11:11 +02:00
68d1ad9b54 bump version to 0.9 2022-10-04 19:40:30 +02:00
0008519903 FAQ: document the color emojis crash issue which affected some systems is fixed
It is fixed in libXft 2.3.6:

https://gitlab.freedesktop.org/xorg/lib/libxft/-/blob/libXft-2.3.5/NEWS
2022-09-16 23:07:09 +02:00
72fd32736a st: use `void' to indicate an empty parameter list 2022-08-18 17:14:10 +02:00
baa9357e96 Makefile: add manual path for OpenBSD 2022-05-01 18:38:40 +02:00
NRK
8629d9a1da code-golfing: cleanup osc color related code
* adds missing function prototype
* move xgetcolor() prototype to win.h (that's where all the other x.c
  func prototype seems to be declared at)
* check for snprintf error/truncation
* reduces code duplication for osc 10/11/12
* unify osc_color_response() and osc4_color_response() into a single function

the latter two was suggested by Quentin Rameau in his patch review on
the hackers list.
2022-04-19 11:43:37 +02:00
11 changed files with 1070 additions and 454 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
*.o
*.orig

3
FAQ
View File

@ -248,3 +248,6 @@ fonts:
Please don't bother reporting this bug to st, but notify the upstream Xft
developers about fixing this bug.
As of 2022-09-05 this now seems to be finally fixed in libXft 2.3.5:
https://gitlab.freedesktop.org/xorg/lib/libxft/-/blob/libXft-2.3.5/NEWS

View File

@ -4,7 +4,7 @@
include config.mk
SRC = st.c x.c
SRC = st.c x.c hb.c
OBJ = $(SRC:.c=.o)
all: options st
@ -22,7 +22,8 @@ config.h:
$(CC) $(STCFLAGS) -c $<
st.o: config.h st.h win.h
x.o: arg.h config.h st.h win.h
x.o: arg.h config.h st.h win.h hb.h
hb.o: st.h
$(OBJ): config.h config.mk

View File

@ -201,6 +201,8 @@ static Shortcut shortcuts[] = {
{ TERMMOD, XK_Y, selpaste, {.i = 0} },
{ ShiftMask, XK_Insert, selpaste, {.i = 0} },
{ TERMMOD, XK_Num_Lock, numlock, {.i = 0} },
{ ShiftMask, XK_Page_Up, kscrollup, {.i = -1} },
{ ShiftMask, XK_Page_Down, kscrolldown, {.i = -1} },
};
/*

View File

@ -1,5 +1,5 @@
# st version
VERSION = 0.8.5
VERSION = 0.9
# Customize below to fit your system
@ -15,10 +15,12 @@ PKG_CONFIG = pkg-config
# includes and libs
INCS = -I$(X11INC) \
`$(PKG_CONFIG) --cflags fontconfig` \
`$(PKG_CONFIG) --cflags freetype2`
`$(PKG_CONFIG) --cflags freetype2` \
`$(PKG_CONFIG) --cflags harfbuzz`
LIBS = -L$(X11LIB) -lm -lrt -lX11 -lutil -lXft \
`$(PKG_CONFIG) --libs fontconfig` \
`$(PKG_CONFIG) --libs freetype2`
`$(PKG_CONFIG) --libs freetype2` \
`$(PKG_CONFIG) --cflags harfbuzz`
# flags
STCPPFLAGS = -DVERSION=\"$(VERSION)\" -D_XOPEN_SOURCE=600
@ -30,6 +32,7 @@ STLDFLAGS = $(LIBS) $(LDFLAGS)
#LIBS = -L$(X11LIB) -lm -lX11 -lutil -lXft \
# `$(PKG_CONFIG) --libs fontconfig` \
# `$(PKG_CONFIG) --libs freetype2`
#MANPREFIX = ${PREFIX}/man
# compiler and linker
# CC = c99

124
hb.c Normal file
View File

@ -0,0 +1,124 @@
#include <stdlib.h>
#include <stdio.h>
#include <math.h>
#include <X11/Xft/Xft.h>
#include <X11/cursorfont.h>
#include <hb.h>
#include <hb-ft.h>
#include "st.h"
#include "hb.h"
#define FEATURE(c1,c2,c3,c4) { .tag = HB_TAG(c1,c2,c3,c4), .value = 1, .start = HB_FEATURE_GLOBAL_START, .end = HB_FEATURE_GLOBAL_END }
#define BUFFER_STEP 256
hb_font_t *hbfindfont(XftFont *match);
typedef struct {
XftFont *match;
hb_font_t *font;
} HbFontMatch;
typedef struct {
size_t capacity;
HbFontMatch *fonts;
} HbFontCache;
static HbFontCache hbfontcache = { 0, NULL };
typedef struct {
size_t capacity;
Rune *runes;
} RuneBuffer;
static RuneBuffer hbrunebuffer = { 0, NULL };
/*
* Poplulate the array with a list of font features, wrapped in FEATURE macro,
* e. g.
* FEATURE('c', 'a', 'l', 't'), FEATURE('d', 'l', 'i', 'g')
*/
hb_feature_t features[] = { };
void
hbunloadfonts()
{
for (int i = 0; i < hbfontcache.capacity; i++) {
hb_font_destroy(hbfontcache.fonts[i].font);
XftUnlockFace(hbfontcache.fonts[i].match);
}
if (hbfontcache.fonts != NULL) {
free(hbfontcache.fonts);
hbfontcache.fonts = NULL;
}
hbfontcache.capacity = 0;
}
hb_font_t *
hbfindfont(XftFont *match)
{
for (int i = 0; i < hbfontcache.capacity; i++) {
if (hbfontcache.fonts[i].match == match)
return hbfontcache.fonts[i].font;
}
/* Font not found in cache, caching it now. */
hbfontcache.fonts = realloc(hbfontcache.fonts, sizeof(HbFontMatch) * (hbfontcache.capacity + 1));
FT_Face face = XftLockFace(match);
hb_font_t *font = hb_ft_font_create(face, NULL);
if (font == NULL)
die("Failed to load Harfbuzz font.");
hbfontcache.fonts[hbfontcache.capacity].match = match;
hbfontcache.fonts[hbfontcache.capacity].font = font;
hbfontcache.capacity += 1;
return font;
}
void hbtransform(HbTransformData *data, XftFont *xfont, const Glyph *glyphs, int start, int length) {
ushort mode = USHRT_MAX;
unsigned int glyph_count;
int rune_idx, glyph_idx, end = start + length;
hb_font_t *font = hbfindfont(xfont);
if (font == NULL)
return;
hb_buffer_t *buffer = hb_buffer_create();
hb_buffer_set_direction(buffer, HB_DIRECTION_LTR);
/* Resize the buffer if required length is larger. */
if (hbrunebuffer.capacity < length) {
hbrunebuffer.capacity = (length / BUFFER_STEP + 1) * BUFFER_STEP;
hbrunebuffer.runes = realloc(hbrunebuffer.runes, hbrunebuffer.capacity * sizeof(Rune));
}
/* Fill buffer with codepoints. */
for (rune_idx = 0, glyph_idx = start; glyph_idx < end; glyph_idx++, rune_idx++) {
hbrunebuffer.runes[rune_idx] = glyphs[glyph_idx].u;
mode = glyphs[glyph_idx].mode;
if (mode & ATTR_WDUMMY)
hbrunebuffer.runes[rune_idx] = 0x0020;
}
hb_buffer_add_codepoints(buffer, hbrunebuffer.runes, length, 0, length);
/* Shape the segment. */
hb_shape(font, buffer, features, sizeof(features)/sizeof(hb_feature_t));
/* Get new glyph info. */
hb_glyph_info_t *info = hb_buffer_get_glyph_infos(buffer, &glyph_count);
hb_glyph_position_t *pos = hb_buffer_get_glyph_positions(buffer, &glyph_count);
/* Fill the output. */
data->buffer = buffer;
data->glyphs = info;
data->positions = pos;
data->count = glyph_count;
}
void hbcleanup(HbTransformData *data) {
hb_buffer_destroy(data->buffer);
memset(data, 0, sizeof(HbTransformData));
}

14
hb.h Normal file
View File

@ -0,0 +1,14 @@
#include <X11/Xft/Xft.h>
#include <hb.h>
#include <hb-ft.h>
typedef struct {
hb_buffer_t *buffer;
hb_glyph_info_t *glyphs;
hb_glyph_position_t *positions;
unsigned int count;
} HbTransformData;
void hbunloadfonts();
void hbtransform(HbTransformData *, XftFont *, const Glyph *, int, int);
void hbcleanup(HbTransformData *);

1018
st.c

File diff suppressed because it is too large Load Diff

32
st.h
View File

@ -11,7 +11,8 @@
#define DIVCEIL(n, d) (((n) + ((d) - 1)) / (d))
#define DEFAULT(a, b) (a) = (a) ? (a) : (b)
#define LIMIT(x, a, b) (x) = (x) < (a) ? (a) : (x) > (b) ? (b) : (x)
#define ATTRCMP(a, b) ((a).mode != (b).mode || (a).fg != (b).fg || \
#define ATTRCMP(a, b) (((a).mode & (~ATTR_WRAP)) != ((b).mode & (~ATTR_WRAP)) || \
(a).fg != (b).fg || \
(a).bg != (b).bg)
#define TIMEDIFF(t1, t2) ((t1.tv_sec-t2.tv_sec)*1000 + \
(t1.tv_nsec-t2.tv_nsec)/1E6)
@ -22,17 +23,19 @@
enum glyph_attribute {
ATTR_NULL = 0,
ATTR_BOLD = 1 << 0,
ATTR_FAINT = 1 << 1,
ATTR_ITALIC = 1 << 2,
ATTR_UNDERLINE = 1 << 3,
ATTR_BLINK = 1 << 4,
ATTR_REVERSE = 1 << 5,
ATTR_INVISIBLE = 1 << 6,
ATTR_STRUCK = 1 << 7,
ATTR_WRAP = 1 << 8,
ATTR_WIDE = 1 << 9,
ATTR_WDUMMY = 1 << 10,
ATTR_SET = 1 << 0,
ATTR_BOLD = 1 << 1,
ATTR_FAINT = 1 << 2,
ATTR_ITALIC = 1 << 3,
ATTR_UNDERLINE = 1 << 4,
ATTR_BLINK = 1 << 5,
ATTR_REVERSE = 1 << 6,
ATTR_INVISIBLE = 1 << 7,
ATTR_STRUCK = 1 << 8,
ATTR_WRAP = 1 << 9,
ATTR_WIDE = 1 << 10,
ATTR_WDUMMY = 1 << 11,
ATTR_SELECTED = 1 << 12,
ATTR_BOLD_FAINT = ATTR_BOLD | ATTR_FAINT,
};
@ -81,6 +84,8 @@ void die(const char *, ...);
void redraw(void);
void draw(void);
void kscrolldown(const Arg *);
void kscrollup(const Arg *);
void printscreen(const Arg *);
void printsel(const Arg *);
void sendbreak(const Arg *);
@ -88,6 +93,7 @@ void toggleprinter(const Arg *);
int tattrset(int);
void tnew(int, int);
int tisaltscreen(void);
void tresize(int, int);
void tsetdirtattr(int);
void ttyhangup(void);
@ -111,8 +117,6 @@ void *xmalloc(size_t);
void *xrealloc(void *, size_t);
char *xstrdup(const char *);
int xgetcolor(int x, unsigned char *r, unsigned char *g, unsigned char *b);
/* config.h globals */
extern char *utmp;
extern char *scroll;

3
win.h
View File

@ -25,11 +25,12 @@ enum win_mode {
void xbell(void);
void xclipcopy(void);
void xdrawcursor(int, int, Glyph, int, int, Glyph);
void xdrawcursor(int, int, Glyph, int, int, Glyph, Line, int);
void xdrawline(Line, int, int, int);
void xfinishdraw(void);
void xloadcols(void);
int xsetcolorname(int, const char *);
int xgetcolor(int, unsigned char *, unsigned char *, unsigned char *);
void xseticontitle(char *);
void xsettitle(char *);
int xsetcursor(int);

150
x.c
View File

@ -19,6 +19,7 @@ char *argv0;
#include "arg.h"
#include "st.h"
#include "win.h"
#include "hb.h"
/* types used in config.h */
typedef struct {
@ -141,6 +142,7 @@ typedef struct {
} DC;
static inline ushort sixd_to_16bit(int);
static void xresetfontsettings(ushort mode, Font **font, int *frcflags);
static int xmakeglyphfontspecs(XftGlyphFontSpec *, const Glyph *, int, int, int);
static void xdrawglyphfontspecs(const XftGlyphFontSpec *, Glyph, int, int, int);
static void xdrawglyph(Glyph, int, int);
@ -757,7 +759,7 @@ xresize(int col, int row)
xclear(0, 0, win.w, win.h);
/* resize to new width */
xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec));
xw.specbuf = xrealloc(xw.specbuf, col * sizeof(GlyphFontSpec) * 4);
}
ushort
@ -1062,6 +1064,9 @@ xunloadfont(Font *f)
void
xunloadfonts(void)
{
/* Clear Harfbuzz font cache. */
hbunloadfonts();
/* Free the loaded fonts in the font cache. */
while (frclen > 0)
XftFontClose(xw.dpy, frc[--frclen].font);
@ -1185,7 +1190,7 @@ xinit(int cols, int rows)
XFillRectangle(xw.dpy, xw.buf, dc.gc, 0, 0, win.w, win.h);
/* font spec buffer */
xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec));
xw.specbuf = xmalloc(cols * sizeof(GlyphFontSpec) * 4);
/* Xft rendering context */
xw.draw = XftDrawCreate(xw.dpy, xw.buf, xw.vis, xw.cmap);
@ -1239,6 +1244,22 @@ xinit(int cols, int rows)
xsel.xtarget = XA_STRING;
}
void
xresetfontsettings(ushort mode, Font **font, int *frcflags)
{
*font = &dc.font;
if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
*font = &dc.ibfont;
*frcflags = FRC_ITALICBOLD;
} else if (mode & ATTR_ITALIC) {
*font = &dc.ifont;
*frcflags = FRC_ITALIC;
} else if (mode & ATTR_BOLD) {
*font = &dc.bfont;
*frcflags = FRC_BOLD;
}
}
int
xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x, int y)
{
@ -1253,49 +1274,65 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
FcPattern *fcpattern, *fontpattern;
FcFontSet *fcsets[] = { NULL };
FcCharSet *fccharset;
int i, f, numspecs = 0;
int i, f, length = 0, start = 0, numspecs = 0;
float cluster_xp = xp, cluster_yp = yp;
HbTransformData shaped = { 0 };
/* Initial values. */
mode = prevmode = glyphs[0].mode;
xresetfontsettings(mode, &font, &frcflags);
for (i = 0, xp = winx, yp = winy + font->ascent; i < len; ++i) {
/* Fetch rune and mode for current glyph. */
rune = glyphs[i].u;
mode = glyphs[i].mode;
/* Skip dummy wide-character spacing. */
if (mode == ATTR_WDUMMY)
if (mode & ATTR_WDUMMY && i < (len - 1))
continue;
/* Determine font for glyph if different from previous glyph. */
if (prevmode != mode) {
prevmode = mode;
font = &dc.font;
frcflags = FRC_NORMAL;
runewidth = win.cw * ((mode & ATTR_WIDE) ? 2.0f : 1.0f);
if ((mode & ATTR_ITALIC) && (mode & ATTR_BOLD)) {
font = &dc.ibfont;
frcflags = FRC_ITALICBOLD;
} else if (mode & ATTR_ITALIC) {
font = &dc.ifont;
frcflags = FRC_ITALIC;
} else if (mode & ATTR_BOLD) {
font = &dc.bfont;
frcflags = FRC_BOLD;
}
yp = winy + font->ascent;
if (
prevmode != mode
|| ATTRCMP(glyphs[start], glyphs[i])
|| selected(x + i, y) != selected(x + start, y)
|| i == (len - 1)
) {
/* Handle 1-character wide segments and end of line */
length = i - start;
if (i == start) {
length = 1;
} else if (i == (len - 1)) {
length = (i - start + 1);
}
/* Lookup character index with default font. */
glyphidx = XftCharIndex(xw.dpy, font->match, rune);
if (glyphidx) {
specs[numspecs].font = font->match;
specs[numspecs].glyph = glyphidx;
specs[numspecs].x = (short)xp;
specs[numspecs].y = (short)yp;
/* Shape the segment. */
hbtransform(&shaped, font->match, glyphs, start, length);
runewidth = win.cw * ((glyphs[start].mode & ATTR_WIDE) ? 2.0f : 1.0f);
cluster_xp = xp; cluster_yp = yp;
for (int code_idx = 0; code_idx < shaped.count; code_idx++) {
int idx = shaped.glyphs[code_idx].cluster;
if (glyphs[start + idx].mode & ATTR_WDUMMY)
continue;
/* Advance the drawing cursor if we've moved to a new cluster */
if (code_idx > 0 && idx != shaped.glyphs[code_idx - 1].cluster) {
xp += runewidth;
numspecs++;
continue;
cluster_xp = xp;
cluster_yp = yp;
runewidth = win.cw * ((glyphs[start + idx].mode & ATTR_WIDE) ? 2.0f : 1.0f);
}
/* Fallback on font cache, search the font cache for match. */
if (shaped.glyphs[code_idx].codepoint != 0) {
/* If symbol is found, put it into the specs. */
specs[numspecs].font = font->match;
specs[numspecs].glyph = shaped.glyphs[code_idx].codepoint;
specs[numspecs].x = cluster_xp + (short)(shaped.positions[code_idx].x_offset / 64.);
specs[numspecs].y = cluster_yp - (short)(shaped.positions[code_idx].y_offset / 64.);
cluster_xp += shaped.positions[code_idx].x_advance / 64.;
cluster_yp += shaped.positions[code_idx].y_advance / 64.;
numspecs++;
} else {
/* If it's not found, try to fetch it through the font cache. */
rune = glyphs[start + idx].u;
for (f = 0; f < frclen; f++) {
glyphidx = XftCharIndex(xw.dpy, frc[f].font, rune);
/* Everything correct. */
@ -1364,9 +1401,22 @@ xmakeglyphfontspecs(XftGlyphFontSpec *specs, const Glyph *glyphs, int len, int x
specs[numspecs].glyph = glyphidx;
specs[numspecs].x = (short)xp;
specs[numspecs].y = (short)yp;
xp += runewidth;
numspecs++;
}
}
/* Cleanup and get ready for next segment. */
hbcleanup(&shaped);
start = i;
/* Determine font for glyph if different from previous glyph. */
if (prevmode != mode) {
prevmode = mode;
xresetfontsettings(mode, &font, &frcflags);
yp = winy + font->ascent;
}
}
}
return numspecs;
}
@ -1517,14 +1567,17 @@ xdrawglyph(Glyph g, int x, int y)
}
void
xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og)
xdrawcursor(int cx, int cy, Glyph g, int ox, int oy, Glyph og, Line line, int len)
{
Color drawcol;
/* remove the old cursor */
if (selected(ox, oy))
og.mode ^= ATTR_REVERSE;
xdrawglyph(og, ox, oy);
/* Redraw the line where cursor was previously.
* It will restore the ligatures broken by the cursor. */
xdrawline(line, 0, oy, len);
if (IS_SET(MODE_HIDE))
return;
@ -1652,18 +1705,16 @@ xdrawline(Line line, int x1, int y1, int x2)
Glyph base, new;
XftGlyphFontSpec *specs = xw.specbuf;
numspecs = xmakeglyphfontspecs(specs, &line[x1], x2 - x1, x1, y1);
i = ox = 0;
for (x = x1; x < x2 && i < numspecs; x++) {
for (x = x1; x < x2; x++) {
new = line[x];
if (new.mode == ATTR_WDUMMY)
continue;
if (selected(x, y1))
new.mode ^= ATTR_REVERSE;
if (i > 0 && ATTRCMP(base, new)) {
xdrawglyphfontspecs(specs, base, i, ox, y1);
specs += i;
numspecs -= i;
if ((i > 0) && ATTRCMP(base, new)) {
numspecs = xmakeglyphfontspecs(specs, &line[ox], x - ox, ox, y1);
xdrawglyphfontspecs(specs, base, numspecs, ox, y1);
i = 0;
}
if (i == 0) {
@ -1672,8 +1723,10 @@ xdrawline(Line line, int x1, int y1, int x2)
}
i++;
}
if (i > 0)
xdrawglyphfontspecs(specs, base, i, ox, y1);
if (i > 0) {
numspecs = xmakeglyphfontspecs(specs, &line[ox], x2 - ox, ox, y1);
xdrawglyphfontspecs(specs, base, numspecs, ox, y1);
}
}
void
@ -1833,7 +1886,7 @@ void
kpress(XEvent *ev)
{
XKeyEvent *e = &ev->xkey;
KeySym ksym;
KeySym ksym = NoSymbol;
char buf[64], *customkey;
int len;
Rune c;
@ -1843,10 +1896,13 @@ kpress(XEvent *ev)
if (IS_SET(MODE_KBDLOCK))
return;
if (xw.ime.xic)
if (xw.ime.xic) {
len = XmbLookupString(xw.ime.xic, e, buf, sizeof buf, &ksym, &status);
else
if (status == XBufferOverflow)
return;
} else {
len = XLookupString(e, buf, sizeof buf, &ksym, NULL);
}
/* 1. shortcuts */
for (bp = shortcuts; bp < shortcuts + LEN(shortcuts); bp++) {
if (ksym == bp->keysym && match(bp->mod, e->state)) {