#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "command.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 << "GUI for GOT (https://github.com/lieuwex/got), which is a rewrite of Timetrap.\n" "This thing runs as a daemon; it gets activated with the Pause/Break key on\n" "your keyboard. The following sequences trigger actions:\n" " E -- Edit the note for the currently running activity\n" " I -- Check into a sheet\n" " O -- Check out of the current activity\n" " -- Switch and check into the 'misc' sheet\n" " Q -- Quit this program (killing it also works of course)\n" << std::flush; return 0; } if (!std::filesystem::exists(got::getDBpath())) { std::cerr << "The GOT database (" << got::getDBpath() << ") doesn't exist!" << std::endl; std::cerr << "Please run 'got' at least once before using this tool." << std::endl; return 1; } auto dpy_pair = XOpenDisplayRAII(nullptr); Display *dpy = dpy_pair.first; bool quitRequested = false; SeqMatcher matcher{dpy}; matcher.addSequence({XK_E}, []() { if (got::getRunning()) { if (auto descr = gui::promptText("Edit currently running entry's text:")) { got::editRunning(*descr); } } else { gui::showNotification("Cannot edit if not checked in"); } }); matcher.addSequence({XK_I}, []() { std::vector sheets = got::getSheets(); auto choice = gui::chooseList("Check in", "Sheet", sheets); if (choice) { auto current = got::getRunning(); if (current) { got::checkOut(); gui::showNotification("Checked out of sheet '" + current->first + "'"); } got::checkIn(*choice); gui::showNotification("Checked in to sheet '" + *choice + "'"); } }); matcher.addSequence({XK_O}, []() { auto current = got::getRunning(); if (current) { got::checkOut(); gui::showNotification("Checked out of sheet '" + current->first + "'"); } else { gui::showNotification("Cannot check out if not checked in"); } }); matcher.addSequence({XK_space}, []() { if (got::getRunning()) got::checkOut(); got::checkIn("misc"); gui::showNotification("Switched to 'misc'"); }); matcher.addSequence({XK_Break}, []() { auto current = got::getRunning(); if (!current) { gui::showNotification("Currently checked out"); } else { gui::showNotification( "Checked into '" + current->first + "'" + (current->second.empty() ? current->second : " (" + current->second + ")") ); } }); matcher.addSequence({XK_Q}, [&quitRequested]() { gui::showNotification("GOT GUI is quitting"); quitRequested = true; }); globalKeyWatch( dpy, XK_Break, [dpy, &matcher, &quitRequested](const XKeyEvent&) -> bool { matcher.reset(); SeqMatcher::Callback cb; globalKeyboardGrab(dpy, [&matcher, &cb](const XKeyEvent &ev) -> bool { auto opt_cb = matcher.observe(ev); if (opt_cb) { cb = *opt_cb; return true; } else { return false; } }); if (cb) cb(); return quitRequested; } ); }