summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--command.h5
-rw-r--r--got.cpp45
-rw-r--r--got.h26
-rw-r--r--gui.cpp35
-rw-r--r--gui.h17
-rw-r--r--main.cpp359
-rw-r--r--seqmatcher.cpp72
-rw-r--r--seqmatcher.h44
-rw-r--r--sqliteutil.cpp55
-rw-r--r--sqliteutil.h9
-rw-r--r--xutil.cpp113
-rw-r--r--xutil.h94
12 files changed, 522 insertions, 352 deletions
diff --git a/command.h b/command.h
index 7166eb6..91d3b08 100644
--- a/command.h
+++ b/command.h
@@ -5,5 +5,10 @@
#include <utility>
+// Run a command and wait for completion; if successful (exit code 0), return
+// the stdout output, else throw an exception.
std::string runCommand(const std::vector<std::string> &args);
+
+// Run a command and wait for completion, returning the exit code and the
+// stdout output.
std::pair<int, std::string> readCommand(const std::vector<std::string> &args);
diff --git a/got.cpp b/got.cpp
new file mode 100644
index 0000000..e60b4c6
--- /dev/null
+++ b/got.cpp
@@ -0,0 +1,45 @@
+#include <sstream>
+#include <algorithm>
+#include "command.h"
+#include "got.h"
+#include "sqliteutil.h"
+
+
+namespace got {
+ std::string getDBpath() {
+ return std::string{getenv("HOME")} + "/.timetrap.db";
+ }
+
+ // Returns {sheet, note}
+ std::optional<std::pair<std::string, std::string>> getRunning() {
+ std::string output = runCommand({"sqlite3", getDBpath(), ".mode csv", "select sheet, note from entries where end is null"});
+ auto table = sqlite::parseCSV(move(output));
+ if (table.empty()) return std::nullopt;
+ else return std::make_pair(table[0][0], table[0][1]);
+ }
+
+ std::vector<std::string> getSheets() {
+ std::string output = runCommand({"sqlite3", getDBpath(), "select distinct sheet from entries"});
+ std::istringstream ss{output};
+ std::vector<std::string> lines;
+ std::string line;
+ while (std::getline(ss, line)) {
+ if (line.size() > 0) lines.push_back(move(line));
+ }
+ std::sort(lines.begin(), lines.end());
+ return lines;
+ }
+
+ void editRunning(const std::string &descr) {
+ runCommand({"got", "edit", descr});
+ }
+
+ void checkOut() {
+ runCommand({"got", "out"});
+ }
+
+ void checkIn(const std::string &sheet) {
+ runCommand({"got", "sheet", sheet});
+ runCommand({"got", "in"});
+ }
+}
diff --git a/got.h b/got.h
new file mode 100644
index 0000000..bc2128c
--- /dev/null
+++ b/got.h
@@ -0,0 +1,26 @@
+#pragma once
+
+#include <vector>
+#include <string>
+#include <optional>
+
+
+namespace got {
+ // The path to the database
+ std::string getDBpath();
+
+ // Returns {sheet, note}
+ std::optional<std::pair<std::string, std::string>> getRunning();
+
+ // Returns names of sheets occurring in the database
+ std::vector<std::string> getSheets();
+
+ // Change the description of the currently running task to the given string
+ void editRunning(const std::string &descr);
+
+ // Check out of any current task
+ void checkOut();
+
+ // Check into the given sheet
+ void checkIn(const std::string &sheet);
+}
diff --git a/gui.cpp b/gui.cpp
new file mode 100644
index 0000000..802bc9e
--- /dev/null
+++ b/gui.cpp
@@ -0,0 +1,35 @@
+#include "command.h"
+#include "gui.h"
+
+
+namespace {
+ std::string trim(const std::string &s) {
+ size_t left = 0;
+ while (left < s.size() && isspace(s[left])) left++;
+ size_t right = s.size() - 1;
+ while (right >= left && isspace(s[right])) right--;
+ return s.substr(left, right - left + 1);
+ }
+}
+
+namespace gui {
+ void showNotification(const std::string &message) {
+ runCommand({"zenity", "--notification", "--text", message});
+ }
+
+ std::optional<std::string> promptText(const std::string &message) {
+ auto result = readCommand({"zenity", "--entry", "--text", message});
+ if (result.first == 0) return result.second;
+ else return std::nullopt;
+ }
+
+ std::optional<std::string> chooseList(const std::string &message, const std::string &header, const std::vector<std::string> &options) {
+ std::vector<std::string> args{
+ "zenity", "--list", "--text", message, "--column", header
+ };
+ args.insert(args.end(), options.begin(), options.end());
+ auto result = readCommand(args);
+ if (result.first == 0) return trim(result.second);
+ else return std::nullopt;
+ }
+}
diff --git a/gui.h b/gui.h
new file mode 100644
index 0000000..9fa77d1
--- /dev/null
+++ b/gui.h
@@ -0,0 +1,17 @@
+#pragma once
+
+#include <vector>
+#include <string>
+#include <optional>
+
+
+namespace gui {
+ // Show a dialog with the given message
+ void showNotification(const std::string &message);
+
+ // Request a line of text from the user
+ std::optional<std::string> promptText(const std::string &message);
+
+ // Let the user choose from a list of items
+ std::optional<std::string> chooseList(const std::string &message, const std::string &header, const std::vector<std::string> &options);
+}
diff --git a/main.cpp b/main.cpp
index 45eef83..f2d6a9a 100644
--- a/main.cpp
+++ b/main.cpp
@@ -1,358 +1,13 @@
#include <iostream>
-#include <sstream>
-#include <memory>
-#include <vector>
-#include <unordered_map>
-#include <variant>
-#include <stdexcept>
-#include <functional>
#include <filesystem>
#include <cstring>
-#include <cassert>
-#include <unistd.h>
-#include <X11/Xlib.h>
#include <X11/Xutil.h>
-#include <X11/XKBlib.h>
-#include "command.h"
+#include "seqmatcher.h"
+#include "got.h"
+#include "gui.h"
+#include "xutil.h"
-std::string trim(const std::string &s) {
- size_t left = 0;
- while (left < s.size() && isspace(s[left])) left++;
- size_t right = s.size() - 1;
- while (right >= left && isspace(s[right])) right--;
- return s.substr(left, right - left + 1);
-}
-
-class X_keycode {
-public:
- X_keycode() : code{0} {}
- X_keycode(unsigned int code) : code{code} {}
- explicit operator unsigned int() const { return code; }
- bool operator==(X_keycode other) const { return code == other.code; }
-private: unsigned int code;
-};
-
-template <>
-struct std::hash<X_keycode> {
- size_t operator()(X_keycode code) const {
- return std::hash<unsigned int>{}((unsigned int)code);
- }
-};
-
-class X_keysym {
-public:
- X_keysym() : sym{0} {}
- X_keysym(unsigned int sym) : sym{sym} {}
- explicit operator unsigned int() const { return sym; }
- X_keycode toCode(Display *dpy) const { return XKeysymToKeycode(dpy, sym); }
- bool operator==(X_keysym other) const { return sym == other.sym; }
-private: unsigned int sym;
-};
-
-template <>
-struct std::hash<X_keysym> {
- size_t operator()(X_keysym sym) const {
- return std::hash<unsigned int>{}((unsigned int)sym);
- }
-};
-
-void bel(Display *dpy) {
- XkbBell(dpy, None, 100, None);
-}
-
-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;
-};
-
-auto XGrabKeyRAII(Display *dpy, X_keycode code, int modifier, Window win) {
- XGrabKey(dpy, (unsigned int)code, modifier, win, False, GrabModeAsync, GrabModeAsync);
- return UponExit{[dpy, code, modifier, win]() {
- XUngrabKey(dpy, (unsigned int)code, modifier, win);
- XSync(dpy, False);
- }};
-}
-
-auto 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 UponExit{[dpy]() {
- XUngrabKeyboard(dpy, CurrentTime);
- XSync(dpy, False);
- }};
-}
-
-auto 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, UponExit{[dpy]() {
- XCloseDisplay(dpy);
- }});
-}
-
-template <typename F> // return true to stop watch and loop
-void globalKeyWatch(Display *dpy, X_keysym headerSym, F callback) {
- const Window root = DefaultRootWindow(dpy);
- const X_keycode headerCode = headerSym.toCode(dpy);
-
- auto guard = XGrabKeyRAII(dpy, headerCode, AnyModifier, root);
-
- XSelectInput(dpy, root, KeyPressMask);
- while (true) {
- XEvent ev;
- XNextEvent(dpy, &ev);
- if (ev.type == KeyPress && ev.xkey.keycode == (unsigned int)headerCode) {
- if (callback(ev.xkey)) return;
- }
- }
-}
-
-template <typename F> // return true to lose grab and loop
-void globalKeyboardGrab(Display *dpy, F 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;
- }
-}
-
-class SeqMatcher {
-public:
- using Callback = std::function<void()>;
-
- struct SymSequence {
- std::vector<X_keysym> syms;
- Callback callback;
- };
-
- SeqMatcher(Display *dpy) : dpy{dpy} {}
-
- SeqMatcher(Display *dpy, std::vector<SymSequence> seqs)
- : dpy{dpy} {
- for (const auto &seq : seqs) addSequence(seq.syms, seq.callback);
- }
-
- void addSequence(const std::vector<X_keysym> &syms, Callback callback) {
- if (syms.empty()) {
- throw std::logic_error("Cannot register empty key sequence");
- }
-
- Node *current = &rootNode;
- for (X_keysym sym : syms) {
- if (std::holds_alternative<Callback>(current->v)) {
- throw std::logic_error("Overlapping key sequences (second is longer)");
- } else {
- if (!std::holds_alternative<NodeMap>(current->v)) {
- current->v.emplace<NodeMap>();
- }
- NodeMap &map = std::get<NodeMap>(current->v);
- X_keycode code = sym.toCode(dpy);
- auto it = map.find(code);
- if (it != map.end()) {
- current = it->second.get();
- } else {
- current = map.emplace(sym.toCode(dpy), std::make_unique<Node>()).first->second.get();
- }
- }
- }
-
- if (auto *map = std::get_if<NodeMap>(&current->v)) {
- if (!map->empty()) {
- throw std::logic_error("Overlapping key sequences (second is shorter)");
- }
- }
- if (std::holds_alternative<Callback>(current->v)) {
- throw std::logic_error("Overlapping key sequences (equally long)");
- }
- current->v.emplace<Callback>(callback);
- }
-
- // Returns bel()-running callback if unrecognised sequence is given
- std::optional<Callback> observe(const XKeyEvent &ev) {
- auto *map = std::get_if<NodeMap>(&curNode->v);
- assert(map);
-
- auto it = map->find(X_keycode{ev.keycode});
- if (it == map->end()) {
- // Sequence not found
- reset();
- return [dpy = dpy]() { bel(dpy); };
- }
-
- curNode = it->second.get();
-
- if (auto *cb = std::get_if<Callback>(&curNode->v)) {
- // Sequence completed
- reset();
- return *cb;
- }
-
- // Need more keys
- return std::nullopt;
- }
-
- void reset() {
- curNode = &rootNode;
- }
-
-private:
- struct Node;
- using NodeMap = std::unordered_map<X_keycode, std::unique_ptr<Node>>;
- struct Node {
- std::variant<NodeMap, Callback> v;
- };
-
- Display *const dpy;
- Node rootNode;
- Node *curNode = &rootNode;
-};
-
-namespace sqlite {
- std::vector<std::vector<std::string>> parseCSV(std::string output) {
- std::istringstream ss{output};
- std::vector<std::vector<std::string>> table;
-
- std::string line;
- while (std::getline(ss, line)) {
- while (!line.empty() && strchr("\r\n", line.back()) != nullptr)
- line.pop_back();
-
- table.emplace_back();
-
- if (line.empty()) continue;
-
- std::vector<std::string> &row = table.back();
- row.emplace_back();
- bool inString = false;
- for (size_t i = 0; i < line.size(); i++) {
- switch (line[i]) {
- case '"':
- if (inString) {
- if (i + 1 < line.size() && line[i+1] == '"') {
- row.back().push_back('"');
- i++;
- } else {
- inString = false;
- }
- } else {
- inString = true;
- }
- break;
-
- case ',':
- if (inString) {
- row.back().push_back(',');
- } else {
- row.emplace_back();
- }
- break;
-
- default:
- row.back().push_back(line[i]);
- break;
- }
- }
- }
-
- return table;
- }
-}
-
-namespace got {
- std::string getDBpath() {
- return std::string{getenv("HOME")} + "/.timetrap.db";
- }
-
- // Returns {sheet, note}
- std::optional<std::pair<std::string, std::string>> getRunning() {
- std::string output = runCommand({"sqlite3", getDBpath(), ".mode csv", "select sheet, note from entries where end is null"});
- auto table = sqlite::parseCSV(move(output));
- if (table.empty()) return std::nullopt;
- else return std::make_pair(table[0][0], table[0][1]);
- }
-
- std::vector<std::string> getSheets() {
- std::string output = runCommand({"sqlite3", getDBpath(), "select distinct sheet from entries"});
- std::istringstream ss{output};
- std::vector<std::string> lines;
- std::string line;
- while (std::getline(ss, line)) {
- if (line.size() > 0) lines.push_back(move(line));
- }
- std::sort(lines.begin(), lines.end());
- return lines;
- }
-
- void editRunning(const std::string &descr) {
- runCommand({"got", "edit", descr});
- }
-
- void checkOut() {
- runCommand({"got", "out"});
- }
-
- void checkIn(const std::string &sheet) {
- runCommand({"got", "sheet", sheet});
- runCommand({"got", "in"});
- }
-}
-
-namespace gui {
- void showNotification(const std::string &message) {
- runCommand({"zenity", "--notification", "--text", message});
- }
-
- std::optional<std::string> promptText(const std::string &message) {
- auto result = readCommand({"zenity", "--entry", "--text", message});
- if (result.first == 0) return result.second;
- else return std::nullopt;
- }
-
- std::optional<std::string> chooseList(const std::string &message, const std::string &header, const std::vector<std::string> &options) {
- std::vector<std::string> args{
- "zenity", "--list", "--text", message, "--column", header
- };
- args.insert(args.end(), options.begin(), options.end());
- auto result = readCommand(args);
- if (result.first == 0) return trim(result.second);
- else return std::nullopt;
- }
-}
-
int main(int argc, char **argv) {
if (argc >= 2 && (strcmp(argv[1], "-h") == 0 || strcmp(argv[1], "--help") == 0)) {
std::cout <<
@@ -374,7 +29,7 @@ int main(int argc, char **argv) {
return 1;
}
- auto dpy_pair = XOpenDisplayRAII(nullptr);
+ auto dpy_pair = x::XOpenDisplayRAII(nullptr);
Display *dpy = dpy_pair.first;
bool quitRequested = false;
@@ -438,14 +93,14 @@ int main(int argc, char **argv) {
quitRequested = true;
});
- globalKeyWatch(
+ x::globalKeyWatch(
dpy, XK_Break,
[dpy, &matcher, &quitRequested](const XKeyEvent&) -> bool {
matcher.reset();
SeqMatcher::Callback cb;
- globalKeyboardGrab(dpy, [&matcher, &cb](const XKeyEvent &ev) -> bool {
+ x::globalKeyboardGrab(dpy, [&matcher, &cb](const XKeyEvent &ev) -> bool {
auto opt_cb = matcher.observe(ev);
if (opt_cb) {
cb = *opt_cb;
diff --git a/seqmatcher.cpp b/seqmatcher.cpp
new file mode 100644
index 0000000..f0abedc
--- /dev/null
+++ b/seqmatcher.cpp
@@ -0,0 +1,72 @@
+#include "seqmatcher.h"
+#include <cassert>
+
+
+SeqMatcher::SeqMatcher(Display *dpy) : dpy{dpy} {}
+
+SeqMatcher::SeqMatcher(Display *dpy, std::vector<SymSequence> seqs)
+ : dpy{dpy} {
+ for (const auto &seq : seqs) addSequence(seq.syms, seq.callback);
+}
+
+void SeqMatcher::addSequence(const std::vector<x::Keysym> &syms, Callback callback) {
+ if (syms.empty()) {
+ throw std::logic_error("Cannot register empty key sequence");
+ }
+
+ Node *current = &rootNode;
+ for (x::Keysym sym : syms) {
+ if (std::holds_alternative<Callback>(current->v)) {
+ throw std::logic_error("Overlapping key sequences (second is longer)");
+ } else {
+ if (!std::holds_alternative<NodeMap>(current->v)) {
+ current->v.emplace<NodeMap>();
+ }
+ NodeMap &map = std::get<NodeMap>(current->v);
+ x::Keycode code = sym.toCode(dpy);
+ auto it = map.find(code);
+ if (it != map.end()) {
+ current = it->second.get();
+ } else {
+ current = map.emplace(sym.toCode(dpy), std::make_unique<Node>()).first->second.get();
+ }
+ }
+ }
+
+ if (auto *map = std::get_if<NodeMap>(&current->v)) {
+ if (!map->empty()) {
+ throw std::logic_error("Overlapping key sequences (second is shorter)");
+ }
+ }
+ if (std::holds_alternative<Callback>(current->v)) {
+ throw std::logic_error("Overlapping key sequences (equally long)");
+ }
+ current->v.emplace<Callback>(callback);
+}
+
+std::optional<SeqMatcher::Callback> SeqMatcher::observe(const XKeyEvent &ev) {
+ auto *map = std::get_if<NodeMap>(&curNode->v);
+ assert(map);
+
+ auto it = map->find(x::Keycode{ev.keycode});
+ if (it == map->end()) {
+ // Sequence not found
+ reset();
+ return [dpy = dpy]() { x::bel(dpy); };
+ }
+
+ curNode = it->second.get();
+
+ if (auto *cb = std::get_if<Callback>(&curNode->v)) {
+ // Sequence completed
+ reset();
+ return *cb;
+ }
+
+ // Need more keys
+ return std::nullopt;
+}
+
+void SeqMatcher::reset() {
+ curNode = &rootNode;
+}
diff --git a/seqmatcher.h b/seqmatcher.h
new file mode 100644
index 0000000..7ce6bf4
--- /dev/null
+++ b/seqmatcher.h
@@ -0,0 +1,44 @@
+#pragma once
+
+#include <functional>
+#include <vector>
+#include <variant>
+#include <unordered_map>
+#include <memory>
+#include "xutil.h"
+
+
+class SeqMatcher {
+public:
+ using Callback = std::function<void()>;
+
+ struct SymSequence {
+ std::vector<x::Keysym> syms;
+ Callback callback;
+ };
+
+ SeqMatcher(Display *dpy);
+
+ // Initialise with an initial list of sequences.
+ SeqMatcher(Display *dpy, std::vector<SymSequence> seqs);
+
+ // Register a key sequence
+ void addSequence(const std::vector<x::Keysym> &syms, Callback callback);
+
+ // Returns bel()-running callback if unrecognised sequence is given
+ std::optional<Callback> observe(const XKeyEvent &ev);
+
+ // Cancel any running sequence and restart from the beginning
+ void reset();
+
+private:
+ struct Node;
+ using NodeMap = std::unordered_map<x::Keycode, std::unique_ptr<Node>>;
+ struct Node {
+ std::variant<NodeMap, Callback> v;
+ };
+
+ Display *const dpy;
+ Node rootNode;
+ Node *curNode = &rootNode;
+};
diff --git a/sqliteutil.cpp b/sqliteutil.cpp
new file mode 100644
index 0000000..93b91de
--- /dev/null
+++ b/sqliteutil.cpp
@@ -0,0 +1,55 @@
+#include <sstream>
+#include <cstring>
+#include "sqliteutil.h"
+
+
+namespace sqlite {
+ std::vector<std::vector<std::string>> parseCSV(std::string output) {
+ std::istringstream ss{output};
+ std::vector<std::vector<std::string>> table;
+
+ std::string line;
+ while (std::getline(ss, line)) {
+ while (!line.empty() && strchr("\r\n", line.back()) != nullptr)
+ line.pop_back();
+
+ table.emplace_back();
+
+ if (line.empty()) continue;
+
+ std::vector<std::string> &row = table.back();
+ row.emplace_back();
+ bool inString = false;
+ for (size_t i = 0; i < line.size(); i++) {
+ switch (line[i]) {
+ case '"':
+ if (inString) {
+ if (i + 1 < line.size() && line[i+1] == '"') {
+ row.back().push_back('"');
+ i++;
+ } else {
+ inString = false;
+ }
+ } else {
+ inString = true;
+ }
+ break;
+
+ case ',':
+ if (inString) {
+ row.back().push_back(',');
+ } else {
+ row.emplace_back();
+ }
+ break;
+
+ default:
+ row.back().push_back(line[i]);
+ break;
+ }
+ }
+ }
+
+ return table;
+ }
+}
diff --git a/sqliteutil.h b/sqliteutil.h
new file mode 100644
index 0000000..2971474
--- /dev/null
+++ b/sqliteutil.h
@@ -0,0 +1,9 @@
+#pragma once
+#include <vector>
+#include <string>
+
+
+namespace sqlite {
+ // Parse sqlite's CSV output, producing the query's result table as strings
+ std::vector<std::vector<std::string>> parseCSV(std::string output);
+}
diff --git a/xutil.cpp b/xutil.cpp
new file mode 100644
index 0000000..e5eb8c2
--- /dev/null
+++ b/xutil.cpp
@@ -0,0 +1,113 @@
+#include <iostream>
+#include <functional>
+#include <X11/XKBlib.h>
+#include "xutil.h"
+
+
+namespace x {
+
+ Keycode::Keycode()
+ : code{0} {}
+
+ Keycode::Keycode(unsigned int code)
+ : code{code} {}
+
+ Keycode::operator unsigned int() const {
+ return code;
+ }
+
+ bool Keycode::operator==(Keycode other) const {
+ return code == other.code;
+ }
+
+ Keysym::Keysym()
+ : sym{0} {}
+
+ Keysym::Keysym(unsigned int sym)
+ : sym{sym} {}
+
+ Keysym::operator unsigned int() const {
+ return sym;
+ }
+
+ Keycode Keysym::toCode(Display *dpy) const {
+ return XKeysymToKeycode(dpy, sym);
+ }
+
+ bool Keysym::operator==(Keysym other) const {
+ return sym == other.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..337a190
--- /dev/null
+++ b/xutil.h
@@ -0,0 +1,94 @@
+#pragma once
+
+#include <functional>
+#include <X11/Xlib.h>
+
+
+namespace x {
+
+ // Typed wrapper for an X keycode
+ class Keycode {
+ public:
+ Keycode();
+ Keycode(unsigned int code);
+ explicit operator unsigned int() const;
+ bool operator==(Keycode other) const;
+ private:
+ unsigned int code;
+ };
+
+ // Typed wrapper for an X keysym
+ class Keysym {
+ public:
+ Keysym();
+ Keysym(unsigned int sym);
+ explicit operator unsigned int() const;
+ Keycode toCode(Display *dpy) const;
+ bool operator==(Keysym other) const;
+ private:
+ unsigned int sym;
+ };
+
+ // 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);
+ }
+};
+