#include "common.h" #include #include #include #include #include #include #include #if HAVE_XCB_ICCCM # include #endif #define HAVE_STRUCT_TIMESPEC 1 #include "paths.h" #include "safe_fifo.h" #include "thread.h" #if HAVE_XCB_ICCCM typedef struct atoms_t { xcb_atom_t WM_DELETE_WINDOW; xcb_atom_t WM_PROTOCOLS; } atoms_t; #endif typedef enum message_type_t { MSG_QUIT, MSG_DRAW, MSG_SWAP, MSG_CLEAR } message_type_t; typedef struct message_t { message_type_t type; } message_t; static message_t quit_message = { MSG_QUIT }; static message_t draw_message = { MSG_DRAW }; static message_t swap_message = { MSG_SWAP }; static message_t clear_message = { MSG_CLEAR }; typedef struct time_target_t time_target_t; typedef bool (* timer_callback_t)(void *data); struct time_target_t { time_target_t *next; struct timespec target; unsigned long interval_ms; void *data; timer_callback_t callback; }; typedef struct main_t { xcb_connection_t *conn; xcb_window_t wnd; xcb_gcontext_t gc; xcb_font_t font; #if HAVE_XCB_ICCCM atoms_t atoms; #endif safe_fifo_t *queue; const char *state; paths_t *paths; time_target_t *first_timer; bool save_queued; bool redraw_queued; unsigned int width, height; uint32_t background[2], foreground[2]; /* State */ bool working; time_t last_time; unsigned long total_min; } main_t; static const char DEFAULT_STATE[] = "timer.state"; static const char FONT_NAME[] = "7x13"; static const unsigned long REDRAW_INTERVAL_MS = 36 * 1000; static bool handle_arguments(main_t *m, int argc, char **argv, int *exitcode); static void draw(main_t *m); static void *run_xcb(void *arg); static bool load_state(main_t *m); static bool save_state(main_t *m); static void default_state(main_t *m); static void swap(main_t *m); static void clear(main_t *m); static void queue_save(main_t *m); static void add_timer(main_t *m, unsigned long interval_ms, timer_callback_t callback, void *data); static void insert_timer(main_t *m, time_target_t *target); static void run_timer(main_t *m); static bool redraw(void *arg); static int timeval_cmp(const struct timespec* x, const struct timespec* y); int main(int argc, char **argv) { main_t m; xcb_screen_iterator_t iter; xcb_screen_t *screen; int screen_index; uint32_t mask, values[2]; xcb_void_cookie_t cookie[10]; size_t cookies = 0; int exitcode; bool quit = false; memset(&m, 0, sizeof(m)); if (!handle_arguments(&m, argc, argv, &exitcode)) { return exitcode; } m.paths = paths_new(); if (!m.paths) { fputs("Unable to allocate memory.\n", stderr); return EXIT_FAILURE; } if (!load_state(&m)) { fputs("Error loading old state.\n", stderr); paths_unref(m.paths); return EXIT_FAILURE; } m.conn = xcb_connect(NULL, &screen_index); if (!m.conn) { fputs("Unable to connect to X11 display.\n", stderr); paths_unref(m.paths); return EXIT_FAILURE; } #if HAVE_XCB_ICCCM { xcb_intern_atom_cookie_t cookie[2]; xcb_intern_atom_reply_t *reply; xcb_generic_error_t *err; cookie[0] = xcb_intern_atom(m.conn, 0, strlen("WM_DELETE_WINDOW"), "WM_DELETE_WINDOW");; cookie[1] = xcb_intern_atom(m.conn, 0, strlen("WM_PROTOCOLS"), "WM_PROTOCOLS");; reply = xcb_intern_atom_reply(m.conn, cookie[0], &err); if (!reply) { fprintf(stderr, "ICCCM init atoms failed\n"); xcb_disconnect(m.conn); paths_unref(m.paths); return EXIT_FAILURE; } m.atoms.WM_DELETE_WINDOW = reply->atom; free(reply); reply = xcb_intern_atom_reply(m.conn, cookie[1], &err); if (!reply) { fprintf(stderr, "ICCCM init atoms failed\n"); xcb_disconnect(m.conn); paths_unref(m.paths); return EXIT_FAILURE; } m.atoms.WM_PROTOCOLS = reply->atom; free(reply); } #endif iter = xcb_setup_roots_iterator(xcb_get_setup(m.conn)); while (screen_index-- > 0) { xcb_screen_next(&iter); } screen = iter.data; { xcb_alloc_color_cookie_t req[4]; xcb_alloc_color_reply_t* rep; /* background, not working */ req[0] = xcb_alloc_color(m.conn, screen->default_colormap, 0x8000, 0x8000, 0x8000); /* background, working */ req[1] = xcb_alloc_color(m.conn, screen->default_colormap, 0x8000, 0xffff, 0x8000); /* foreground, not working */ req[2] = xcb_alloc_color(m.conn, screen->default_colormap, 0, 0, 0); /* foreground, working */ req[3] = xcb_alloc_color(m.conn, screen->default_colormap, 0, 0, 0); rep = xcb_alloc_color_reply(m.conn, req[0], 0); m.background[0] = rep ? rep->pixel : screen->white_pixel; free(rep); rep = xcb_alloc_color_reply(m.conn, req[1], 0); m.background[1] = rep ? rep->pixel : screen->white_pixel; free(rep); rep = xcb_alloc_color_reply(m.conn, req[2], 0); m.foreground[0] = rep ? rep->pixel : screen->black_pixel; free(rep); rep = xcb_alloc_color_reply(m.conn, req[3], 0); m.foreground[1] = rep ? rep->pixel : screen->black_pixel; free(rep); } m.font = xcb_generate_id(m.conn); cookie[cookies++] = xcb_open_font_checked(m.conn, m.font, sizeof(FONT_NAME) / sizeof(FONT_NAME[0]), FONT_NAME); mask = XCB_CW_BACK_PIXEL | XCB_CW_EVENT_MASK; values[0] = screen->black_pixel; values[1] = XCB_EVENT_MASK_EXPOSURE | XCB_EVENT_MASK_BUTTON_PRESS | XCB_EVENT_MASK_KEY_RELEASE; m.width = 100; m.height = 50; m.wnd = xcb_generate_id(m.conn); cookie[cookies++] = xcb_create_window_checked(m.conn, XCB_COPY_FROM_PARENT, m.wnd, screen->root, 0, 0, m.width, m.height, 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, screen->root_visual, mask, values); m.gc = xcb_generate_id(m.conn); mask = XCB_GC_FONT; values[0] = m.font; cookie[cookies++] = xcb_create_gc_checked(m.conn, m.gc, m.wnd, mask, values); cookie[cookies++] = xcb_map_window_checked(m.conn, m.wnd); #if HAVE_XCB_ICCCM { xcb_atom_t protocols[1]; protocols[0] = m.atoms.WM_DELETE_WINDOW; xcb_icccm_set_wm_protocols(m.conn, m.wnd, m.atoms.WM_PROTOCOLS, 1, protocols); xcb_icccm_set_wm_name(m.conn, m.wnd, XCB_ATOM_STRING, 8, 5, "timer"); xcb_icccm_set_wm_class(m.conn, m.wnd, 8, "jk.timer"); } #endif while (cookies--) { xcb_generic_error_t *error = xcb_request_check(m.conn, cookie[cookies]); if (error) { fprintf(stderr, "Error: %d\n", error->error_code); xcb_disconnect(m.conn); paths_unref(m.paths); return EXIT_FAILURE; } } xcb_flush(m.conn); m.queue = safe_fifo_new(); thread_new(run_xcb, &m); if (m.working) { add_timer(&m, REDRAW_INTERVAL_MS, redraw, &m); m.redraw_queued = true; } while (!quit) { message_t *msg; if (m.first_timer) { struct timespec now; thread_abstime(&now, 0); if (timeval_cmp(&now, &m.first_timer->target) >= 0) { run_timer(&m); msg = safe_fifo_trypop(m.queue); } else { msg = safe_fifo_timedpop(m.queue, &m.first_timer->target); } } else { msg = safe_fifo_pop(m.queue); } if (msg) { switch (msg->type) { case MSG_QUIT: quit = true; break; case MSG_DRAW: draw(&m); break; case MSG_SWAP: swap(&m); break; case MSG_CLEAR: clear(&m); break; } } } if (m.save_queued) { save_state(&m); } xcb_close_font(m.conn, m.font); xcb_free_gc(m.conn, m.gc); xcb_destroy_window(m.conn, m.wnd); xcb_disconnect(m.conn); while (m.first_timer) { time_target_t *next = m.first_timer->next; free(m.first_timer); m.first_timer = next; } paths_unref(m.paths); safe_fifo_unref(m.queue); return EXIT_SUCCESS; } static void print_usage(void) { fputs("Usage: timer [OPTION]...\n", stdout); fputs("Timer is a timekeeping tool.\n", stdout); fputs("\n", stdout); fputs("Options:\n", stdout); fputs(" -state FILE load state from FILE instead of default\n", stdout); fputs(" -help display this information and exit\n", stdout); fputs(" -version display version and exit\n", stdout); } static void print_version(void) { fputs("timer version " VERSION " written by Joel Klinghed\n", stdout); } bool handle_arguments(main_t *m, int argc, char **argv, int *exitcode) { bool usage = false, version = false, error = false; int a; for (a = 1; a < argc; a++) { if (argv[a][0] == '-') { if (strcmp(argv[a] + 1, "help") == 0) { usage = true; break; } else if (strcmp(argv[a] + 1, "version") == 0) { version = true; break; } else if (strcmp(argv[a] + 1, "state") == 0) { if (++a == argc) { error = true; fputs("Option `state` expects an argument.\n", stderr); break; } if (m->state) { error = true; fputs("Option `state` given twice.\n", stderr); break; } m->state = argv[a]; } else { error = true; fprintf(stderr, "Unknown option: %s\n", argv[a] + 1); break; } } else { break; } } if (!error && a < argc) { fputs("Too many arguments given.\n", stderr); error = true; } if (usage) { print_usage(); *exitcode = error ? EXIT_FAILURE : EXIT_SUCCESS; return false; } if (error) { fputs("Try `timer -help` for usage.\n", stderr); *exitcode = EXIT_FAILURE; return false; } if (version) { print_version(); *exitcode = EXIT_SUCCESS; return false; } return true; } void draw(main_t *m) { xcb_rectangle_t r; xcb_char2b_t *tmp; uint32_t values[2]; char text[50]; int x, y, i, len; xcb_query_text_extents_cookie_t cookie; xcb_query_text_extents_reply_t *reply; if (m->working) { double diff = difftime(time(NULL), m->last_time); diff /= 60.0 * 60.0; len = snprintf(text, sizeof(text), "%.2f (%.2f)", diff, (double)m->total_min / 60.0 + diff); } else { len = snprintf(text, sizeof(text), "(%.2f)", (double)m->total_min / 60.0); } if (len < 0 || len == sizeof(text)) { return; } tmp = malloc(len * sizeof(xcb_char2b_t)); for (i = 0; i < len; i++) { tmp[i].byte1 = 0; tmp[i].byte2 = text[i]; } cookie = xcb_query_text_extents(m->conn, m->font, len, tmp); r.x = 0; r.y = 0; r.width = m->width; r.height = m->height; values[0] = m->background[m->working ? 1 : 0]; xcb_change_gc(m->conn, m->gc, XCB_GC_FOREGROUND, values); xcb_poly_fill_rectangle(m->conn, m->wnd, m->gc, 1, &r); values[0] = m->foreground[m->working ? 1 : 0]; values[1] = m->background[m->working ? 1 : 0]; xcb_change_gc(m->conn, m->gc, XCB_GC_FOREGROUND | XCB_GC_BACKGROUND, values); x = 0; y = m->height / 2; reply = xcb_query_text_extents_reply(m->conn, cookie, NULL); if (reply) { if (reply->overall_width < m->width) { x = (m->width - reply->overall_width) / 2; } else { x = 0; } y += reply->font_ascent / 2; free(reply); } xcb_image_text_16(m->conn, len, m->wnd, m->gc, x, y, tmp); xcb_flush(m->conn); free(tmp); } static void send_message(main_t *m, message_t* msg) { safe_fifo_push(m->queue, msg); } void *run_xcb(void *arg) { main_t *m = arg; xcb_key_symbols_t *syms; syms = xcb_key_symbols_alloc(m->conn); for (;;) { bool done = false; xcb_generic_event_t *event = xcb_wait_for_event(m->conn); if (!event || xcb_connection_has_error(m->conn)) { free(event); break; } switch (XCB_EVENT_RESPONSE_TYPE(event)) { case XCB_EXPOSE: { xcb_expose_event_t *e = (xcb_expose_event_t*)event; if (e->count == 0) { send_message(m, &draw_message); } break; } #if HAVE_XCB_ICCCM case XCB_CLIENT_MESSAGE: { xcb_client_message_event_t *e = (xcb_client_message_event_t*)event; if (e->type == m->atoms.WM_PROTOCOLS && e->format == 32 && e->data.data32[0] == m->atoms.WM_DELETE_WINDOW) { done = true; } break; } #endif case XCB_BUTTON_PRESS: { xcb_button_press_event_t *e = (xcb_button_press_event_t*)event; if (e->detail == 1) { send_message(m, &swap_message); } break; } case XCB_KEY_RELEASE: { xcb_key_press_event_t *e = (xcb_key_press_event_t*)event; xcb_keysym_t sym = xcb_key_press_lookup_keysym(syms, e, 0); if (sym == ' ') { send_message(m, &swap_message); } else if (sym == 'r' && (e->state & XCB_MOD_MASK_CONTROL)) { send_message(m, &clear_message); } break; } case XCB_MAPPING_NOTIFY: { xcb_mapping_notify_event_t *e = (xcb_mapping_notify_event_t*)event; xcb_refresh_keyboard_mapping(syms, e); break; } } free(event); if (done) { break; } } xcb_key_symbols_free(syms); send_message(m, &quit_message); return NULL; } bool load_state(main_t *m) { FILE *fh; char *tmp, *line; const char *fname; char buf[1024]; char *end; long l; unsigned long ul; struct tm tm; if (m->state) { fname = m->state; tmp = NULL; } else { const char *dir = paths_user_dir(m->paths, PATHS_DATA); size_t dlen = strlen(dir); tmp = malloc(dlen + 1 + sizeof(DEFAULT_STATE)); if (!tmp) { fputs("Out of memory\n", stderr); return false; } memcpy(tmp, dir, dlen); tmp[dlen] = '/'; memcpy(tmp + dlen + 1, DEFAULT_STATE, sizeof(DEFAULT_STATE)); fname = tmp; } fh = fopen(fname, "rb"); if (!fh) { if (errno == ENOENT) { default_state(m); free(tmp); return true; } fprintf(stderr, "Error loading state from %s: %s\n", fname, strerror(errno)); free(tmp); return false; } line = fgets(buf, sizeof(buf), fh); if (!line) { fprintf(stderr, "Error reading state from %s: %s\n", fname, strerror(errno)); free(tmp); return false; } fclose(fh); end = NULL; l = strtol(line, &end, 10); if ((l != -1 && l != 1) || !end || *end != '|') { fprintf(stderr, "Invalid data in state %s: %s\n", fname, strerror(errno)); free(tmp); return false; } line = end + 1; end = NULL; ul = strtoul(line, &end, 10); if (!end || *end != '|') { fprintf(stderr, "Invalid data in state %s: %s\n", fname, strerror(errno)); free(tmp); return false; } end = strptime(end + 1, "%Y-%m-%d %H:%M:%S", &tm); if (!end) { fprintf(stderr, "Invalid data in state %s: %s\n", fname, strerror(errno)); free(tmp); return false; } m->working = l == 1; m->last_time = timegm(&tm); m->total_min = ul; free(tmp); return true; } bool save_state(main_t *m) { paths_file_t *file = paths_write(m->paths, PATHS_DATA, m->state ? m->state : DEFAULT_STATE, PATHS_CREATE | PATHS_TRUNCATE, 0600); char buf[1024]; size_t len2; int pos, len; m->save_queued = false; if (!file) { fprintf(stderr, "Failed to save state: %s\n", strerror(errno)); return false; } len = snprintf(buf, sizeof(buf), "%d|%lu|", m->working ? 1 : -1, m->total_min); if (len < 0 || len == sizeof(buf)) { fprintf(stderr, "Failed to save state: %s\n", strerror(errno)); paths_file_abort(file); return false; } len2 = strftime(buf + len, sizeof(buf) - len, "%Y-%m-%d %H:%M:%S", gmtime(&m->last_time)); if (len2 == 0 || len2 == sizeof(buf) - len) { fprintf(stderr, "Failed to save state: %s\n", strerror(errno)); paths_file_abort(file); return false; } len += len2; pos = 0; while (pos < len) { ssize_t got = paths_file_write(file, buf + pos, len - pos); if (got <= 0) { fprintf(stderr, "Failed to save state: %s\n", strerror(errno)); paths_file_abort(file); return false; } pos += got; } if (paths_file_close(file)) { fprintf(stderr, "Failed to finish saving state: %s\n", strerror(errno)); paths_file_abort(file); return false; } return true; } void default_state(main_t *m) { m->working = false; m->last_time = time(NULL); m->total_min = 0; } void swap(main_t *m) { if (m->working) { double diff = difftime(time(NULL), m->last_time); m->total_min += round(diff / 60.0); m->working = false; } else { m->working = true; if (!m->redraw_queued) { add_timer(m, REDRAW_INTERVAL_MS, redraw, m); m->redraw_queued = true; } } m->last_time = time(NULL); draw(m); queue_save(m); } void clear(main_t *m) { if (m->working) { return; } default_state(m); draw(m); queue_save(m); } static bool do_save(void *arg) { main_t *m = arg; assert(m->save_queued); save_state(m); return false; } void queue_save(main_t *m) { if (m->save_queued) { return; } add_timer(m, 5000, do_save, m); m->save_queued = true; } void add_timer(main_t *m, unsigned long interval_ms, timer_callback_t callback, void *data) { time_target_t* tgt = calloc(1, sizeof(time_target_t)); assert(interval_ms > 0); tgt->interval_ms = interval_ms; tgt->callback = callback; tgt->data = data; thread_abstime(&tgt->target, interval_ms); insert_timer(m, tgt); } void insert_timer(main_t* m, time_target_t *target) { time_target_t *cur, *last; assert(target->next == NULL); if (!m->first_timer) { m->first_timer = target; return; } last = NULL; cur = m->first_timer; while (cur) { if (timeval_cmp(&cur->target, &target->target) > 0) { target->next = cur; if (last == NULL) { m->first_timer = target; return; } else { break; } } last = cur; cur = last->next; } last->next = target; } void run_timer(main_t *m) { time_target_t *target = m->first_timer; m->first_timer = target->next; target->next = NULL; if (target->callback(target->data)) { thread_abstime(&target->target, target->interval_ms); insert_timer(m, target); } else { free(target); } } bool redraw(void *arg) { main_t* m = arg; assert(m->redraw_queued); if (m->working) { draw(m); return true; } m->redraw_queued = false; return false; } int timeval_cmp(const struct timespec* x, const struct timespec* y) { assert(x && y); if (x->tv_sec < y->tv_sec) { return -1; } else if (x->tv_sec == y->tv_sec) { if (x->tv_nsec < y->tv_nsec) { return -1; } else if (x->tv_nsec == y->tv_nsec) { return 0; } } return 1; }