From c66b8ded10f0d5e872e507fdff3010f732aa5449 Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Thu, 2 Apr 2020 16:06:35 +0200 Subject: Split main.cpp into separate files --- command.h | 5 + got.cpp | 45 ++++++++ got.h | 26 +++++ gui.cpp | 35 ++++++ gui.h | 17 +++ main.cpp | 359 ++------------------------------------------------------- seqmatcher.cpp | 72 ++++++++++++ seqmatcher.h | 44 +++++++ sqliteutil.cpp | 55 +++++++++ sqliteutil.h | 9 ++ xutil.cpp | 113 ++++++++++++++++++ xutil.h | 94 +++++++++++++++ 12 files changed, 522 insertions(+), 352 deletions(-) create mode 100644 got.cpp create mode 100644 got.h create mode 100644 gui.cpp create mode 100644 gui.h create mode 100644 seqmatcher.cpp create mode 100644 seqmatcher.h create mode 100644 sqliteutil.cpp create mode 100644 sqliteutil.h create mode 100644 xutil.cpp create mode 100644 xutil.h diff --git a/command.h b/command.h index 7166eb6..91d3b08 100644 --- a/command.h +++ b/command.h @@ -5,5 +5,10 @@ #include +// 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 &args); + +// Run a command and wait for completion, returning the exit code and the +// stdout output. std::pair readCommand(const std::vector &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 +#include +#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> 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 getSheets() { + std::string output = runCommand({"sqlite3", getDBpath(), "select distinct sheet from entries"}); + std::istringstream ss{output}; + std::vector 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 +#include +#include + + +namespace got { + // The path to the database + std::string getDBpath(); + + // Returns {sheet, note} + std::optional> getRunning(); + + // Returns names of sheets occurring in the database + std::vector 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 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 chooseList(const std::string &message, const std::string &header, const std::vector &options) { + std::vector 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 +#include +#include + + +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 promptText(const std::string &message); + + // Let the user choose from a list of items + std::optional chooseList(const std::string &message, const std::string &header, const std::vector &options); +} diff --git a/main.cpp b/main.cpp index 45eef83..f2d6a9a 100644 --- a/main.cpp +++ b/main.cpp @@ -1,358 +1,13 @@ #include -#include -#include -#include -#include -#include -#include -#include #include #include -#include -#include -#include #include -#include -#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 { - size_t operator()(X_keycode code) const { - return std::hash{}((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 { - size_t operator()(X_keysym sym) const { - return std::hash{}((unsigned int)sym); - } -}; - -void bel(Display *dpy) { - XkbBell(dpy, None, 100, None); -} - -template -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; -}; - -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 // 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 // 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; - - struct SymSequence { - std::vector syms; - Callback callback; - }; - - SeqMatcher(Display *dpy) : dpy{dpy} {} - - SeqMatcher(Display *dpy, std::vector seqs) - : dpy{dpy} { - for (const auto &seq : seqs) addSequence(seq.syms, seq.callback); - } - - void addSequence(const std::vector &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(current->v)) { - throw std::logic_error("Overlapping key sequences (second is longer)"); - } else { - if (!std::holds_alternative(current->v)) { - current->v.emplace(); - } - NodeMap &map = std::get(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()).first->second.get(); - } - } - } - - if (auto *map = std::get_if(¤t->v)) { - if (!map->empty()) { - throw std::logic_error("Overlapping key sequences (second is shorter)"); - } - } - if (std::holds_alternative(current->v)) { - throw std::logic_error("Overlapping key sequences (equally long)"); - } - current->v.emplace(callback); - } - - // Returns bel()-running callback if unrecognised sequence is given - std::optional observe(const XKeyEvent &ev) { - auto *map = std::get_if(&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(&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>; - struct Node { - std::variant v; - }; - - Display *const dpy; - Node rootNode; - Node *curNode = &rootNode; -}; - -namespace sqlite { - std::vector> parseCSV(std::string output) { - std::istringstream ss{output}; - std::vector> 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 &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> 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 getSheets() { - std::string output = runCommand({"sqlite3", getDBpath(), "select distinct sheet from entries"}); - std::istringstream ss{output}; - std::vector 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 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 chooseList(const std::string &message, const std::string &header, const std::vector &options) { - std::vector 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 + + +SeqMatcher::SeqMatcher(Display *dpy) : dpy{dpy} {} + +SeqMatcher::SeqMatcher(Display *dpy, std::vector seqs) + : dpy{dpy} { + for (const auto &seq : seqs) addSequence(seq.syms, seq.callback); +} + +void SeqMatcher::addSequence(const std::vector &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(current->v)) { + throw std::logic_error("Overlapping key sequences (second is longer)"); + } else { + if (!std::holds_alternative(current->v)) { + current->v.emplace(); + } + NodeMap &map = std::get(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()).first->second.get(); + } + } + } + + if (auto *map = std::get_if(¤t->v)) { + if (!map->empty()) { + throw std::logic_error("Overlapping key sequences (second is shorter)"); + } + } + if (std::holds_alternative(current->v)) { + throw std::logic_error("Overlapping key sequences (equally long)"); + } + current->v.emplace(callback); +} + +std::optional SeqMatcher::observe(const XKeyEvent &ev) { + auto *map = std::get_if(&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(&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 +#include +#include +#include +#include +#include "xutil.h" + + +class SeqMatcher { +public: + using Callback = std::function; + + struct SymSequence { + std::vector syms; + Callback callback; + }; + + SeqMatcher(Display *dpy); + + // Initialise with an initial list of sequences. + SeqMatcher(Display *dpy, std::vector seqs); + + // Register a key sequence + void addSequence(const std::vector &syms, Callback callback); + + // Returns bel()-running callback if unrecognised sequence is given + std::optional observe(const XKeyEvent &ev); + + // Cancel any running sequence and restart from the beginning + void reset(); + +private: + struct Node; + using NodeMap = std::unordered_map>; + struct Node { + std::variant 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 +#include +#include "sqliteutil.h" + + +namespace sqlite { + std::vector> parseCSV(std::string output) { + std::istringstream ss{output}; + std::vector> 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 &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 +#include + + +namespace sqlite { + // Parse sqlite's CSV output, producing the query's result table as strings + std::vector> 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 +#include +#include +#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>; + + 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 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 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 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 +#include + + +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 + 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; + }; + + // 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> 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> XGrabKeyboardRAII(Display *dpy, Window win); + + // Open the specified display (pass nullptr to use $DISPLAY). Drop the UponExit to close. + std::pair>> 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 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 callback); + +} + +template <> +struct std::hash { + size_t operator()(x::Keycode code) const { + return std::hash{}((unsigned int)code); + } +}; + +template <> +struct std::hash { + size_t operator()(x::Keysym sym) const { + return std::hash{}((unsigned int)sym); + } +}; + -- cgit v1.2.3-70-g09d2