diff options
Diffstat (limited to 'main.cpp')
-rw-r--r-- | main.cpp | 236 |
1 files changed, 221 insertions, 15 deletions
@@ -1,16 +1,29 @@ #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" +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} {} @@ -73,6 +86,7 @@ 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); }}; } @@ -80,10 +94,12 @@ 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); }}; } @@ -185,30 +201,28 @@ public: current->v.emplace<Callback>(callback); } - // Returns a sequence was completed, either successfully or erroneously. - // Needs more events to complete a sequence iff it returns false. - bool observe(const XKeyEvent &ev) { + // 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 - bel(dpy); reset(); - return true; + return [dpy = dpy]() { bel(dpy); }; } curNode = it->second.get(); if (auto *cb = std::get_if<Callback>(&curNode->v)) { - (*cb)(); + // Sequence completed reset(); - return true; + return *cb; } // Need more keys - return false; + return std::nullopt; } void reset() { @@ -227,20 +241,200 @@ private: Node *curNode = &rootNode; }; -int main() { +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 << + "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" + " <break> E -- Edit the note for the currently running activity\n" + " <break> I -- Check into a sheet\n" + " <break> O -- Check out of the current activity\n" + " <break> <space> -- Switch and check into the 'misc' sheet\n" + " <break> 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_A, XK_B}, []() { - std::cout << "key ab" << std::endl; + + 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_B}, []() { - std::cout << "key b" << std::endl; + + matcher.addSequence({XK_I}, []() { + std::vector<std::string> 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; }); @@ -248,9 +442,21 @@ int main() { dpy, XK_Break, [dpy, &matcher, &quitRequested](const XKeyEvent&) -> bool { matcher.reset(); - globalKeyboardGrab(dpy, [&matcher](const XKeyEvent &ev) -> bool { - return matcher.observe(ev); + + 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; } ); |