summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom@tomsmeding.com>2024-01-31 15:42:17 +0100
committerTom Smeding <tom@tomsmeding.com>2024-01-31 15:42:17 +0100
commit6060db32b949d62757b668f972648465f1e302c7 (patch)
tree53fcc624d250a7d42d15cfcae4909ed6c21dd3be
Initial
-rw-r--r--.gitignore4
-rw-r--r--Makefile38
-rw-r--r--main.cpp120
-rwxr-xr-xmake_keysym_table.sh29
-rw-r--r--xutil.cpp85
-rw-r--r--xutil.h95
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;
+ }
+ }
+
+}
diff --git a/xutil.h b/xutil.h
new file mode 100644
index 0000000..609d20c
--- /dev/null
+++ b/xutil.h
@@ -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);
+ }
+};
+