summaryrefslogtreecommitdiff
path: root/src/main.c
diff options
context:
space:
mode:
Diffstat (limited to 'src/main.c')
-rw-r--r--src/main.c893
1 files changed, 893 insertions, 0 deletions
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 <errno.h>
+#include <math.h>
+#include <stdio.h>
+#include <string.h>
+
+#include <xcb/xcb.h>
+#include <xcb/xcb_event.h>
+#include <xcb/xcb_keysyms.h>
+#if HAVE_XCB_ICCCM
+# include <xcb/xcb_icccm.h>
+#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;
+}