summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorTom Smeding <tom.smeding@gmail.com>2020-03-14 10:41:32 +0100
committerTom Smeding <tom.smeding@gmail.com>2020-03-14 10:41:32 +0100
commit6ac952edd3fda6ee4b52ec917e205396eb8e961e (patch)
treec0f07b6625354fbeb40034fef4a964872c93c1df
parentc64fb2c0b28fc07f863c195746931d26a716f58d (diff)
Make it work reasonably
-rw-r--r--command.cpp82
-rw-r--r--command.h9
-rw-r--r--main.cpp236
3 files changed, 312 insertions, 15 deletions
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 <stdexcept>
+#include <cstring>
+#include <errno.h>
+#include <unistd.h>
+#include <signal.h>
+#include <sys/wait.h>
+#include "command.h"
+
+
+std::pair<int, std::string> readCommand(const std::vector<std::string> &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<char*> 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<std::string> &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 <vector>
+#include <string>
+#include <utility>
+
+
+std::string runCommand(const std::vector<std::string> &args);
+std::pair<int, std::string> readCommand(const std::vector<std::string> &args);
diff --git a/main.cpp b/main.cpp
index 1db9b62..45eef83 100644
--- a/main.cpp
+++ b/main.cpp
@@ -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;
}
);