From 06950aab233de6a2f47293d59575bb42f6131660 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Wed, 27 Jan 2021 22:06:49 +0100 Subject: Complete rewrite using C++ and with shared state support --- src/timer.cc | 555 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 555 insertions(+) create mode 100644 src/timer.cc (limited to 'src/timer.cc') 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 +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#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 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(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 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 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 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 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(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(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(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(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(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(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 + void post(Args&&... args) { + bool notify; + { + std::lock_guard lock(action_lock_); + notify = action_.empty(); + action_.emplace_back(std::forward(args)...); + } + if (notify) + action_cond_.notify_one(); + } + + void update_text() { + char tmp[50]; + int len; + + using hours = std::chrono::duration>; + + if (active_) { + auto diff = std::chrono::duration_cast( + 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 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 string_atom_; + std::optional wm_protocols_; + std::optional wm_delete_window_; + std::optional background_inactive_; + std::optional background_active_; + std::optional foreground_inactive_; + std::optional foreground_active_; + std::unique_ptr 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 epoch_; + + std::vector 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 action_; + + std::filesystem::path const state_file_; + std::unique_ptr 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 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 " << 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()) + ? EXIT_SUCCESS : EXIT_FAILURE; +} -- cgit v1.2.3-70-g09d2