From 6ac952edd3fda6ee4b52ec917e205396eb8e961e Mon Sep 17 00:00:00 2001 From: Tom Smeding Date: Sat, 14 Mar 2020 18:41:32 +0900 Subject: Make it work reasonably --- command.cpp | 82 +++++++++++++++++++++ command.h | 9 +++ main.cpp | 236 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- 3 files changed, 312 insertions(+), 15 deletions(-) create mode 100644 command.cpp create mode 100644 command.h diff --git a/command.cpp b/command.cpp new file mode 100644 index 0000000..7d62163 --- /dev/null +++ b/command.cpp @@ -0,0 +1,82 @@ +#include +#include +#include +#include +#include +#include +#include "command.h" + + +std::pair readCommand(const std::vector &args) { + int pipefd[2]; + if (pipe(pipefd) < 0) { + throw std::runtime_error("readCommand: pipe: " + std::string{strerror(errno)}); + } + + pid_t pid = fork(); + if (pid < 0) { + throw std::runtime_error("readCommand: fork: " + std::string{strerror(errno)}); + } + + if (pid == 0) { + close(STDOUT_FILENO); + dup2(pipefd[1], STDOUT_FILENO); + close(pipefd[0]); + close(pipefd[1]); + + std::vector argv(args.size() + 1); + for (size_t i = 0; i < args.size(); i++) { + argv[i] = new char[args[i].size() + 1]; + memcpy(argv[i], args[i].data(), args[i].size() + 1); + } + argv[args.size()] = nullptr; + + execvp(argv[0], argv.data()); + perror("execvp"); + exit(255); + } + + close(pipefd[1]); + + std::string output; + while (true) { + size_t base = output.size(); + output.resize(base + 4096); + ssize_t nr = read(pipefd[0], output.data() + base, 4096); + if (nr < 0) { + if (errno == EINTR) { + output.resize(base); + continue; + } + kill(pid, SIGTERM); + throw std::runtime_error("readCommand: read: " + std::string{strerror(errno)}); + } + output.resize(base + nr); + if (nr == 0) break; + } + + while (true) { + int status; + pid_t wpid = waitpid(pid, &status, 0); + if (wpid < 0) { + if (errno == EINTR) continue; + kill(pid, SIGTERM); + throw std::runtime_error("readCommand: wait: " + std::string{strerror(errno)}); + } + if (WIFEXITED(status)) { + return std::make_pair(WEXITSTATUS(status), output); + } + } +} + +std::string runCommand(const std::vector &args) { + auto result = readCommand(args); + if (result.first != 0) { + std::string cmdline; + for (const std::string &a : args) cmdline += " " + a; + throw std::runtime_error( + "Command exited with exit code " + std::to_string(result.first) + ":" + cmdline + ); + } + return result.second; +} diff --git a/command.h b/command.h new file mode 100644 index 0000000..7166eb6 --- /dev/null +++ b/command.h @@ -0,0 +1,9 @@ +#pragma once + +#include +#include +#include + + +std::string runCommand(const std::vector &args); +std::pair readCommand(const std::vector &args); diff --git a/main.cpp b/main.cpp index 1db9b62..45eef83 100644 --- a/main.cpp +++ b/main.cpp @@ -1,16 +1,29 @@ #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} {} @@ -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); } - // 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 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 - bel(dpy); reset(); - return true; + return [dpy = dpy]() { bel(dpy); }; } curNode = it->second.get(); if (auto *cb = std::get_if(&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> 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_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 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; } ); -- cgit v1.2.3