summaryrefslogtreecommitdiff
path: root/src/timer.cc
diff options
context:
space:
mode:
authorJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
committerJoel Klinghed <the_jk@spawned.biz>2021-01-27 22:06:49 +0100
commit06950aab233de6a2f47293d59575bb42f6131660 (patch)
tree62f6eed4a6d35414f656d22b9ac7420849018a11 /src/timer.cc
parent1ef9c463f1efc1adfb62e42ab3dd17e8c6394373 (diff)
Complete rewrite using C++ and with shared state support
Diffstat (limited to 'src/timer.cc')
-rw-r--r--src/timer.cc555
1 files changed, 555 insertions, 0 deletions
diff --git a/src/timer.cc b/src/timer.cc
new file mode 100644
index 0000000..8c97d32
--- /dev/null
+++ b/src/timer.cc
@@ -0,0 +1,555 @@
+#include "common.hh"
+
+#include "args.hh"
+#include "timer_state.hh"
+#include "xcb_atoms.hh"
+#include "xcb_colors.hh"
+#include "xcb_connection.hh"
+#include "xcb_event.hh"
+#include "xcb_resource.hh"
+#include "xcb_resources.hh"
+#include "xcb_xkb.hh"
+#include "xdg.hh"
+
+#include <algorithm>
+#include <condition_variable>
+#include <deque>
+#include <errno.h>
+#include <iostream>
+#include <limits>
+#include <mutex>
+#include <optional>
+#include <string.h>
+#include <thread>
+#include <xcb/xcb_icccm.h>
+
+#ifndef VERSION
+# warning No version defined
+# define VERSION
+#endif
+
+namespace {
+
+constexpr char const kTitle[] = "Timer";
+constexpr char const kClass[] = "org.the_jk.timer";
+
+constexpr std::chrono::seconds const kRedrawInterval{36};
+
+class Timer : public TimerState::Delegate {
+public:
+ explicit Timer(std::filesystem::path state_file)
+ : state_file_(std::move(state_file)),
+ state_(TimerState::create(state_file_, this)) {}
+
+ bool good() const {
+ return state_ != nullptr;
+ }
+
+ bool run(Option const* display, std::optional<std::string> font_name) {
+ int screen_index = 0;
+ if (display->is_set()) {
+ conn_ = xcb::make_shared_conn(xcb_connect(display->arg().c_str(),
+ &screen_index));
+ } else {
+ conn_ = xcb::make_shared_conn(xcb_connect(nullptr, &screen_index));
+ }
+
+ {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "Unable to connect to X display: " << err << std::endl;
+ return false;
+ }
+ }
+
+ auto atoms = xcb::Atoms::create(conn_);
+ string_atom_ = atoms->get("STRING");
+ wm_protocols_ = atoms->get("WM_PROTOCOLS");
+ wm_delete_window_ = atoms->get("WM_DELETE_WINDOW");
+
+ auto* screen = xcb::get_screen(conn_.get(), screen_index);
+ assert(screen);
+
+ auto colors = xcb::Colors::create(conn_, screen->default_colormap);
+ background_inactive_ =
+ colors->get_with_fallback(0x80, 0x80, 0x80, screen->white_pixel);
+ foreground_inactive_ =
+ colors->get_with_fallback(0x00, 0x00, 0x00, screen->black_pixel);
+ background_active_ =
+ colors->get_with_fallback(0x80, 0xff, 0x80, screen->white_pixel);
+ foreground_active_ =
+ colors->get_with_fallback(0x00, 0x00, 0x00, screen->black_pixel);
+
+ keyboard_ = xcb::Keyboard::create(conn_.get());
+ if (!keyboard_) {
+ std::cerr << "Failed to initialize XKB." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ wnd_ = xcb::make_unique_wnd(conn_);
+ gc_ = xcb::make_unique_gc(conn_);
+ font_ = xcb::make_unique_font(conn_);
+
+ if (!font_name) {
+ auto resources = xcb::Resources::create(conn_);
+ font_name = resources->get_string("timer.font", "");
+ if (!font_name) {
+ font_name = "fixed";
+ }
+ }
+
+ auto font_cookie = xcb_open_font_checked(conn_.get(), font_->id(),
+ font_name->size(),
+ font_name->data());
+
+ if (!atoms->sync()) {
+ std::cerr << "Failed to get X atoms." << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (!colors->sync()) {
+ std::cerr << "Failed to get X colors." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ uint32_t value_list[3];
+ uint32_t value_mask = 0;
+ value_mask |= XCB_CW_BACK_PIXEL;
+ value_list[0] = screen->black_pixel;
+ value_mask |= XCB_CW_EVENT_MASK;
+ value_list[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_KEY_PRESS |
+ XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_STRUCTURE_NOTIFY;
+ xcb_create_window(conn_.get(), XCB_COPY_FROM_PARENT, wnd_->id(),
+ screen->root, 0, 0, wnd_width_, wnd_height_, 0,
+ XCB_WINDOW_CLASS_INPUT_OUTPUT,
+ screen->root_visual, value_mask, value_list);
+
+ xcb_icccm_set_wm_name(conn_.get(), wnd_->id(), string_atom_->get(),
+ 8, sizeof(kTitle) - 1, kTitle);
+ xcb_icccm_set_wm_class(conn_.get(), wnd_->id(), sizeof(kClass) - 1, kClass);
+ xcb_atom_t atom_list[1];
+ atom_list[0] = wm_delete_window_->get();
+ xcb_icccm_set_wm_protocols(conn_.get(), wnd_->id(),
+ wm_protocols_->get(), 1, atom_list);
+
+ value_mask = XCB_GC_FONT;
+ value_list[0] = font_->id();
+ xcb_create_gc(conn_.get(), gc_->id(), wnd_->id(), value_mask, value_list);
+
+ {
+ xcb::generic_error error(xcb_request_check(conn_.get(), font_cookie));
+ if (error) {
+ std::cerr << "Failed to load font " << *font_name << ": "
+ << static_cast<int>(error->error_code) << std::endl;
+ return false;
+ }
+ }
+
+ update_text();
+
+ xcb_map_window(conn_.get(), wnd_->id());
+ xcb_flush(conn_.get());
+
+ std::thread xcb_thread(&Timer::run_xcb, this);
+
+ bool quit = false;
+ std::chrono::steady_clock::time_point next_redraw;
+ if (active_) {
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ }
+ while (!quit) {
+ ActionData action;
+ {
+ std::unique_lock<std::mutex> lock(action_lock_);
+ while (action_.empty()) {
+ if (active_) {
+ if (action_cond_.wait_until(lock, next_redraw)
+ == std::cv_status::timeout) {
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ update_text();
+ draw();
+ }
+ } else {
+ action_cond_.wait(lock);
+ }
+ }
+ action = action_.front();
+ action_.pop_front();
+ }
+
+ switch (action.action) {
+ case Action::QUIT:
+ quit = true;
+ break;
+ case Action::DRAW:
+ if (action.b) {
+ draw();
+ }
+ break;
+ case Action::RESIZE:
+ wnd_width_ = action.rect.width;
+ wnd_height_ = action.rect.height;
+ break;
+ case Action::TOGGLE:
+ if (active_) {
+ state_->stop();
+ } else {
+ state_->start();
+ }
+ break;
+ case Action::SEND_RESET:
+ state_->reset();
+ break;
+ case Action::RECV_RESET:
+ total_ = std::chrono::minutes::zero();
+ update_text();
+ draw();
+ break;
+ case Action::START:
+ active_ = true;
+ next_redraw = std::chrono::steady_clock::now() + kRedrawInterval;
+ total_ = action.duration;
+ epoch_ = action.point;
+ update_text();
+ draw();
+ break;
+ case Action::STOP:
+ active_ = false;
+ total_ = action.duration;
+ update_text();
+ draw();
+ break;
+ case Action::RESTART:
+ state_ = TimerState::create(state_file_, this);
+ if (!state_) {
+ std::cerr << "Timer state failed and unable to restart." << std::endl;
+ quit = true;
+ conn_.reset();
+ break;
+ }
+ }
+ }
+
+ xcb_thread.join();
+ return true;
+ }
+
+ void start(
+ std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch) override {
+ post(Action::START, total, epoch);
+ }
+
+ void stop(std::chrono::minutes total) override {
+ post(Action::STOP, total);
+ }
+
+ void reset() override {
+ post(Action::RECV_RESET);
+ }
+
+ void restart() override {
+ post(Action::RESTART);
+ }
+
+private:
+ enum class Action {
+ QUIT,
+ DRAW,
+ RESIZE,
+ TOGGLE,
+ START,
+ STOP,
+ SEND_RESET,
+ RECV_RESET,
+ RESTART,
+ };
+
+ struct ActionData {
+ Action action;
+ xcb_rectangle_t rect;
+ bool b{false};
+ std::chrono::minutes duration;
+ std::chrono::time_point<std::chrono::system_clock> point;
+
+ ActionData() = default;
+
+ explicit ActionData(Action action)
+ : action(action) {
+ assert(action == Action::QUIT || action == Action::TOGGLE ||
+ action == Action::SEND_RESET || action == Action::RECV_RESET ||
+ action == Action::RESTART);
+ }
+
+ ActionData(Action action, xcb_rectangle_t rect, bool last)
+ : action(action), rect(rect), b(last) {
+ assert(action == Action::DRAW);
+ }
+
+ ActionData(Action action, uint16_t width, uint16_t height)
+ : action(action), rect({0, 0, width, height}) {
+ assert(action == Action::RESIZE);
+ }
+
+ ActionData(Action action, std::chrono::minutes total,
+ std::chrono::time_point<std::chrono::system_clock> epoch)
+ : action(action), duration(total), point(epoch) {
+ assert(action == Action::START);
+ }
+
+ ActionData(Action action, std::chrono::minutes total)
+ : action(action), duration(total) {
+ assert(action == Action::STOP);
+ }
+ };
+
+ void run_xcb() {
+ while (true) {
+ xcb::generic_event event(xcb_wait_for_event(conn_.get()));
+ if (!event) {
+ auto err = xcb_connection_has_error(conn_.get());
+ if (err) {
+ std::cerr << "X connection had fatal error: " << err << std::endl;
+ } else {
+ std::cerr << "X connection had fatal I/O error." << std::endl;
+ }
+ break;
+ }
+ auto response_type = XCB_EVENT_RESPONSE_TYPE(event.get());
+ if (response_type == XCB_EXPOSE) {
+ auto* e = reinterpret_cast<xcb_expose_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ xcb_rectangle_t rect;
+ rect.x = e->x;
+ rect.y = e->y;
+ rect.width = e->width;
+ rect.height = e->height;
+ post(Action::DRAW, rect, e->count == 0);
+ }
+ continue;
+ } else if (response_type == XCB_KEY_PRESS) {
+ auto* e = reinterpret_cast<xcb_key_press_event_t*>(event.get());
+ if (e->event == wnd_->id()) {
+ auto str = keyboard_->get_utf8(e);
+ if (str == "q" || str == "\x1b" /* Escape */) {
+ break;
+ } else if (str == " ") {
+ post(Action::TOGGLE);
+ } else if ((e->state & XCB_MOD_MASK_CONTROL) &&
+ str == "\x12" /* Ctrl + R */) {
+ post(Action::SEND_RESET);
+ }
+ }
+ continue;
+ } else if (response_type == XCB_BUTTON_PRESS) {
+ auto* e = reinterpret_cast<xcb_button_press_event_t*>(event.get());
+ if (e->event == wnd_->id()) {
+ if (e->detail == 1 /* Left button */) {
+ post(Action::TOGGLE);
+ }
+ }
+ continue;
+ } else if (response_type == XCB_CONFIGURE_NOTIFY) {
+ auto* e = reinterpret_cast<xcb_configure_notify_event_t*>(event.get());
+ if (e->window == wnd_->id()) {
+ post(Action::RESIZE, e->width, e->height);
+ }
+ continue;
+ } else if (response_type == XCB_REPARENT_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ } else if (response_type == XCB_MAP_NOTIFY) {
+ // Ignored, part of XCB_EVENT_MASK_STRUCTURE_NOTIFY
+ continue;
+ } else if (keyboard_->handle_event(conn_.get(), event.get())) {
+ continue;
+ } else if (response_type == XCB_CLIENT_MESSAGE) {
+ auto* e = reinterpret_cast<xcb_client_message_event_t*>(event.get());
+ if (e->window == wnd_->id() && e->type == wm_protocols_->get() &&
+ e->format == 32) {
+ if (e->data.data32[0] == wm_delete_window_->get()) {
+ break;
+ }
+ }
+ continue;
+ }
+
+#ifndef NDEBUG
+ if (response_type == 0) {
+ auto* e = reinterpret_cast<xcb_generic_error_t*>(event.get());
+ std::cout << "Unhandled error: "
+ << xcb_event_get_error_label(e->error_code) << std::endl;
+ } else {
+ std::cout << "Unhandled event: " << xcb_event_get_label(response_type)
+ << std::endl;
+ }
+#endif
+ }
+
+ post(Action::QUIT);
+ }
+
+ template<class... Args>
+ void post(Args&&... args) {
+ bool notify;
+ {
+ std::lock_guard<std::mutex> lock(action_lock_);
+ notify = action_.empty();
+ action_.emplace_back(std::forward<Args>(args)...);
+ }
+ if (notify)
+ action_cond_.notify_one();
+ }
+
+ void update_text() {
+ char tmp[50];
+ int len;
+
+ using hours = std::chrono::duration<float, std::ratio<60 * 60>>;
+
+ if (active_) {
+ auto diff = std::chrono::duration_cast<std::chrono::minutes>(
+ std::chrono::system_clock::now() - epoch_);
+
+ len = snprintf(tmp, sizeof(tmp), "%.2f (%.2f)", hours(diff).count(),
+ hours(total_ + diff).count());
+ } else {
+ len = snprintf(tmp, sizeof(tmp), "(%.2f)", hours(total_).count());
+ }
+ if (len < 0 || len == sizeof(tmp))
+ return;
+
+ text_.resize(len);
+ for (int i = 0; i < len; ++i) {
+ text_[i].byte1 = 0;
+ text_[i].byte2 = tmp[i];
+ }
+
+ text_extents_cookie_ = xcb_query_text_extents(conn_.get(), font_->id(),
+ text_.size(), text_.data());
+ text_extents_width_ = 0;
+ }
+
+ void draw() {
+ xcb_rectangle_t r{0, 0, wnd_width_, wnd_height_};
+
+ uint32_t values[2];
+ values[0] =
+ active_ ? background_active_->get() : background_inactive_->get();
+ xcb_change_gc(conn_.get(), gc_->id(), XCB_GC_FOREGROUND, values);
+ xcb_poly_fill_rectangle(conn_.get(), wnd_->id(), gc_->id(), 1, &r);
+
+ values[0] =
+ active_ ? foreground_active_->get() : foreground_inactive_->get();
+ values[1] =
+ active_ ? background_active_->get() : background_inactive_->get();
+ xcb_change_gc(conn_.get(), gc_->id(),
+ XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values);
+
+ if (text_extents_width_ == 0) {
+ xcb::reply<xcb_query_text_extents_reply_t> reply(
+ xcb_query_text_extents_reply(conn_.get(), text_extents_cookie_,
+ nullptr));
+ if (reply) {
+ text_extents_width_ = reply->overall_width;
+ text_extents_height_ = reply->font_ascent;
+ } else {
+ text_extents_width_ = 1;
+ text_extents_height_ = 0;
+ }
+ }
+
+ auto x = (text_extents_width_ < wnd_width_) ?
+ (wnd_width_ - text_extents_width_) / 2 : 0;
+ auto y = wnd_height_ / 2 + text_extents_height_ / 2;
+
+ xcb_image_text_16(conn_.get(), text_.size(), wnd_->id(), gc_->id(), x, y,
+ text_.data());
+
+ xcb_flush(conn_.get());
+ }
+
+ xcb::shared_conn conn_;
+ std::optional<xcb::Atoms::Reference> string_atom_;
+ std::optional<xcb::Atoms::Reference> wm_protocols_;
+ std::optional<xcb::Atoms::Reference> wm_delete_window_;
+ std::optional<xcb::Colors::Color> background_inactive_;
+ std::optional<xcb::Colors::Color> background_active_;
+ std::optional<xcb::Colors::Color> foreground_inactive_;
+ std::optional<xcb::Colors::Color> foreground_active_;
+ std::unique_ptr<xcb::Keyboard> keyboard_;
+ xcb::unique_wnd wnd_;
+ xcb::unique_gc gc_;
+ xcb::unique_font font_;
+ uint16_t wnd_width_{100};
+ uint16_t wnd_height_{50};
+
+ bool active_{false};
+ std::chrono::minutes total_{0};
+ std::chrono::time_point<std::chrono::system_clock> epoch_;
+
+ std::vector<xcb_char2b_t> text_;
+ xcb_query_text_extents_cookie_t text_extents_cookie_;
+ uint16_t text_extents_width_{0};
+ uint16_t text_extents_height_{0};
+
+ std::mutex action_lock_;
+ std::condition_variable action_cond_;
+ std::deque<ActionData> action_;
+
+ std::filesystem::path const state_file_;
+ std::unique_ptr<TimerState> state_;
+};
+
+} // namespace
+
+int main(int argc, char** argv) {
+ auto args = Args::create();
+ auto* help = args->add_option('h', "help", "display this text and exit.");
+ auto* version = args->add_option('V', "version", "display version and exit.");
+ auto* opt_state_file = args->add_option_with_arg(
+ 'S', "state", "load state from FILE instead of default.", "FILE");
+ auto* opt_font_name = args->add_option_with_arg(
+ 'F', "font", "use font named FONT instead of default or Xresources.",
+ "FONT");
+ auto* display = args->add_option_with_arg(
+ 'D', "display", "connect to DISPLAY instead of default.", "DISPLAY");
+ std::vector<std::string> arguments;
+ if (!args->run(argc, argv, "timer", std::cerr, &arguments)) {
+ std::cerr << "Try `timer --help` for usage." << std::endl;
+ return EXIT_FAILURE;
+ }
+ if (help->is_set()) {
+ std::cout << "Usage: `timer [OPTIONS]`\n"
+ << "Timer is a timekeeping tool.\n"
+ << "\n";
+ args->print_descriptions(std::cout, 80);
+ return EXIT_SUCCESS;
+ }
+ if (version->is_set()) {
+ std::cout << "Timer " VERSION " written by "
+ << "Joel Klinghed <the_jk@spawned.biz>" << std::endl;
+ std::cout << "Icon by Free Preloaders [https://freeicons.io/profile/726]"
+ << " on https://freeicons.io" << std::endl;
+ return EXIT_SUCCESS;
+ }
+ if (!arguments.empty()) {
+ std::cerr << "Unexpected arguments after options.\n"
+ << "Try `timer --help` for usage." << std::endl;
+ return EXIT_FAILURE;
+ }
+
+ std::filesystem::path state_file;
+ if (opt_state_file->is_set()) {
+ state_file = opt_state_file->arg();
+ } else {
+ state_file = xdg::path_to_write(xdg::Type::DATA, "timer.state");
+ }
+
+ Timer timer(state_file);
+ if (!timer.good())
+ return EXIT_FAILURE;
+ return timer.run(display,
+ opt_font_name->is_set() ? opt_font_name->arg() :
+ std::optional<std::string>())
+ ? EXIT_SUCCESS : EXIT_FAILURE;
+}