#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; }