commit f3f4efd63fa35cabf5ffb819b32e62656f0fec6b Author: zcake Date: Tue Dec 22 13:51:39 2020 +0800 uploading ... diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..0ccc34f --- /dev/null +++ b/LICENSE @@ -0,0 +1,23 @@ +MIT/X Consortium License + +© 2011 Christoph Lohmann <20h@r-36.net> +© 2008-2011 Enno Boland +© 2020 Maarten van Gompel + +Permission is hereby granted, free of charge, to any person obtaining a +copy of this software and associated documentation files (the "Software"), +to deal in the Software without restriction, including without limitation +the rights to use, copy, modify, merge, publish, distribute, sublicense, +and/or sell copies of the Software, and to permit persons to whom the +Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL +THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +DEALINGS IN THE SOFTWARE. diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3a0dcd2 --- /dev/null +++ b/Makefile @@ -0,0 +1,63 @@ +# svkbd - simple virtual keyboard +# See LICENSE file for copyright and license details. +.POSIX: + +NAME = svkbd +VERSION = 0.2.2 + +include config.mk + +BIN = ${NAME} +SRC = drw.c ${NAME}.c util.c +OBJ = ${SRC:.c=.o} +MAN1 = ${NAME}.1 + +all: ${BIN} + +options: + @echo svkbd build options: + @echo "CFLAGS = ${SVKBD_CFLAGS}" + @echo "CPPLAGS = ${SVKBD_CPPFLAGS}" + @echo "LDFLAGS = ${SVKBD_LDFLAGS}" + @echo "CC = ${CC}" + +config.h: + cp config.def.h $@ + +.c.o: + ${CC} ${SVKBD_CFLAGS} ${SVKBD_CPPFLAGS} -c $< + +${OBJ}: config.h config.mk + +${BIN}: ${OBJ} + ${CC} -o ${BIN} ${OBJ} ${SVKBD_LDFLAGS} + +clean: + rm -f ${NAME}-?? ${NAME}-??.o ${OBJ} ${BIN} + +dist: + rm -rf "${NAME}-${VERSION}" + mkdir -p "${NAME}-${VERSION}" + cp LICENSE Makefile README.md config.def.h config.mk ${MAN1} \ + drw.h util.h ${SRC} ${NAME}-${VERSION} + for i in layout.*.h; \ + do \ + cp $$i ${NAME}-${VERSION}; \ + done + tar -cf - "${NAME}-${VERSION}" | \ + gzip -c > "${NAME}-${VERSION}.tar.gz" + rm -rf "${NAME}-${VERSION}" + +install: all + mkdir -p ${DESTDIR}${PREFIX}/bin + cp ${NAME} ${DESTDIR}${PREFIX}/bin + chmod 755 ${DESTDIR}${PREFIX}/bin/${NAME} + mkdir -p "${DESTDIR}${MANPREFIX}/man1" + sed "s/VERSION/${VERSION}/g" < ${MAN1} > ${DESTDIR}${MANPREFIX}/man1/${MAN1} + chmod 644 ${DESTDIR}${MANPREFIX}/man1/${MAN1} + +uninstall: + rm -f ${DESTDIR}${PREFIX}/bin/${NAME} + rm -f ${DESTDIR}${MANPREFIX}/man1/${MAN1} + +.PHONY: all clean dist options install uninstall diff --git a/README.md b/README.md new file mode 100644 index 0000000..cd54e65 --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +SVKBD: Simple Virtual Keyboard +================================= + +This is a simple virtual keyboard, intended to be used in environments, +where no keyboard is available. + +Installation +------------ + + $ make + $ make install + +This will create by default `svkbd-intl`, which is svkbd using an international +layout with multiple layers and overlays, and optimised for mobile devices. + +You can create svkbd for additional layouts by doing: + + $ make LAYOUT=$layout + +This will take the file `layout.$layout.h` and create `svkbd-$layout`. +`make install` will then pick up the new file and install it accordingly. + +Layouts +--------- + +The following layouts are available: + +* **Mobile Layouts:** + * ``mobile-intl`` - A small international layout optimised for mobile devices. This layout consists of multiple layers which + can be switched on the fly, and overlays that appear on long-press of certain keys, adding input ability for + diacritics and other variants, as well as some emoji. The layers are: + * a basic qwerty layer + * a layer for numeric input, arrows, and punctuation + * a layer for function keys, media keys, and arrows + * a cyrillic layer (ЙЦУКЕН based); the э key is moved to an overlay on е + * a dialer/numeric layer + * ``mobile-plain`` - This is a plain layout with only a qwerty layer and numeric/punctuation layer. It was + originally made for [sxmo](https://sr.ht/~mil/Sxmo/). +* **Traditional layouts**: + * ``en`` - An english layout without layers (QWERTY) + * ``de`` - A german layout (QWERTZ) + * ``ru`` - A russian layout (ЙЦУКЕН) + * ``sh`` - A serbo-croatian layout using latin script (QWERTZ) + +Usage +----- + + $ svkbd-mobile-intl + +This will open svkbd at the bottom of the screen, showing the default +international layout. + + $ svkbd-mobile-intl -d + +This tells svkbd to announce itself being a dock window, which then +is managed differently between different window managers. If using dwm +and the dock patch, then this will make svkbd being managed by dwm and +some space of the screen being reserved for it. + + $ svkbd-mobile-intl -g 400x200+1+1 + +This will start svkbd-intl with a size of 400x200 and at the upper left +window corner. + +For layouts that consist of multiple layers, you can enable layers on program start through either the ``-l`` flag or +through the ``SVKBD_LAYERS`` environment variable. They both take a comma separated list of layer names (as defined in +your ``layout.*.h``). Use the ``↺`` button in the bottom-left to cycle through all the layers. + +Some layouts come with overlays that will show when certain keys are hold pressed for a longer time. For +example, a long press on the ``a`` key will enable an overview showing all kinds of diacritic combinations for ``a``. + +Overlay functionality interferes with the ability to hold a key and have it outputted repeatedly. You can disable +overlay functionality with the ``-O`` flag or by setting the environment variable ``SVKBD_ENABLEOVERLAYS=0``. There is +also a key on the function layer of the keyboard itself to enable/disable this behaviour on the fly. Its label shows +``≅`` when the overlay functionality is enabled and ``≇`` when not. + +Notes +--------- + +This virtual keyboard does not actually modify the X keyboard layout, the ``mobile-intl``, ``mobile-plain`` and ``en`` layouts simply rely on a standard US QWERTY layout (setxkbmap us) being activated, the other layouts (``de``, ``ru``, ``sh``) require their respective XKB keymaps to be active. + +If you use another XKB layout you will get unpredictable output that does not match the labels on the virtual keycaps! + +Repository +---------- + + git clone https://git.suckless.org/svkbd diff --git a/config.def.h b/config.def.h new file mode 100644 index 0000000..df37ff9 --- /dev/null +++ b/config.def.h @@ -0,0 +1,13 @@ +static const Bool wmborder = True; +static int fontsize = 20; +static double overlay_delay = 1.0; +static int heightfactor = 16; //one row of keys takes up 1/x of the screen height +static const char *fonts[] = { + "DejaVu Sans:bold:size=20" +}; +static const char *colors[SchemeLast][2] = { + /* fg bg */ + [SchemeNorm] = { "#ffffff", "#14313d" }, + [SchemePress] = { "#ffffff", "#000000" }, + [SchemeHighlight] = { "#58a7c6", "#005577" }, +}; diff --git a/config.h b/config.h new file mode 100644 index 0000000..11bb5a4 --- /dev/null +++ b/config.h @@ -0,0 +1,17 @@ +static const Bool wmborder = True; +static const char *fonts[] = { +"FiraCode Nerd Font:style=Light:size=10:antialias=true:autohint=true", +"Chiron Sans HK Light:style=Light:size=10", +"OpenMoji:style=Color", +}; + +static int fontsize = 10; +static double overlay_delay = 1.0; +static int heightfactor = 30; //one row of keys takes up 1/x of the screen height +static const char *colors[SchemeLast][2] = { + /* fg bg */ + [SchemeNorm] = { "#ff5555", "#282a36" }, + [SchemePress] = { "#50fa7b","#282a36" }, + [SchemeHighlight] = { "#ff79c6", "#282a36" }, + +}; diff --git a/config.mk b/config.mk new file mode 100644 index 0000000..d50217d --- /dev/null +++ b/config.mk @@ -0,0 +1,25 @@ +# paths +PREFIX = /usr/local +MANPREFIX = ${PREFIX}/share/man + +X11INC = /usr/X11R6/include +X11LIB = /usr/X11R6/lib + +PKG_CONFIG = pkg-config + +# Xinerama, comment if you don't want it +XINERAMALIBS = -L${X11LIB} -lXinerama +XINERAMAFLAGS = -DXINERAMA + +# includes and libs +INCS = -I. -I./layouts -I${X11INC} \ + `$(PKG_CONFIG) --cflags fontconfig` \ + `$(PKG_CONFIG) --cflags freetype2` +LIBS = -L${X11LIB} -lX11 -lXtst -lXft ${XINERAMALIBS} \ + `$(PKG_CONFIG) --libs fontconfig` \ + `$(PKG_CONFIG) --libs freetype2` + +# use system flags +SVKBD_CFLAGS = ${CFLAGS} ${INCS} +SVKBD_LDFLAGS = ${LDFLAGS} ${LIBS} +SVKBD_CPPFLAGS = ${CPPFLAGS} -D_DEFAULT_SOURCE -DVERSION=\"VERSION\" ${XINERAMAFLAGS} -DLAYOUT=\"layout.h\" diff --git a/drw.c b/drw.c new file mode 100644 index 0000000..868465c --- /dev/null +++ b/drw.c @@ -0,0 +1,418 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include +#include + +#include "drw.h" +#include "util.h" + +#define UTF_INVALID 0xFFFD +#define UTF_SIZ 4 + +static const unsigned char utfbyte[UTF_SIZ + 1] = {0x80, 0, 0xC0, 0xE0, 0xF0}; +static const unsigned char utfmask[UTF_SIZ + 1] = {0xC0, 0x80, 0xE0, 0xF0, 0xF8}; +static const long utfmin[UTF_SIZ + 1] = { 0, 0, 0x80, 0x800, 0x10000}; +static const long utfmax[UTF_SIZ + 1] = {0x10FFFF, 0x7F, 0x7FF, 0xFFFF, 0x10FFFF}; + +static long +utf8decodebyte(const char c, size_t *i) +{ + for (*i = 0; *i < (UTF_SIZ + 1); ++(*i)) + if (((unsigned char)c & utfmask[*i]) == utfbyte[*i]) + return (unsigned char)c & ~utfmask[*i]; + return 0; +} + +static size_t +utf8validate(long *u, size_t i) +{ + if (!BETWEEN(*u, utfmin[i], utfmax[i]) || BETWEEN(*u, 0xD800, 0xDFFF)) + *u = UTF_INVALID; + for (i = 1; *u > utfmax[i]; ++i) + ; + return i; +} + +static size_t +utf8decode(const char *c, long *u, size_t clen) +{ + size_t i, j, len, type; + long udecoded; + + *u = UTF_INVALID; + if (!clen) + return 0; + udecoded = utf8decodebyte(c[0], &len); + if (!BETWEEN(len, 1, UTF_SIZ)) + return 1; + for (i = 1, j = 1; i < clen && j < len; ++i, ++j) { + udecoded = (udecoded << 6) | utf8decodebyte(c[i], &type); + if (type) + return j; + } + if (j < len) + return 0; + *u = udecoded; + utf8validate(u, len); + + return len; +} + +Drw * +drw_create(Display *dpy, int screen, Window root, unsigned int w, unsigned int h) +{ + Drw *drw = ecalloc(1, sizeof(Drw)); + + drw->dpy = dpy; + drw->screen = screen; + drw->root = root; + drw->w = w; + drw->h = h; + drw->drawable = XCreatePixmap(dpy, root, w, h, DefaultDepth(dpy, screen)); + drw->gc = XCreateGC(dpy, root, 0, NULL); + XSetLineAttributes(dpy, drw->gc, 1, LineSolid, CapButt, JoinMiter); + + return drw; +} + +void +drw_resize(Drw *drw, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + drw->w = w; + drw->h = h; + if (drw->drawable) + XFreePixmap(drw->dpy, drw->drawable); + drw->drawable = XCreatePixmap(drw->dpy, drw->root, w, h, DefaultDepth(drw->dpy, drw->screen)); +} + +void +drw_free(Drw *drw) +{ + XFreePixmap(drw->dpy, drw->drawable); + XFreeGC(drw->dpy, drw->gc); + drw_fontset_free(drw->fonts); + free(drw); +} + +/* This function is an implementation detail. Library users should use + * drw_fontset_create instead. + */ +static Fnt * +xfont_create(Drw *drw, const char *fontname, FcPattern *fontpattern) +{ + Fnt *font; + XftFont *xfont = NULL; + FcPattern *pattern = NULL; + + if (fontname) { + /* Using the pattern found at font->xfont->pattern does not yield the + * same substitution results as using the pattern returned by + * FcNameParse; using the latter results in the desired fallback + * behaviour whereas the former just results in missing-character + * rectangles being drawn, at least with some fonts. */ + if (!(xfont = XftFontOpenName(drw->dpy, drw->screen, fontname))) { + fprintf(stderr, "error, cannot load font from name: '%s'\n", fontname); + return NULL; + } + if (!(pattern = FcNameParse((FcChar8 *) fontname))) { + fprintf(stderr, "error, cannot parse font name to pattern: '%s'\n", fontname); + XftFontClose(drw->dpy, xfont); + return NULL; + } + } else if (fontpattern) { + if (!(xfont = XftFontOpenPattern(drw->dpy, fontpattern))) { + fprintf(stderr, "error, cannot load font from pattern.\n"); + return NULL; + } + } else { + die("no font specified."); + } + + /* Do not allow using color fonts. This is a workaround for a BadLength + * error from Xft with color glyphs. Modelled on the Xterm workaround. See + * https://bugzilla.redhat.com/show_bug.cgi?id=1498269 + * https://lists.suckless.org/dev/1701/30932.html + * https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=916349 + * and lots more all over the internet. + */ + FcBool iscol; + if(FcPatternGetBool(xfont->pattern, FC_COLOR, 0, &iscol) == FcResultMatch && iscol) { + XftFontClose(drw->dpy, xfont); + return NULL; + } + + font = ecalloc(1, sizeof(Fnt)); + font->xfont = xfont; + font->pattern = pattern; + font->h = xfont->ascent + xfont->descent; + font->dpy = drw->dpy; + + return font; +} + +static void +xfont_free(Fnt *font) +{ + if (!font) + return; + if (font->pattern) + FcPatternDestroy(font->pattern); + XftFontClose(font->dpy, font->xfont); + free(font); +} + +Fnt* +drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount) +{ + Fnt *cur, *ret = NULL; + size_t i; + + if (!drw || !fonts) + return NULL; + + for (i = 1; i <= fontcount; i++) { + if ((cur = xfont_create(drw, fonts[fontcount - i], NULL))) { + cur->next = ret; + ret = cur; + } + } + return (drw->fonts = ret); +} + +void +drw_fontset_free(Fnt *font) +{ + if (font) { + drw_fontset_free(font->next); + xfont_free(font); + } +} + +void +drw_clr_create(Drw *drw, Clr *dest, const char *clrname) +{ + if (!drw || !dest || !clrname) + return; + + if (!XftColorAllocName(drw->dpy, DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen), + clrname, dest)) + die("error, cannot allocate color '%s'", clrname); +} + +/* Wrapper to create color schemes. The caller has to call free(3) on the + * returned color scheme when done using it. */ +Clr * +drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount) +{ + size_t i; + Clr *ret; + + /* need at least two colors for a scheme */ + if (!drw || !clrnames || clrcount < 2 || !(ret = ecalloc(clrcount, sizeof(XftColor)))) + die("error, cannot create color scheme (drw=%d) (clrcount=%d)", drw, clrcount); + + for (i = 0; i < clrcount; i++) + drw_clr_create(drw, &ret[i], clrnames[i]); + return ret; +} + +void +drw_setfontset(Drw *drw, Fnt *set) +{ + if (drw) + drw->fonts = set; +} + +void +drw_setscheme(Drw *drw, Clr *scm) +{ + if (drw) + drw->scheme = scm; +} + +void +drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert) +{ + if (!drw || !drw->scheme) + return; + XSetForeground(drw->dpy, drw->gc, invert ? drw->scheme[ColBg].pixel : drw->scheme[ColFg].pixel); + if (filled) + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + else + XDrawRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w - 1, h - 1); +} + +int +drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert) +{ + char buf[1024]; + int ty; + unsigned int ew; + XftDraw *d = NULL; + Fnt *usedfont, *curfont, *nextfont; + size_t i, len; + int utf8strlen, utf8charlen, render = x || y || w || h; + long utf8codepoint = 0; + const char *utf8str; + FcCharSet *fccharset; + FcPattern *fcpattern; + FcPattern *match; + XftResult result; + int charexists = 0; + + if (!drw || (render && !drw->scheme) || !text || !drw->fonts) + return 0; + + if (!render) { + w = ~w; + } else { + XSetForeground(drw->dpy, drw->gc, drw->scheme[invert ? ColFg : ColBg].pixel); + XFillRectangle(drw->dpy, drw->drawable, drw->gc, x, y, w, h); + d = XftDrawCreate(drw->dpy, drw->drawable, + DefaultVisual(drw->dpy, drw->screen), + DefaultColormap(drw->dpy, drw->screen)); + x += lpad; + w -= lpad; + } + + usedfont = drw->fonts; + while (1) { + utf8strlen = 0; + utf8str = text; + nextfont = NULL; + while (*text) { + utf8charlen = utf8decode(text, &utf8codepoint, UTF_SIZ); + for (curfont = drw->fonts; curfont; curfont = curfont->next) { + charexists = charexists || XftCharExists(drw->dpy, curfont->xfont, utf8codepoint); + if (charexists) { + if (curfont == usedfont) { + utf8strlen += utf8charlen; + text += utf8charlen; + } else { + nextfont = curfont; + } + break; + } + } + + if (!charexists || nextfont) + break; + else + charexists = 0; + } + + if (utf8strlen) { + drw_font_getexts(usedfont, utf8str, utf8strlen, &ew, NULL); + /* shorten text if necessary */ + for (len = MIN(utf8strlen, sizeof(buf) - 1); len && ew > w; len--) + drw_font_getexts(usedfont, utf8str, len, &ew, NULL); + + if (len) { + memcpy(buf, utf8str, len); + buf[len] = '\0'; + if (len < utf8strlen) + for (i = len; i && i > len - 3; buf[--i] = '.') + ; /* NOP */ + + if (render) { + ty = y + (h - usedfont->h) / 2 + usedfont->xfont->ascent; + XftDrawStringUtf8(d, &drw->scheme[invert ? ColBg : ColFg], + usedfont->xfont, x, ty, (XftChar8 *)buf, len); + } + x += ew; + w -= ew; + } + } + + if (!*text) { + break; + } else if (nextfont) { + charexists = 0; + usedfont = nextfont; + } else { + /* Regardless of whether or not a fallback font is found, the + * character must be drawn. */ + charexists = 1; + + fccharset = FcCharSetCreate(); + FcCharSetAddChar(fccharset, utf8codepoint); + + if (!drw->fonts->pattern) { + /* Refer to the comment in xfont_create for more information. */ + die("the first font in the cache must be loaded from a font string."); + } + + fcpattern = FcPatternDuplicate(drw->fonts->pattern); + FcPatternAddCharSet(fcpattern, FC_CHARSET, fccharset); + FcPatternAddBool(fcpattern, FC_SCALABLE, FcTrue); + FcPatternAddBool(fcpattern, FC_COLOR, FcFalse); + + FcConfigSubstitute(NULL, fcpattern, FcMatchPattern); + FcDefaultSubstitute(fcpattern); + match = XftFontMatch(drw->dpy, drw->screen, fcpattern, &result); + + FcCharSetDestroy(fccharset); + FcPatternDestroy(fcpattern); + + if (match) { + usedfont = xfont_create(drw, NULL, match); + if (usedfont && XftCharExists(drw->dpy, usedfont->xfont, utf8codepoint)) { + for (curfont = drw->fonts; curfont->next; curfont = curfont->next) + ; /* NOP */ + curfont->next = usedfont; + } else { + xfont_free(usedfont); + usedfont = drw->fonts; + } + } + } + } + if (d) + XftDrawDestroy(d); + + return x + (render ? w : 0); +} + +void +drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h) +{ + if (!drw) + return; + + XCopyArea(drw->dpy, drw->drawable, win, drw->gc, x, y, w, h, x, y); +} + +void +drw_sync(Drw *drw) +{ + XSync(drw->dpy, False); +} + +unsigned int +drw_fontset_getwidth(Drw *drw, const char *text) +{ + if (!drw || !drw->fonts || !text) + return 0; + return drw_text(drw, 0, 0, 0, 0, 0, text, 0); +} + +void +drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h) +{ + XGlyphInfo ext; + + if (!font || !text) + return; + + XftTextExtentsUtf8(font->dpy, font->xfont, (XftChar8 *)text, len, &ext); + if (w) + *w = ext.xOff; + if (h) + *h = font->h; +} diff --git a/drw.h b/drw.h new file mode 100644 index 0000000..f397611 --- /dev/null +++ b/drw.h @@ -0,0 +1,50 @@ +/* See LICENSE file for copyright and license details. */ + +typedef struct Fnt { + Display *dpy; + unsigned int h; + XftFont *xfont; + FcPattern *pattern; + struct Fnt *next; +} Fnt; + +enum { ColFg, ColBg }; /* Clr scheme index */ +typedef XftColor Clr; + +typedef struct { + unsigned int w, h; + Display *dpy; + int screen; + Window root; + Drawable drawable; + GC gc; + Clr *scheme; + Fnt *fonts; +} Drw; + +/* Drawable abstraction */ +Drw *drw_create(Display *dpy, int screen, Window win, unsigned int w, unsigned int h); +void drw_resize(Drw *drw, unsigned int w, unsigned int h); +void drw_free(Drw *drw); + +/* Fnt abstraction */ +Fnt *drw_fontset_create(Drw* drw, const char *fonts[], size_t fontcount); +void drw_fontset_free(Fnt* set); +unsigned int drw_fontset_getwidth(Drw *drw, const char *text); +void drw_font_getexts(Fnt *font, const char *text, unsigned int len, unsigned int *w, unsigned int *h); + +/* Colorscheme abstraction */ +void drw_clr_create(Drw *drw, Clr *dest, const char *clrname); +Clr *drw_scm_create(Drw *drw, const char *clrnames[], size_t clrcount); + +/* Drawing context manipulation */ +void drw_setfontset(Drw *drw, Fnt *set); +void drw_setscheme(Drw *drw, Clr *scm); + +/* Drawing functions */ +void drw_rect(Drw *drw, int x, int y, unsigned int w, unsigned int h, int filled, int invert); +int drw_text(Drw *drw, int x, int y, unsigned int w, unsigned int h, unsigned int lpad, const char *text, int invert); + +/* Map functions */ +void drw_map(Drw *drw, Window win, int x, int y, unsigned int w, unsigned int h); +void drw_sync(Drw *drw); diff --git a/drw.o b/drw.o new file mode 100644 index 0000000..87d440c Binary files /dev/null and b/drw.o differ diff --git a/layout.h b/layout.h new file mode 100644 index 0000000..0bc7831 --- /dev/null +++ b/layout.h @@ -0,0 +1,168 @@ +//    +#define KEYS 71 +static Key keys_en[KEYS] = { + { "`~", XK_grave, 1 }, + { "1!", XK_1, 1 }, + { "2@", XK_2, 1 }, + { "3#", XK_3, 1 }, + { "4$", XK_4, 1 }, + { "5%", XK_5, 1 }, + { "6^", XK_6, 1 }, + { "7&", XK_7, 1 }, + { "8*", XK_8, 1 }, + { "9(", XK_9, 1 }, + { "0)", XK_0, 1 }, + { "-_", XK_minus, 1 }, + { "=+", XK_plus, 1 }, + { "⌫", XK_BackSpace, 2 }, + { 0 }, /* New row */ + { "->|", XK_Tab, 1 }, + { 0, XK_q, 1 }, + { 0, XK_w, 1 }, + { 0, XK_e, 1 }, + { 0, XK_r, 1 }, + { 0, XK_t, 1 }, + { 0, XK_y, 1 }, + { 0, XK_u, 1 }, + { 0, XK_i, 1 }, + { 0, XK_o, 1 }, + { 0, XK_p, 1 }, + { "[", XK_bracketleft, 1 }, + { "]", XK_bracketright, 1 }, + { "↵", XK_Return, 1 }, + { 0 }, /* New row */ + { "Esc", XK_Escape, 1 }, + { 0, XK_a, 1 }, + { 0, XK_s, 1 }, + { 0, XK_d, 1 }, + { 0, XK_f, 1 }, + { 0, XK_g, 1 }, + { 0, XK_h, 1 }, + { 0, XK_j, 1 }, + { 0, XK_k, 1 }, + { 0, XK_l, 1 }, + { ":;", XK_semicolon, 1 }, + { "'\"", XK_exclam, 1 }, + { "\\|", XK_backslash, 1 }, + { 0 }, /* New row */ + { "Shift", XK_Shift_L, 3 }, + { 0, XK_z, 1 }, + { 0, XK_x, 1 }, + { 0, XK_c, 1 }, + { 0, XK_v, 1 }, + { 0, XK_b, 1 }, + { 0, XK_n, 1 }, + { 0, XK_m, 1 }, + { "↑", XK_Up, 1 }, + { ",", XK_colon, 1 }, + { ".", XK_period, 1 }, + { "/?", XK_slash, 1 }, + { "Shift", XK_Shift_R, 2 }, + { 0 }, /* New row */ + { "", XK_Cancel, 1}, + { "Ctrl", XK_Control_L, 2 }, + { "Alt", XK_Alt_L, 2 }, + { "", XK_Super_L, 2 }, + { "Hyper", XK_Hyper_L, 2 }, + { "", XK_space, 5 }, + { "←", XK_Left, 1 }, + { "↓", XK_Down, 1 }, + { "→", XK_Right, 1}, + { "Alt", XK_Alt_R, 2 }, + { "", XK_Super_R, 2 }, + { "Hyper", XK_Hyper_R, 2 }, + { "Ctrl", XK_Control_R, 2 }, +}; +static Key keys_emoji [KEYS] = { + { "🙂", 0x101f642 ,1 }, //1 + { "😀", 0x101f600 ,1 },//2 + { "😁", 0x101f601 ,1 },//3 + { "😂", 0x101f602 ,1 },//4 + { "😃", 0x101f603 ,1 },//5 + { "😄", 0x101f604 ,1 },//6 + { "😅", 0x101f605 ,1 },//7 + { "😆", 0x101f606 ,1 },//8 + { "😇", 0x101f607 ,1 },//9 + { "😈", 0x101f608 ,1 },//0 + { "😉", 0x101f609 ,1 },//11 + { "😊", 0x101f60a ,1 },//12 + { "😋", 0x101f60b ,1 },//13 + { "😌", 0x101f60c ,1 },//14 + { "😍", 0x101f60d ,1 },//15 + { "😎", 0x101f60e ,1 },//16 + { "😏", 0x101f60f ,1 },//17 + { "😐", 0x101f610 ,1 },//18 + { "😒", 0x101f612 ,1 },//19 + { "😓", 0x101f613 ,1 },//20 + { "😛", 0x101f61b ,1 },//21 + { "😮", 0x101f62e ,1 },//22 + { "😟", 0x101f61f ,1 },//23 + { "😟", 0x101f620 ,1 },//24 + { "😢", 0x101f622 ,1 },//25 + { "😭", 0x101f62d ,1 },//26 + { "😳", 0x101f633 ,1 },//27 + { "😴", 0x101f634 ,1 },//28 + { "Del", XK_Delete, 1 },//29 + {0}, + { "▶", XF86XK_AudioPlay, 1 },//30 + { "●", XF86XK_AudioRecord, 1 },//31 + { "■", XF86XK_AudioStop, 1 },//32 + { "◂◂", XF86XK_AudioPrev, 1 },//33 + { "▸▸", XF86XK_AudioNext, 1 },//34 + { "♫M", XF86XK_AudioMute, 1 },//35 + { "♫-", XF86XK_AudioLowerVolume, 1 },//36 + { "♫+", XF86XK_AudioRaiseVolume, 1 },//37 + { "☀-", XF86XK_MonBrightnessDown, 1 },//38 + { "☀+", XF86XK_MonBrightnessUp, 1 },//39 +{ "F1", XK_F1, 1 },//40 + { "F2", XK_F2, 1 },//41 + { "F3", XK_F3, 1 },//42 + { "F4", XK_F4, 1 },//43 + { "F5", XK_F5, 1 },//44 + { "F6", XK_F6, 1 },//45 + { "F7", XK_F7, 1 },//46 + { "F8", XK_F8, 1 },//47 + { "F9", XK_F9, 1 },//48 + { "F10", XK_F10, 1 },//49 + { "F11", XK_F11, 1 },//50 + { "F12", XK_F12, 1 },//51 + { "", XK_Cancel, 1},//52 + { "", XK_space, 5 },//53 + {0}, + { "", XK_space, 10 },//54 + {0}, + { "", XK_space, 10 },//55 + {0}, + { "", XK_space, 10 },//56 + {0}, + { "", XK_space, 10 },//57 + {0}, + { "", XK_space, 10 },//58 + {0}, + { "", XK_space, 10 },//59 +{0}, +{0}, + + }; + +Buttonmod buttonmods[] = { + { XK_Shift_L, Button2 }, + { XK_Alt_L, Button3 }, +}; + +#define OVERLAYS 1 +static Key overlay[OVERLAYS] = { + { 0, XK_Cancel }, +}; + +#define LAYERS 2 +static char* layer_names[LAYERS] = { + "en", + "emoji", +}; + +static Key* available_layers[LAYERS] = { + keys_en, + keys_emoji, +}; + diff --git a/svkbd b/svkbd new file mode 100755 index 0000000..b397699 Binary files /dev/null and b/svkbd differ diff --git a/svkbd-arrows b/svkbd-arrows new file mode 100755 index 0000000..44e1531 Binary files /dev/null and b/svkbd-arrows differ diff --git a/svkbd.1 b/svkbd.1 new file mode 100644 index 0000000..45d1adf --- /dev/null +++ b/svkbd.1 @@ -0,0 +1,72 @@ +.Dd August 4, 2020 +.Dt SVKBD 1 +.Os +.Sh NAME +.Nm svkbd +.Nd simple virtual keyboard +.Sh SYNOPSIS +.Nm +.Op Fl D +.Op Fl d +.Op Fl g Ar geometry +.Op Fl fn Ar font +.Op Fl O +.Op Fl h +.Op Fl H Ar heightfactor +.Op Fl l Ar layers +.Op Fl s Ar layer +.Op Fl v +.Sh DESCRIPTION +.Nm +is a simple virtual keyboard for X.org, intended to be used in environments, where no +keyboard is available. +.Sh OPTIONS +.Bl -tag -width Ds +.It Fl D +Debug mode; output verbosely to stderr. +.It Fl d +Set the _NET_WM_WINDOW_TYPE_DOCK property to hint windowmanagers it is +dockable, by default off. +.It Fl fn Ar font +Defines the font or font set used. e.g. "Monospace:bold:size=20" (an Xft font). +.It Fl g Ar geometry +Adjust the initial window position or size as specified by the standard X11 +geometry format. +.It Fl h +Show the usage information. +.It Fl H Ar heightfactor +Affects the vertical space taken by the keyboard. +One row of keys takes up 1/heighfactor of the screen's total height. +.It Fl l Ar layers +Comma separated list of layers to enable (by name). If not set, all layers +in the layout will be available. +The layer names are defined by the layout you compiled. +.It Fl s Ar layer +The layer to show on program start-up (by name). If not set, the first +layer of the layout will be shown. +.It Fl O +Disable overlay functionality. +.It Fl v +Show the version information. +.El +.Sh ENVIRONMENT +The following environment variables can be defined, providing +an alternative to some of the command line parameters. +The command line parameters, however, always take precedence: +.Bl -tag -width Ds +.It Ev SVKBD_LAYERS +Comma separated list of layers to enable (by name). The layer names are defined by the layout +you compiled. +.It Ev SVKBD_ENABLEOVERLAYS +Set this to 0 if you want to disable overlay functionality. +.It Ev SVKBD_HEIGHTFACTOR +Affects the vertical space taken by the keyboard. +One row of keys takes up 1/heighfactor of the screen's total height. +.El +.Sh SEE ALSO +.Xr XParseGeometry 3 +.Sh AUTHORS +.An Christoph Lohmann Aq Mt 20h@r-36.net +.An Enno Boland Aq Mt gottox@s01.de +.An Miles Alan Aq Mt m@milesalan.com +.An Maarten van Gompel Aq Mt proycon@anaproy.nl diff --git a/svkbd.c b/svkbd.c new file mode 100644 index 0000000..9b1282a --- /dev/null +++ b/svkbd.c @@ -0,0 +1,999 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#ifdef XINERAMA +#include +#endif + +#include "drw.h" +#include "util.h" + +/* macros */ +#define LENGTH(x) (sizeof x / sizeof x[0]) +#define TEXTW(X) (drw_fontset_getwidth(drw, (X))) +#define STRINGTOKEYSYM(X) (XStringToKeySym(X)) + +/* enums */ +enum { SchemeNorm, SchemePress, SchemeHighlight, SchemeLast }; +enum { NetWMWindowType, NetLast }; + +/* typedefs */ +typedef struct { + char *label; + KeySym keysym; + unsigned int width; + int x, y, w, h; + Bool pressed; + Bool highlighted; +} Key; + +typedef struct { + KeySym mod; + unsigned int button; +} Buttonmod; + +/* function declarations */ +static void printdbg(const char *fmt, ...); +static void motionnotify(XEvent *e); +static void buttonpress(XEvent *e); +static void buttonrelease(XEvent *e); +static void cleanup(void); +static void configurenotify(XEvent *e); +static void countrows(); +static int countkeys(Key *layer); +static void drawkeyboard(void); +static void drawkey(Key *k); +static void expose(XEvent *e); +static Key *findkey(int x, int y); +static void leavenotify(XEvent *e); +static void press(Key *k, KeySym mod); +static double get_press_duration(); +static void run(void); +static void setup(void); +static void simulate_keypress(KeySym keysym); +static void simulate_keyrelease(KeySym keysym); +static void showoverlay(int idx); +static void hideoverlay(); +static void cyclelayer(); +static void setlayer(); +static void togglelayer(); +static void unpress(Key *k, KeySym mod); +static void updatekeys(); + +/* variables */ +static int screen; +static void (*handler[LASTEvent]) (XEvent *) = { + [ButtonPress] = buttonpress, + [ButtonRelease] = buttonrelease, + [ConfigureNotify] = configurenotify, + [Expose] = expose, + [LeaveNotify] = leavenotify, + [MotionNotify] = motionnotify +}; +static Atom netatom[NetLast]; +static Display *dpy; +static Drw *drw; +static Window root, win; +static Clr* scheme[SchemeLast]; +static Bool running = True, isdock = False; +static KeySym pressedmod = 0; +static struct timeval pressbegin; +static int currentlayer = 0; +static int enableoverlays = 1; +static int currentoverlay = -1; /* -1 = no overlay */ +static KeySym overlaykeysym = 0; /* keysym for which the overlay is presented */ +static int releaseprotect = 0; /* set to 1 after overlay is shown, protecting against immediate release */ +static int tmp_keycode = 1; +static int rows = 0, ww = 0, wh = 0, wx = 0, wy = 0; +static char *name = "svkbd"; +static int debug = 0; +static int numlayers = 0; +static int numkeys = 0; + +static KeySym ispressingkeysym; + +Bool ispressing = False; +Bool sigtermd = False; + +/* configuration, allows nested code to access above variables */ +#include "config.h" +#ifndef LAYOUT +#error "make sure to define LAYOUT" +#endif +#include LAYOUT + +static Key keys[KEYS] = { NULL }; +static Key* layers[LAYERS]; + +void +motionnotify(XEvent *e) +{ + XPointerMovedEvent *ev = &e->xmotion; + int i; + + for (i = 0; i < numkeys; i++) { + if (keys[i].keysym && ev->x > keys[i].x + && ev->x < keys[i].x + keys[i].w + && ev->y > keys[i].y + && ev->y < keys[i].y + keys[i].h) { + if (keys[i].highlighted != True) { + if (ispressing) { + keys[i].pressed = True; + } else { + keys[i].highlighted = True; + } + drawkey(&keys[i]); + } + continue; + } + + if (!IsModifierKey(keys[i].keysym) && keys[i].pressed == True) { + unpress(&keys[i], 0); + + drawkey(&keys[i]); + } + if (keys[i].highlighted == True) { + keys[i].highlighted = False; + drawkey(&keys[i]); + } + } +} + +void +buttonpress(XEvent *e) +{ + XButtonPressedEvent *ev = &e->xbutton; + Key *k; + KeySym mod = 0; + int i; + + ispressing = True; + + for (i = 0; i < LENGTH(buttonmods); i++) { + if (ev->button == buttonmods[i].button) { + mod = buttonmods[i].mod; + break; + } + } + if ((k = findkey(ev->x, ev->y))) + press(k, mod); +} + +void +buttonrelease(XEvent *e) +{ + XButtonPressedEvent *ev = &e->xbutton; + Key *k; + KeySym mod = 0; + int i; + + ispressing = False; + + for (i = 0; i < LENGTH(buttonmods); i++) { + if (ev->button == buttonmods[i].button) { + mod = buttonmods[i].mod; + break; + } + } + + if (ev->x < 0 || ev->y < 0) { + unpress(NULL, mod); + } else { + if ((k = findkey(ev->x, ev->y))) + unpress(k, mod); + } +} + +void +cleanup(void) +{ + int i; + + for (i = 0; i < SchemeLast; i++) + free(scheme[i]); + drw_sync(drw); + drw_free(drw); + XSync(dpy, False); + drw_free(drw); + XDestroyWindow(dpy, win); + XSync(dpy, False); + XSetInputFocus(dpy, PointerRoot, RevertToPointerRoot, CurrentTime); +} + +void +configurenotify(XEvent *e) +{ + XConfigureEvent *ev = &e->xconfigure; + + if (ev->window == win && (ev->width != ww || ev->height != wh)) { + ww = ev->width; + wh = ev->height; + drw_resize(drw, ww, wh); + updatekeys(); + } +} + +void +countrows(void) +{ + int i; + + for (i = 0, rows = 1; i < numkeys; i++) { + if (keys[i].keysym == 0) + rows++; + } +} + +int +countkeys(Key *layer) +{ + int i, nkeys = 0; + + for (i = 0; i < KEYS; i++) { + if (i > 0 && layer[i].keysym == 0 && layer[i - 1].keysym == 0) { + nkeys--; + break; + } + nkeys++; + } + + return nkeys; +} + +void +drawkeyboard(void) +{ + int i; + + for (i = 0; i < numkeys; i++) { + if (keys[i].keysym != 0) + drawkey(&keys[i]); + } +} + +void +drawkey(Key *k) +{ + int x, y, w, h; + const char *l; + + if (k->pressed) + drw_setscheme(drw, scheme[SchemePress]); + else if (k->highlighted) + drw_setscheme(drw, scheme[SchemeHighlight]); + else + drw_setscheme(drw, scheme[SchemeNorm]); + drw_rect(drw, k->x, k->y, k->w, k->h, 1, 1); + drw_rect(drw, k->x, k->y, k->w, k->h, 0, 0); + + if (k->keysym == XK_KP_Insert) { + if (enableoverlays) { + l = "≅"; + } else { + l = "≇"; + } + } else if (k->label) { + l = k->label; + } else { + l = XKeysymToString(k->keysym); + } + h = fontsize * 2; + y = k->y + (k->h / 2) - (h / 2); + w = TEXTW(l); + x = k->x + (k->w / 2) - (w / 2); + drw_text(drw, x, y, w, h, 0, l, 0); + drw_map(drw, win, k->x, k->y, k->w, k->h); +} + +void +expose(XEvent *e) +{ + XExposeEvent *ev = &e->xexpose; + + if (ev->count == 0 && (ev->window == win)) + drawkeyboard(); +} + +Key * +findkey(int x, int y) { + int i; + + for (i = 0; i < numkeys; i++) { + if (keys[i].keysym && x > keys[i].x && + x < keys[i].x + keys[i].w && + y > keys[i].y && y < keys[i].y + keys[i].h) { + return &keys[i]; + } + } + return NULL; +} + +int +hasoverlay(KeySym keysym) +{ + int begin, i; + + begin = 0; + for (i = 0; i < OVERLAYS; i++) { + if (overlay[i].keysym == XK_Cancel) { + begin = i+1; + } else if (overlay[i].keysym == keysym) { + return begin+1; + } + } + return -1; +} + +void +leavenotify(XEvent *e) +{ + if (currentoverlay != -1) + hideoverlay(); + unpress(NULL, 0); +} + +void +record_press_begin(KeySym ks) +{ + /* record the begin of the press, don't simulate the actual keypress yet */ + gettimeofday(&pressbegin, NULL); + ispressingkeysym = ks; +} + +void +press(Key *k, KeySym mod) +{ + int i; + int overlayidx = -1; + + k->pressed = !k->pressed; + + if (debug) printdbg("Begin press: %ld\n", k->keysym); + pressbegin.tv_sec = 0; + pressbegin.tv_usec = 0; + ispressingkeysym = 0; + + if (!IsModifierKey(k->keysym)) { + if (enableoverlays && currentoverlay == -1) + overlayidx = hasoverlay(k->keysym); + if (enableoverlays && overlayidx != -1) { + if (!pressbegin.tv_sec && !pressbegin.tv_usec) { + /*record the begin of the press, don't simulate the actual keypress yet */ + record_press_begin(k->keysym); + } + } else { + if (debug) printdbg("Simulating press: %ld\n", k->keysym); + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed && IsModifierKey(keys[i].keysym)) { + simulate_keypress(keys[i].keysym); + } + } + pressedmod = mod; + if (pressedmod) { + simulate_keypress(mod); + } + simulate_keypress(k->keysym); + + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed && IsModifierKey(keys[i].keysym)) { + simulate_keyrelease(keys[i].keysym); + } + } + } + } + drawkey(k); +} + +int +tmp_remap(KeySym keysym) +{ + XChangeKeyboardMapping(dpy, tmp_keycode, 1, &keysym, 1); + XSync(dpy, False); + + return tmp_keycode; +} + +void +simulate_keypress(KeySym keysym) +{ + KeyCode code = XKeysymToKeycode(dpy, keysym); + if (code == 0) + code = tmp_remap(keysym); + XTestFakeKeyEvent(dpy, code, True, 0); +} + +void +simulate_keyrelease(KeySym keysym) +{ + KeyCode code = XKeysymToKeycode(dpy, keysym); + if (code == 0) + code = tmp_remap(keysym); + XTestFakeKeyEvent(dpy, code, False, 0); +} + +double +get_press_duration(void) +{ + struct timeval now; + + gettimeofday(&now, NULL); + + return (double) ((now.tv_sec * 1000000L + now.tv_usec) - + (pressbegin.tv_sec * 1000000L + pressbegin.tv_usec)) / + (double) 1000000L; +} + +void +unpress(Key *k, KeySym mod) +{ + int i; + + if (k != NULL) { + switch(k->keysym) { + case XK_Cancel: + cyclelayer(); + break; + case XK_script_switch: + togglelayer(); + break; + case XK_KP_Insert: + enableoverlays = !enableoverlays; + break; + case XK_Break: + running = False; + break; + default: + break; + } + } + + if ((pressbegin.tv_sec || pressbegin.tv_usec) && enableoverlays && k && k->keysym == ispressingkeysym) { + if (currentoverlay == -1) { + if (get_press_duration() < overlay_delay) { + if (debug) printdbg("Delayed simulation of press after release: %ld\n", k->keysym); + /* simulate the press event, as we postponed it earlier in press() */ + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed && IsModifierKey(keys[i].keysym)) { + simulate_keypress(keys[i].keysym); + } + } + pressedmod = mod; + if (pressedmod) { + simulate_keypress(mod); + } + simulate_keypress(k->keysym); + pressbegin.tv_sec = 0; + pressbegin.tv_usec = 0; + } else { + return; + } + } + } + + if (debug) { + if (k) { + printdbg("Simulation of release: %ld\n", k->keysym); + } else { + printdbg("Simulation of release (all keys)\n"); + } + } + + + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed && !IsModifierKey(keys[i].keysym)) { + simulate_keyrelease(keys[i].keysym); + keys[i].pressed = 0; + drawkey(&keys[i]); + break; + } + } + if (i != numkeys) { + if (pressedmod) { + simulate_keyrelease(mod); + } + pressedmod = 0; + + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed) { + simulate_keyrelease(keys[i].keysym); + keys[i].pressed = 0; + drawkey(&keys[i]); + } + } + } + + if (enableoverlays && currentoverlay != -1) { + if (releaseprotect) { + releaseprotect = 0; + } else { + hideoverlay(); + } + } +} + +void +run(void) +{ + XEvent ev; + int xfd; + fd_set fds; + struct timeval tv; + double duration = 0.0; + int i, r; + + xfd = ConnectionNumber(dpy); + tv.tv_sec = 1; + tv.tv_usec = 0; + + XFlush(dpy); + + while (running) { + usleep(100000L); /* 100ms */ + FD_ZERO(&fds); + FD_SET(xfd, &fds); + r = select(xfd + 1, &fds, NULL, NULL, &tv); + if (r) { + while (XPending(dpy)) { + XNextEvent(dpy, &ev); + if (handler[ev.type]) { + (handler[ev.type])(&ev); /* call handler */ + } + } + } else { + /* time-out expired without anything interesting happening, check for long-presses */ + if (ispressing && ispressingkeysym) { + duration = get_press_duration(); + if (debug == 2) printdbg("%f\n", duration); + if (get_press_duration() >= overlay_delay) { + if (debug) printdbg("press duration %f\n", duration); + showoverlay(hasoverlay(ispressingkeysym)); + pressbegin.tv_sec = 0; + pressbegin.tv_usec = 0; + ispressingkeysym = 0; + } + } + } + if (r == -1 || sigtermd) { + /* an error occurred or we received a signal */ + /* E.g. Generally in scripts we want to call SIGTERM on svkbd in which case + if the user is holding for example the enter key (to execute + the kill or script that does the kill), that causes an issue + since then X doesn't know the keyup is never coming.. (since + process will be dead before finger lifts - in that case we + just trigger out fake up presses for all keys */ + if (debug) printdbg("signal received, releasing all keys"); + for (i = 0; i < numkeys; i++) { + XTestFakeKeyEvent(dpy, XKeysymToKeycode(dpy, keys[i].keysym), False, 0); + } + running = False; + } + } +} + +void +setup(void) +{ + XSetWindowAttributes wa; + XTextProperty str; + XSizeHints *sizeh = NULL; + XClassHint *ch; + XWMHints *wmh; + Atom atype = -1; + int i, j, sh, sw; + +#ifdef XINERAMA + XineramaScreenInfo *info = NULL; +#endif + + /* init screen */ + screen = DefaultScreen(dpy); + root = RootWindow(dpy, screen); +#ifdef XINERAMA + if (XineramaIsActive(dpy)) { + info = XineramaQueryScreens(dpy, &i); + sw = info[0].width; + sh = info[0].height; + XFree(info); + } else +#endif + { + sw = DisplayWidth(dpy, screen); + sh = DisplayHeight(dpy, screen); + } + drw = drw_create(dpy, screen, root, sw, sh); + if (!drw_fontset_create(drw, fonts, LENGTH(fonts))) + die("no fonts could be loaded"); + drw_setscheme(drw, scheme[SchemeNorm]); + + /* find an unused keycode to use as a temporary keycode (derived from source: + https://stackoverflow.com/questions/44313966/c-xtest-emitting-key-presses-for-every-unicode-character) */ + KeySym *keysyms; + int keysyms_per_keycode = 0; + int keycode_low, keycode_high; + Bool key_is_empty; + int symindex; + + XDisplayKeycodes(dpy, &keycode_low, &keycode_high); + keysyms = XGetKeyboardMapping(dpy, keycode_low, keycode_high - keycode_low, &keysyms_per_keycode); + for (i = keycode_low; i <= keycode_high; i++) { + key_is_empty = True; + for (j = 0; j < keysyms_per_keycode; j++) { + symindex = (i - keycode_low) * keysyms_per_keycode + j; + if (keysyms[symindex] != 0) { + key_is_empty = False; + } else { + break; + } + } + if (key_is_empty) { + tmp_keycode = i; + break; + } + } + + /* init appearance */ + for (j = 0; j < SchemeLast; j++) + scheme[j] = drw_scm_create(drw, colors[j], 2); + + /* init atoms */ + if (isdock) { + netatom[NetWMWindowType] = XInternAtom(dpy, + "_NET_WM_WINDOW_TYPE", False); + atype = XInternAtom(dpy, "_NET_WM_WINDOW_TYPE_DOCK", False); + } + + /* init appearance */ + countrows(); + if (!ww) + ww = sw; + if (!wh) + wh = sh * rows / heightfactor; + + if (!wx) + wx = 0; + if (wx < 0) + wx = sw + wx - ww; + if (!wy) + wy = sh - wh; + if (wy < 0) + wy = sh + wy - wh; + + for (i = 0; i < numkeys; i++) + keys[i].pressed = 0; + + wa.override_redirect = !wmborder; + wa.border_pixel = scheme[SchemeNorm][ColFg].pixel; + wa.background_pixel = scheme[SchemeNorm][ColBg].pixel; + win = XCreateWindow(dpy, root, wx, wy, ww, wh, 0, + CopyFromParent, CopyFromParent, CopyFromParent, + CWOverrideRedirect | CWBorderPixel | + CWBackingPixel, &wa); + XSelectInput(dpy, win, StructureNotifyMask|ButtonReleaseMask| + ButtonPressMask|ExposureMask|LeaveWindowMask| + PointerMotionMask); + + wmh = XAllocWMHints(); + wmh->input = False; + wmh->flags = InputHint; + if (!isdock) { + sizeh = XAllocSizeHints(); + sizeh->flags = PMaxSize | PMinSize; + sizeh->min_width = sizeh->max_width = ww; + sizeh->min_height = sizeh->max_height = wh; + } + XStringListToTextProperty(&name, 1, &str); + ch = XAllocClassHint(); + ch->res_class = name; + ch->res_name = name; + + XSetWMProperties(dpy, win, &str, &str, NULL, 0, sizeh, wmh, + ch); + + XFree(keysyms); + XFree(ch); + XFree(wmh); + XFree(str.value); + if (sizeh != NULL) + XFree(sizeh); + + if (isdock) { + XChangeProperty(dpy, win, netatom[NetWMWindowType], XA_ATOM, + 32, PropModeReplace, + (unsigned char *)&atype, 1); + } + + XMapRaised(dpy, win); + drw_resize(drw, ww, wh); + updatekeys(); + drawkeyboard(); +} + +void +updatekeys(void) +{ + int i, j; + int x = 0, y = 0, h, base, r = rows; + + h = (wh - 1) / rows; + for (i = 0; i < numkeys; i++, r--) { + for (j = i, base = 0; j < numkeys && keys[j].keysym != 0; j++) + base += keys[j].width; + for (x = 0; i < numkeys && keys[i].keysym != 0; i++) { + keys[i].x = x; + keys[i].y = y; + keys[i].w = keys[i].width * (ww - 1) / base; + keys[i].h = r == 1 ? wh - y - 1 : h; + x += keys[i].w; + } + if (base != 0) + keys[i - 1].w = ww - 1 - keys[i - 1].x; + y += h; + } +} + +void +usage(char *argv0) +{ + fprintf(stderr, "usage: %s [-hdvDO] [-g geometry] [-fn font] [-l layers] [-s initial_layer]\n", argv0); + fprintf(stderr, "Options:\n"); + fprintf(stderr, " -d - Set Dock Window Type\n"); + fprintf(stderr, " -D - Enable debug\n"); + fprintf(stderr, " -O - Disable overlays\n"); + fprintf(stderr, " -l - Comma separated list of layers to enable\n"); + fprintf(stderr, " -s - Layer to select on program start\n"); + fprintf(stderr, " -H [int] - Height fraction, one key row takes 1/x of the screen height"); + fprintf(stderr, " -fn [font] - Set font (Xft, e.g: DejaVu Sans:bold:size=20)\n"); + exit(1); +} + +void +setlayer(void) +{ + numkeys = countkeys(layers[currentlayer]); + memcpy(&keys, layers[currentlayer], sizeof(Key) * numkeys); +} + +void +cyclelayer(void) +{ + currentlayer++; + if (currentlayer >= numlayers) + currentlayer = 0; + if (debug) printdbg("Cycling to layer %d\n", currentlayer); + setlayer(); + updatekeys(); + drawkeyboard(); +} + +void +togglelayer(void) +{ + if (currentlayer > 0) { + currentlayer = 0; + } else if (numlayers > 1) { + currentlayer = 1; + } + if (debug) printdbg("Toggling layer %d\n", currentlayer); + setlayer(); + updatekeys(); + drawkeyboard(); +} + +void +showoverlay(int idx) +{ + if (debug) printdbg("Showing overlay %d\n", idx); + int i,j; + + /* unpress existing key (visually only) */ + for (i = 0; i < numkeys; i++) { + if (keys[i].pressed && !IsModifierKey(keys[i].keysym)) { + keys[i].pressed = 0; + drawkey(&keys[i]); + break; + } + } + + for (i = idx, j=0; i < OVERLAYS; i++, j++) { + if (overlay[i].keysym == XK_Cancel) + break; + while (keys[j].keysym == 0) + j++; + keys[j].label = overlay[i].label; + keys[j].keysym = overlay[i].keysym; + } + currentoverlay = idx; + overlaykeysym = ispressingkeysym; + releaseprotect = 1; + updatekeys(); + drawkeyboard(); + XSync(dpy, False); +} + +void +hideoverlay(void) +{ + if (debug) printdbg("Hiding overlay, overlay was #%d\n", currentoverlay); + currentoverlay = -1; + overlaykeysym = 0; + currentlayer--; + cyclelayer(); +} + +void +sigterm(int signo) +{ + running = False; + sigtermd = True; + if (debug) printdbg("SIGTERM received\n"); +} + +void +init_layers(char *layer_names_list, const char *initial_layer_name) +{ + char *s; + int j, found; + + if (layer_names_list == NULL) { + numlayers = LAYERS; + memcpy(&layers, &available_layers, sizeof(available_layers)); + if (initial_layer_name != NULL) { + for (j = 0; j < LAYERS; j++) { + if (strcmp(layer_names[j], initial_layer_name) == 0) { + currentlayer = j; + break; + } + } + } + } else { + s = strtok(layer_names_list, ","); + while (s != NULL) { + if (numlayers+1 > LAYERS) + die("too many layers specified"); + found = 0; + for (j = 0; j < LAYERS; j++) { + if (strcmp(layer_names[j], s) == 0) { + fprintf(stderr, "Adding layer %s\n", s); + layers[numlayers] = available_layers[j]; + if (initial_layer_name != NULL && strcmp(layer_names[j], initial_layer_name) == 0) { + currentlayer = numlayers; + } + found = 1; + break; + } + } + if (!found) { + fprintf(stderr, "Undefined layer: %s\n", s); + exit(3); + } + numlayers++; + s = strtok(NULL,","); + } + } + setlayer(); +} + +void +printdbg(const char *fmt, ...) +{ + va_list ap; + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + fflush(stderr); +} + +int +main(int argc, char *argv[]) +{ + char *initial_layer_name = NULL; + char *layer_names_list = NULL; + char *tmp; + int i, xr, yr, bitm; + unsigned int wr, hr; + + signal(SIGTERM, sigterm); + + if (OVERLAYS <= 1) { + enableoverlays = 0; + } else { + if ((tmp = getenv("SVKBD_ENABLEOVERLAYS"))) + enableoverlays = atoi(tmp); + } + if ((tmp = getenv("SVKBD_LAYERS"))) { + if (!(layer_names_list = strdup(tmp))) + die("memory allocation error"); + } + + if ((tmp = getenv("SVKBD_HEIGHTFACTOR"))) + heightfactor = atoi(tmp); + + /* parse command line arguments */ + for (i = 1; argv[i]; i++) { + if (!strcmp(argv[i], "-v")) { + die("svkbd-"VERSION); + } else if (!strcmp(argv[i], "-d")) { + isdock = True; + continue; + } else if (!strncmp(argv[i], "-g", 2)) { + if (i >= argc - 1) + continue; + + bitm = XParseGeometry(argv[i + 1], &xr, &yr, &wr, &hr); + if (bitm & XValue) + wx = xr; + if (bitm & YValue) + wy = yr; + if (bitm & WidthValue) + ww = (int)wr; + if (bitm & HeightValue) + wh = (int)hr; + if (bitm & XNegative && wx == 0) + wx = -1; + if (bitm & YNegative && wy == 0) + wy = -1; + i++; + } else if (!strcmp(argv[i], "-fn")) { /* font or font set */ + fonts[0] = argv[++i]; + } else if (!strcmp(argv[i], "-D")) { + debug = 1; + } else if (!strcmp(argv[i], "-h")) { + usage(argv[0]); + } else if (!strcmp(argv[i], "-O")) { + enableoverlays = 0; + } else if (!strcmp(argv[i], "-l")) { + if (i >= argc - 1) + continue; + free(layer_names_list); + if (!(layer_names_list = strdup(argv[++i]))) + die("memory allocation error"); + } else if (!strcmp(argv[i], "-s")) { + if (i >= argc - 1) + continue; + initial_layer_name = argv[++i]; + } else if (!strcmp(argv[i], "-H")) { + if (i >= argc - 1) + continue; + heightfactor = atoi(argv[++i]); + } else { + fprintf(stderr, "Invalid argument: %s\n", argv[i]); + exit(2); + } + } + + if (heightfactor <= 0) + die("height factor must be a positive integer"); + + init_layers(layer_names_list, initial_layer_name); + + if (!setlocale(LC_CTYPE, "") || !XSupportsLocale()) + fprintf(stderr, "warning: no locale support"); + if (!(dpy = XOpenDisplay(0))) + die("cannot open display"); + setup(); + run(); + cleanup(); + XCloseDisplay(dpy); + free(layer_names_list); + + return 0; +} diff --git a/svkbd.o b/svkbd.o new file mode 100644 index 0000000..550c7d2 Binary files /dev/null and b/svkbd.o differ diff --git a/util.c b/util.c new file mode 100644 index 0000000..176f807 --- /dev/null +++ b/util.c @@ -0,0 +1,36 @@ +/* See LICENSE file for copyright and license details. */ +#include +#include +#include +#include + +#include "util.h" + +void * +ecalloc(size_t nmemb, size_t size) +{ + void *p; + + if (!(p = calloc(nmemb, size))) + die("calloc:"); + return p; +} + +void +die(const char *fmt, ...) +{ + va_list ap; + + va_start(ap, fmt); + vfprintf(stderr, fmt, ap); + va_end(ap); + + if (fmt[0] && fmt[strlen(fmt)-1] == ':') { + fputc(' ', stderr); + perror(NULL); + } else { + fputc('\n', stderr); + } + + exit(1); +} diff --git a/util.h b/util.h new file mode 100644 index 0000000..f633b51 --- /dev/null +++ b/util.h @@ -0,0 +1,8 @@ +/* See LICENSE file for copyright and license details. */ + +#define MAX(A, B) ((A) > (B) ? (A) : (B)) +#define MIN(A, B) ((A) < (B) ? (A) : (B)) +#define BETWEEN(X, A, B) ((A) <= (X) && (X) <= (B)) + +void die(const char *fmt, ...); +void *ecalloc(size_t nmemb, size_t size); diff --git a/util.o b/util.o new file mode 100644 index 0000000..0dd6399 Binary files /dev/null and b/util.o differ