From 36885e794a758d84ab75a905385dca0980c5e902 Mon Sep 17 00:00:00 2001 From: Joel Klinghed Date: Fri, 11 Jan 2013 00:30:48 +0100 Subject: Initial commit --- src/main.c | 893 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 893 insertions(+) create mode 100644 src/main.c (limited to 'src/main.c') diff --git a/src/main.c b/src/main.c new file mode 100644 index 0000000..6c30fec --- /dev/null +++ b/src/main.c @@ -0,0 +1,893 @@ +#include "common.h" + +#include +#include +#include +#include + +#include +#include +#include +#if HAVE_XCB_ICCCM +# include +#endif + +#include "compiler.h" +#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); + } +#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; +} -- cgit v1.2.3-70-g09d2