diff options
-rw-r--r-- | .gitignore | 4 | ||||
-rw-r--r-- | Makefile | 38 | ||||
-rw-r--r-- | main.cpp | 120 | ||||
-rwxr-xr-x | make_keysym_table.sh | 29 | ||||
-rw-r--r-- | xutil.cpp | 85 | ||||
-rw-r--r-- | xutil.h | 95 |
6 files changed, 371 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d0750f6 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +obj/ +blockuntilkey +keysym_table.h +keysym_table.cpp diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..3003995 --- /dev/null +++ b/Makefile @@ -0,0 +1,38 @@ +CXX = g++ +CXXFLAGS = -Wall -Wextra -std=c++17 -O2 -g +LDFLAGS = -lX11 +TARGET = blockuntilkey + +# 'sort' removes duplicates +SOURCES := $(sort $(wildcard *.cpp) keysym_table.cpp) +HEADERS := $(sort $(wildcard *.cpp) keysym_table.h) + +OBJDIR = obj + +.PHONY: all clean + +all: $(TARGET) + +clean: + @echo "Cleaning" + @rm -f $(TARGET) + @rm -rf $(OBJDIR) + + +$(OBJDIR)/%.o: %.cpp $(wildcard *.h) | $(OBJDIR) + @echo "CXX $<" + @$(CXX) $(CXXFLAGS) -c -o $@ $< + +$(TARGET): $(patsubst %.cpp,$(OBJDIR)/%.o,$(SOURCES)) | $(OBJDIR) + @echo "LD -o $@" + @$(CXX) -o $@ $^ $(LDFLAGS) + +$(OBJDIR): + @mkdir -p $(OBJDIR) + + +keysym_table.h: make_keysym_table.sh /usr/include/xkbcommon/xkbcommon-keysyms.h + ./make_keysym_table.sh --header >$@ + +keysym_table.cpp: make_keysym_table.sh /usr/include/xkbcommon/xkbcommon-keysyms.h + ./make_keysym_table.sh --source >$@ diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..d597629 --- /dev/null +++ b/main.cpp @@ -0,0 +1,120 @@ +#include <iostream> +#include <vector> +#include <string> +#include <optional> +#include <utility> +#include <cstring> +#include "xutil.h" +#include "keysym_table.h" + + +namespace { + void usage(const char *argv0) { + std::cerr + << "Usage:\n" + << " " << argv0 << " <key>\n" + << " " << argv0 << " [<key> <output>]*\n" + << "\n" + << "Keys are specified with the X11 keysym name or ID (in hexadecimal with \"0x\").\n" + << "Both are given by xev(1).\n" + << "Examples:\n" + << "- KP_1 and 0xffb1 are valid key values for the '1' key on the numpad.\n" + << "- XF86Launch7 and 0x1008ff47 are valid key values for F16 on an Apple keyboard.\n" + << "The values can also be found in /usr/include/xkbcommon/xkbcommon-keysyms.h .\n" + << "\n" + << "If a single key is given, the program grabs that key and exits when the key is\n" + << "pressed. If multiple keys are given, the output string corresponding to the\n" + << "pressed key will be printed before exiting.\n" + ; + } + + std::vector<std::pair<x::Keysym, std::string>> parse_watchlist(int argc, char **argv) { + std::vector<std::pair<x::Keysym, std::string>> watchlist; + + if (argc <= 1) { + usage(argv[0]); + exit(1); + } + + for (int i = 1; i < argc; i++) { + if (strcmp(argv[i], "--help") == 0 || strcmp(argv[i], "-h") == 0) { + usage(argv[0]); + exit(0); + } + + std::optional<x::Keysym> msym; + + if (argv[i][0] == '0' && argv[i][1] == 'x') { + char *endp; + unsigned code = strtol(argv[i] + 2, &endp, 16); + if (argv[i][2] == '\0' || *endp != '\0') { + std::cerr << "Invalid hexadecimal number in '" << argv[i] << "'\n"; + exit(1); + } + msym = x::Keysym{code}; + } + + if (!msym) { + for (size_t j = 0; j < keysym_table.size(); j++) { + if (strcmp(argv[i], keysym_table[j].first) == 0) { + msym = keysym_table[j].second; + break; + } + } + } + + if (!msym) { + std::cerr << "Unknown keysym '" << argv[i] << "'\n"; + exit(1); + } + + std::string output; + if (i == 1 && argc == 2) { + // empty output + } else if (i >= argc - 1) { + std::cerr << "Missing output for keysym '" << argv[i] << "'\n"; + exit(1); + } else { + output = argv[i+1]; + i++; // skip output + } + + watchlist.emplace_back(*msym, std::move(output)); + } + + return watchlist; + } +} + +int main(int argc, char **argv) { + std::vector<std::pair<x::Keysym, std::string>> watchlist = parse_watchlist(argc, argv); + + auto dpypair = x::XOpenDisplayRAII(nullptr); + Display *dpy = dpypair.first; + + Window root = DefaultRootWindow(dpy); + + std::vector<x::UponExit<std::function<void()>>> cleaners; + cleaners.reserve(watchlist.size()); + std::vector<x::Keycode> keycodes; + keycodes.reserve(watchlist.size()); + + for (const auto &p : watchlist) { + x::Keycode code = p.first.toCode(dpy); + keycodes.push_back(code); + cleaners.push_back(x::XGrabKeyRAII(dpy, code, AnyModifier, root)); + } + + XSelectInput(dpy, root, KeyPressMask); + while (true) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type != KeyPress) continue; + for (size_t i = 0; i < watchlist.size(); i++) { + if (ev.xkey.keycode == (unsigned int)keycodes[i]) { + if (watchlist[i].second.size() > 0) std::cout << watchlist[i].second << std::endl; + return 0; + } + } + } +} diff --git a/make_keysym_table.sh b/make_keysym_table.sh new file mode 100755 index 0000000..5b0c325 --- /dev/null +++ b/make_keysym_table.sh @@ -0,0 +1,29 @@ +#!/usr/bin/env bash + +output=$(sed -n 's/^#define XKB_KEY_\([a-zA-Z_0-9]\+\)\s\+\(0x[0-9a-f]\+\).*/\t{"\1", {\2}},/p' /usr/include/xkbcommon/xkbcommon-keysyms.h) + +len=$(wc -l <<<"$output") + +if [[ $1 = --source ]]; then + cat <<EOF +#include "keysym_table.h" + +const std::array<std::pair<const char*, x::Keysym>, $len> keysym_table{{ +$output +}}; +EOF +elif [[ $1 = --header ]]; then + cat <<EOF +#pragma once + +#include <array> +#include <utility> +#include "xutil.h" + + +extern const std::array<std::pair<const char*, x::Keysym>, $len> keysym_table; +EOF +else + echo >&2 "$0: invalid flags" + exit 1 +fi diff --git a/xutil.cpp b/xutil.cpp new file mode 100644 index 0000000..7adceaa --- /dev/null +++ b/xutil.cpp @@ -0,0 +1,85 @@ +#include <iostream> +#include <functional> +#include <X11/XKBlib.h> +#include "xutil.h" + + +namespace x { + + Keycode Keysym::toCode(Display *dpy) const { + return XKeysymToKeycode(dpy, sym); + } + + void bel(Display *dpy) { + XkbBell(dpy, None, 100, None); + } + + using UponExitF = UponExit<std::function<void()>>; + + UponExitF XGrabKeyRAII(Display *dpy, Keycode code, int modifier, Window win) { + XGrabKey(dpy, (unsigned int)code, modifier, win, False, GrabModeAsync, GrabModeAsync); + return UponExitF{[dpy, code, modifier, win]() { + XUngrabKey(dpy, (unsigned int)code, modifier, win); + XSync(dpy, False); + }}; + } + + UponExitF XGrabKeyboardRAII(Display *dpy, Window win) { + int ret = XGrabKeyboard(dpy, win, False, GrabModeAsync, GrabModeAsync, CurrentTime); + if (ret == AlreadyGrabbed) { + XUngrabKeyboard(dpy, CurrentTime); + XSync(dpy, False); + throw std::runtime_error("Cannot grab keyboard: already grabbed"); + } + return UponExitF{[dpy]() { + XUngrabKeyboard(dpy, CurrentTime); + XSync(dpy, False); + }}; + } + + std::pair<Display*, UponExitF> XOpenDisplayRAII(const char *name) { + Display *dpy = XOpenDisplay(name); + if (dpy == nullptr) { + std::cerr << "Cannot open X display" << std::endl; + exit(1); + } + return std::make_pair(dpy, UponExitF{[dpy]() { + XCloseDisplay(dpy); + }}); + } + + void globalKeyWatch(Display *dpy, Keysym keysym, std::function<bool(const XKeyEvent&)> callback) { + const Window root = DefaultRootWindow(dpy); + const Keycode keycode = keysym.toCode(dpy); + + auto guard = XGrabKeyRAII(dpy, keycode, AnyModifier, root); + + XSelectInput(dpy, root, KeyPressMask); + while (true) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == KeyPress && ev.xkey.keycode == (unsigned int)keycode) { + if (callback(ev.xkey)) return; + } + } + } + + void globalKeyboardGrab(Display *dpy, std::function<bool(const XKeyEvent&)> callback) { + const Window root = DefaultRootWindow(dpy); + + try { + auto guard = XGrabKeyboardRAII(dpy, root); + + while (true) { + XEvent ev; + XNextEvent(dpy, &ev); + if (ev.type == KeyPress) { + if (callback(ev.xkey)) return; + } + } + } catch (std::exception &e) { + std::cerr << e.what() << std::endl; + } + } + +} @@ -0,0 +1,95 @@ +#pragma once + +#include <functional> +#include <optional> +#include <X11/Xlib.h> + + +namespace x { + + // Typed wrapper for an X keycode + class Keycode { + public: + Keycode() = default; + inline Keycode(unsigned int code) : code{code} {}; + inline explicit operator unsigned int() const { return code; }; + inline bool operator==(Keycode other) const { return code == other.code; } + private: + unsigned int code = 0; + }; + + // Typed wrapper for an X keysym + class Keysym { + public: + Keysym() = default; + inline Keysym(unsigned int sym) : sym{sym} {}; + inline explicit operator unsigned int() const { return sym; } + Keycode toCode(Display *dpy) const; + inline bool operator==(Keysym other) const { return sym == other.sym; } + private: + unsigned int sym = 0; + }; + + // Play the system bell + void bel(Display *dpy); + + // Run a handler on scope exit + template <typename Cleanup> + class UponExit { + public: + UponExit(Cleanup cleanup) : cleanup{cleanup} {} + ~UponExit() { + if (cleanup) (*cleanup)(); + } + UponExit(const UponExit&) = delete; + UponExit(UponExit &&other) : cleanup{move(other.cleanup)} { + other.cleanup.reset(); + } + UponExit& operator=(const UponExit&) = delete; + UponExit& operator=(UponExit &&other) { + cleanup = move(other.cleanup); + other.cleanup.reset(); + } + + private: + std::optional<Cleanup> cleanup; + }; + + // Grab a single key (code + modifier) on the X server in the given window. + // Drop the UponExit to ungrab. For a global grab, use + // DefaultRootWindow(dpy) as the window. + UponExit<std::function<void()>> XGrabKeyRAII(Display *dpy, Keycode code, int modifier, Window win); + + // Grab a the whole keyboard on the X server in the given window. Drop the + // UponExit to ungrab. For a global grab, use DefaultRootWindow(dpy) as the + // window. + UponExit<std::function<void()>> XGrabKeyboardRAII(Display *dpy, Window win); + + // Open the specified display (pass nullptr to use $DISPLAY). Drop the UponExit to close. + std::pair<Display*, UponExit<std::function<void()>>> XOpenDisplayRAII(const char *name); + + // Grab the specified key globally for any modifier combination, and run + // the callback on every hit of that key. Ungrabs and returns when the + // callback returns true. + void globalKeyWatch(Display *dpy, Keysym keysym, std::function<bool(const XKeyEvent&)> callback); + + // Grab the whole keyboard globally, and run the callback on every keyboard + // key event. Ungrabs and returns when the callback returns true. + void globalKeyboardGrab(Display *dpy, std::function<bool(const XKeyEvent&)> callback); + +} + +template <> +struct std::hash<x::Keycode> { + size_t operator()(x::Keycode code) const { + return std::hash<unsigned int>{}((unsigned int)code); + } +}; + +template <> +struct std::hash<x::Keysym> { + size_t operator()(x::Keysym sym) const { + return std::hash<unsigned int>{}((unsigned int)sym); + } +}; + |