/* * TTS - track your time. * Copyright (c) 2012-2014 Felicity Tarnell. * * Permission is granted to anyone to use this software for any purpose, * including commercial applications, and to alter it and redistribute it * freely. This software is provided 'as-is', without any express or implied * warranty. */ #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "config.h" #ifdef HAVE_IOREGISTERFORSYSTEMPOWER # define USE_DARWIN_POWER # include # include # include # include # include # include # include # include #endif #include "tailq.h" #include "tts.h" #include "entry.h" #include "wide.h" #include "tts_curses.h" #include "ui.h" #include "functions.h" #include "commands.h" #include "bindings.h" #include "str.h" #include "style.h" #include "variable.h" volatile sig_atomic_t doexit; time_t laststatus; history_t *searchhist; history_t *prompthist; #define STATFILE ".rttts" #define RCFILE ".ttsrc" int load(void); int save(void); static time_t lastsave; static char statfile[PATH_MAX + 1]; static int load_file(const char *); static tkey_t keys[] = { { KEY_BREAK, WIDE("") }, { KEY_DOWN, WIDE("") }, { KEY_UP, WIDE("") }, { KEY_LEFT, WIDE("") }, { KEY_RIGHT, WIDE("") }, { KEY_HOME, WIDE("") }, { KEY_BACKSPACE, WIDE("") }, { 0x7F, WIDE("") }, /* DEL */ { KEY_F(0), WIDE("") }, { KEY_F(1), WIDE("") }, { KEY_F(2), WIDE("") }, { KEY_F(3), WIDE("") }, { KEY_F(4), WIDE("") }, { KEY_F(5), WIDE("") }, { KEY_F(6), WIDE("") }, { KEY_F(7), WIDE("") }, { KEY_F(8), WIDE("") }, { KEY_F(9), WIDE("") }, { KEY_F(10), WIDE("") }, { KEY_F(11), WIDE("") }, { KEY_F(12), WIDE("") }, { KEY_F(13), WIDE("") }, { KEY_F(14), WIDE("") }, { KEY_F(15), WIDE("") }, { KEY_F(16), WIDE("") }, { KEY_F(17), WIDE("") }, { KEY_F(18), WIDE("") }, { KEY_F(19), WIDE("") }, { KEY_F(20), WIDE("") }, { KEY_F(21), WIDE("") }, { KEY_F(22), WIDE("") }, { KEY_F(23), WIDE("") }, { KEY_F(24), WIDE("") }, { KEY_NPAGE, WIDE("") }, { KEY_PPAGE, WIDE("") }, { '\001', WIDE("") }, { '\002', WIDE("") }, { '\003', WIDE("") }, { '\004', WIDE("") }, { '\005', WIDE("") }, { '\006', WIDE("") }, { '\007', WIDE("") }, { '\010', WIDE("") }, { '\011', WIDE("") }, { '\011', WIDE("") }, { '\012', WIDE("") }, { '\013', WIDE("") }, { '\014', WIDE("") }, { '\015', WIDE("") }, { '\016', WIDE("") }, { '\017', WIDE("") }, { '\020', WIDE("") }, { '\021', WIDE("") }, { '\022', WIDE("") }, { '\023', WIDE("") }, { '\024', WIDE("") }, { '\025', WIDE("") }, { '\026', WIDE("") }, { '\027', WIDE("") }, { '\030', WIDE("") }, { '\031', WIDE("") }, { ' ', WIDE("") }, { KEY_ENTER, WIDE("") }, { KEY_BACKSPACE, WIDE("") }, { KEY_DC, WIDE("") } }; int pagestart; entry_t *curent; int showinv = 0; static attrname_t attrnames[] = { { WIDE("normal"), WA_NORMAL }, { WIDE("bold"), WA_BOLD }, { WIDE("reverse"), WA_REVERSE }, { WIDE("blink"), WA_BLINK }, { WIDE("dim"), WA_DIM }, { WIDE("underline"), WA_UNDERLINE }, { WIDE("standout"), WA_STANDOUT } }; static colour_t colours[] = { { WIDE("black"), COLOR_BLACK }, { WIDE("red"), COLOR_RED }, { WIDE("green"), COLOR_GREEN }, { WIDE("yellow"), COLOR_YELLOW }, { WIDE("blue"), COLOR_BLUE }, { WIDE("magenta"), COLOR_MAGENTA }, { WIDE("cyan"), COLOR_CYAN }, { WIDE("white"), COLOR_WHITE } }; static short default_fg, default_bg; style_t sy_header = { 1, 0 }, sy_status = { 2, 0 }, sy_entry = { 3, 0 }, sy_running = { 4, 0 }, sy_selected = { 5, 0 }, sy_date = { 6, 0 }; time_t itime = 0; int show_billable = 0; int delete_advance = 1; int mark_advance = 1; int bill_advance = 0; int bill_increment = 0; WCHAR *auto_nonbillable; variable_t variables[] = { { WIDE("delete_advance"), VTYPE_BOOL, &delete_advance }, { WIDE("mark_advance"), VTYPE_BOOL, &mark_advance }, { WIDE("billable_advance"), VTYPE_BOOL, &bill_advance }, { WIDE("show_billable"), VTYPE_BOOL, &show_billable }, { WIDE("auto_non_billable"), VTYPE_STRING, &auto_nonbillable }, { WIDE("bill_increment"), VTYPE_INT, &bill_increment }, { } }; #ifdef USE_DARWIN_POWER static IONotificationPortRef port_ref; static io_object_t notifier; static mach_port_t ioport; static io_connect_t root_port; static volatile sig_atomic_t donesleep; static time_t sleeptime; static void power_setup(struct kevent *); static void power_handle(struct kevent *); static void power_event(void *, io_service_t, natural_t, void *); static void prompt_sleep(void); static void power_setup(ev) struct kevent *ev; { mach_port_t pset; int ret; if ((ret = mach_port_allocate(mach_task_self(), MACH_PORT_RIGHT_PORT_SET, &pset)) != KERN_SUCCESS) { fprintf(stderr, "mach_port_allocate: %s [%x]\n", mach_error_string(ret), ret); exit(1); } /* Register a handler for sleep and wake events */ root_port = IORegisterForSystemPower(NULL, &port_ref, power_event, ¬ifier); ioport = IONotificationPortGetMachPort(port_ref); EV_SET(ev, pset, EVFILT_MACHPORT, EV_ADD, 0, 0, 0); if ((ret = mach_port_insert_member(mach_task_self(), ioport, pset)) != KERN_SUCCESS) { fprintf(stderr, "mach_port_insert_member: %s [%x]\n", mach_error_string(ret), ret); exit(1); } } static void power_handle(ev) struct kevent *ev; { mach_msg_return_t ret; void *msg = alloca(ev->data); /* Receive the message */ memset(msg, 0, ev->data); ret = mach_msg(msg, MACH_RCV_MSG, 0, ev->data, ioport, MACH_MSG_TIMEOUT_NONE, MACH_PORT_NULL); if (ret != MACH_MSG_SUCCESS) { fprintf(stderr, "mach_msg: %s [%x]\n", mach_error_string(ret), ret); exit(1); } /* Give the message to IOKit to handle */ IODispatchCalloutFromMessage(NULL, msg, port_ref); } static void power_event(ref, service, msgtype, arg) void *ref, *arg; io_service_t service; natural_t msgtype; { static time_t sleep_started; time_t diff; switch (msgtype) { case kIOMessageSystemWillSleep: /* System is about to sleep; save the current time */ time(&sleep_started); IOAllowPowerChange(root_port, (long) arg); break; case kIOMessageSystemHasPoweredOn: /* System has finished wake-up; calculate the sleep time and * prompt the user. */ time(&diff); diff -= sleep_started; sleeptime += diff; prompt_sleep(); break; } } #endif static void sigexit(sig) { doexit = 1; } int main(argc, argv) char **argv; { struct passwd *pw; char rcfile[PATH_MAX + 1]; #ifdef USE_DARWIN_POWER int kq; struct kevent evs[2], rev; # define STDIN_EV 0 # define IOKIT_EV 1 #endif setlocale(LC_ALL, ""); #ifdef USE_DARWIN_POWER if ((kq = kqueue()) == -1) { perror("kqueue"); return 1; } memset(evs, 0, sizeof(evs)); EV_SET(&evs[STDIN_EV], STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, 0); power_setup(&evs[IOKIT_EV]); if (kevent(kq, evs, 2, NULL, 0, NULL) == -1) { perror("kevent"); return 1; } #endif signal(SIGTERM, sigexit); signal(SIGINT, sigexit); searchhist = hist_new(); prompthist = hist_new(); if ((pw = getpwuid(getuid())) == NULL || !pw->pw_dir || !*pw->pw_dir) { fprintf(stderr, "Sorry, I can't find your home directory."); return 1; } snprintf(statfile, sizeof(statfile), "%s/%s", pw->pw_dir, STATFILE); snprintf(rcfile, sizeof(rcfile), "%s/%s", pw->pw_dir, RCFILE); initscr(); in_curses = 1; start_color(); #ifdef HAVE_USE_DEFAULT_COLORS use_default_colors(); #endif cbreak(); noecho(); nonl(); nodelay(stdscr, 1); pair_content(0, &default_fg, &default_bg); refresh(); intrflush(stdscr, TRUE); keypad(stdscr, TRUE); leaveok(stdscr, TRUE); titwin = newwin(1, 0, 0, 0); intrflush(titwin, FALSE); keypad(titwin, TRUE); leaveok(titwin, TRUE); statwin = newwin(1, 0, LINES - 1, 0); intrflush(statwin, FALSE); keypad(statwin, TRUE); leaveok(statwin, TRUE); listwin = newwin(LINES - 2, 0, 1, 0); intrflush(listwin, FALSE); keypad(listwin, TRUE); leaveok(listwin, TRUE); init_pair(1, default_fg, default_bg); init_pair(2, default_fg, default_bg); init_pair(3, default_fg, default_bg); init_pair(4, default_fg, default_bg); init_pair(5, default_fg, default_bg); init_pair(6, default_fg, default_bg); style_set(&sy_header, WIDE("reverse"), NULL); style_set(&sy_status, WIDE("normal"), NULL); style_set(&sy_entry, WIDE("normal"), NULL); style_set(&sy_selected, WIDE("normal"), NULL); style_set(&sy_running, WIDE("bold"), NULL); style_set(&sy_date, WIDE("underline"), NULL); apply_styles(); if (load_file(rcfile) == -1) { endwin(); return 1; } curs_set(0); bind_key(WIDE("?"), WIDE("help")); bind_key(WIDE("a"), WIDE("add")); bind_key(WIDE("A"), WIDE("add-old")); bind_key(WIDE("d"), WIDE("delete")); bind_key(WIDE("u"), WIDE("undelete")); bind_key(WIDE("q"), WIDE("quit")); bind_key(WIDE(""), WIDE("quit")); bind_key(WIDE("i"), WIDE("invoice")); bind_key(WIDE("b"), WIDE("billable")); bind_key(WIDE("m"), WIDE("mark")); bind_key(WIDE("U"), WIDE("unmarkall")); bind_key(WIDE(""), WIDE("startstop")); bind_key(WIDE("e"), WIDE("edit-desc")); bind_key(WIDE("\\"), WIDE("edit-time")); bind_key(WIDE(""), WIDE("showhide-inv")); bind_key(WIDE("c"), WIDE("copy")); bind_key(WIDE("+"), WIDE("add-time")); bind_key(WIDE("-"), WIDE("sub-time")); bind_key(WIDE("/"), WIDE("search")); bind_key(WIDE("$"), WIDE("sync")); bind_key(WIDE(""), WIDE("prev")); bind_key(WIDE(""), WIDE("next")); bind_key(WIDE(":"), WIDE("execute")); bind_key(WIDE("M"), WIDE("merge")); bind_key(WIDE("r"), WIDE("interrupt")); bind_key(WIDE("R"), WIDE("interrupt")); /* * Make sure we can save (even if it's an empty file or nothing has * changed) before the user starts entering entries. */ load(); save(); drawheader(); drawstatus(WIDE("")); if (!TTS_TAILQ_EMPTY(&entries)) { curent = TTS_TAILQ_FIRST(&entries); while (!showinv && curent->en_flags.efl_invoiced) if ((curent = TTS_TAILQ_NEXT(curent, en_entries)) == NULL) break; } for (;;) { INT c; binding_t *bi; #ifdef USE_DARWIN_POWER struct timespec timeout; int nev; #else fd_set in_set; struct timeval timeout; #endif if (doexit) break; drawheader(); drawentries(); wrefresh(listwin); #ifdef USE_DARWIN_POWER timeout.tv_sec = 0; timeout.tv_nsec = 500000000; if ((nev = kevent(kq, NULL, 0, &rev, 1, &timeout)) == -1) { if (doexit) break; if (errno == EINTR) continue; perror("kevent"); return 1; } if (nev == 1 && (rev.filter == EVFILT_MACHPORT)) { power_handle(&rev); continue; } #else /* Wait for input to be ready. */ FD_ZERO(&in_set); FD_SET(STDIN_FILENO, &in_set); timeout.tv_sec = 0; timeout.tv_usec = 500000; if (select(STDIN_FILENO + 1, &in_set, NULL, NULL, &timeout) == -1) { if (doexit) break; if (errno == EINTR) continue; } #endif while (GETCH(&c) != ERR) { #ifdef KEY_RESIZE if (c == KEY_RESIZE) continue; #endif drawstatus(WIDE("")); TTS_TAILQ_FOREACH(bi, &bindings, bi_entries) { if (bi->bi_code != c) continue; bi->bi_func->fn_hdl(); goto next;; } drawstatus(WIDE("Unknown command.")); next: ; } if (doexit) break; if (time(NULL) - laststatus >= 2) drawstatus(WIDE("")); if (time(NULL) - lastsave > 60) save(); } save(); endwin(); return 0; } void drawentries() { int i, nlines; int cline = 0; time_t lastday = 0; entry_t *en; chtype oldbg; getmaxyx(listwin, nlines, i); TTS_TAILQ_FOREACH(en, &entries, en_entries) en->en_flags.efl_visible = 0; en = TTS_TAILQ_FIRST(&entries); for (i = 0; i < pagestart; i++) if ((en = TTS_TAILQ_NEXT(en, en_entries)) == NULL) return; for (; en; en = TTS_TAILQ_NEXT(en, en_entries)) { time_t n; int h, s, m; WCHAR flags[10], stime[16], *p; attr_t attrs = WA_NORMAL; if (!showinv && en->en_flags.efl_invoiced) continue; oldbg = getbkgd(listwin); if (lastday != entry_day(en)) { struct tm *lt; WCHAR lbl[128]; time_t itime = entry_time_for_day(entry_day(en), 1, 0), ntime = entry_time_for_day(entry_day(en), 0, 0), btime = entry_time_for_day(entry_day(en), 2, bill_increment); int hi, mi, si, hn, mn, sn, hb, mb, sb, ht, mt, st; WCHAR hdrtext[256]; time_to_hms(itime, hi, mi, si); time_to_hms(ntime, hn, mn, sn); time_to_hms(btime, hb, mb, sb); time_to_hms(itime + ntime, ht, mt, st); oldbg = getbkgd(listwin); wbkgdset(listwin, style_bg(sy_entry)); wattr_on(listwin, style_fg(sy_entry), NULL); wmove(listwin, cline, 0); wclrtoeol(listwin); wbkgdset(listwin, oldbg); wattr_off(listwin, style_fg(sy_entry), NULL); if (++cline >= nlines) break; lastday = entry_day(en); lt = localtime(&lastday); STRFTIME(lbl, WSIZEOF(lbl), WIDE("%A, %d %B %Y"), lt); if (show_billable) SNPRINTF(hdrtext, WSIZEOF(hdrtext), WIDE("%-30"FMT_L"s [I:%02d:%02d:%02d / " "N:%02d:%02d:%02d / T:%02d:%02d:%02d / " "B:%02d:%02d:%02d]"), lbl, hi, mi, si, hn, mn, sn, ht, mt, st, hb, mb, sb); else SNPRINTF(hdrtext, WSIZEOF(hdrtext), WIDE("%-30"FMT_L"s [I:%02d:%02d:%02d / " "N:%02d:%02d:%02d / T:%02d:%02d:%02d]"), lbl, hi, mi, si, hn, mn, sn, ht, mt, st); wattr_on(listwin, style_fg(sy_date), NULL); wbkgdset(listwin, style_bg(sy_date)); wmove(listwin, cline, 0); WADDSTR(listwin, hdrtext); wclrtoeol(listwin); wattr_off(listwin, style_fg(sy_date), NULL); wbkgdset(listwin, oldbg); if (++cline >= nlines) { wbkgdset(listwin, oldbg); wattr_off(listwin, style_fg(sy_date), NULL); break; } oldbg = getbkgd(listwin); wbkgdset(listwin, style_bg(sy_entry)); wattr_on(listwin, style_fg(sy_entry), NULL); wmove(listwin, cline, 0); wclrtoeol(listwin); wbkgdset(listwin, oldbg); wattr_off(listwin, style_fg(sy_entry), NULL); if (++cline >= nlines) break; } en->en_flags.efl_visible = 1; wmove(listwin, cline, 0); attrs = style_fg(sy_entry); if (en->en_started && en == curent) attrs = style_fg(sy_selected) | (style_fg(sy_running) & ( WA_STANDOUT | WA_UNDERLINE | WA_REVERSE | WA_BLINK | WA_DIM | WA_BOLD)); else if (en->en_started) attrs = style_fg(sy_running); else if (en == curent) attrs = style_fg(sy_selected); wbkgdset(listwin, ' ' | (attrs & ~WA_UNDERLINE)); wattr_on(listwin, attrs, NULL); if (en == curent) { WADDSTR(listwin, WIDE(" -> ")); } else WADDSTR(listwin, WIDE(" ")); n = en->en_secs; if (en->en_started) n += time(NULL) - en->en_started; h = n / (60 * 60); n %= (60 * 60); m = n / 60; n %= 60; s = n; SNPRINTF(stime, WSIZEOF(stime), WIDE("%02d:%02d:%02d%c "), h, m, s, (itime && (en == running)) ? '*' : ' '); WADDSTR(listwin, stime); memset(flags, 0, sizeof(flags)); p = flags; if (en->en_flags.efl_marked) *p++ = 'M'; else *p++ = ' '; if (en->en_flags.efl_invoiced) *p++ = 'I'; else *p++ = ' '; if (!en->en_flags.efl_nonbillable) *p++ = 'B'; else *p++ = ' '; if (en->en_flags.efl_deleted) *p++ = 'D'; else *p++ = ' '; if (*flags) { WCHAR s[10]; SNPRINTF(s, WSIZEOF(s), WIDE("%-5"FMT_L"s "), flags); WADDSTR(listwin, s); } else WADDSTR(listwin, WIDE(" ")); WADDSTR(listwin, en->en_desc); wclrtoeol(listwin); wbkgdset(listwin, oldbg); wattr_off(listwin, attrs, NULL); if (++cline >= nlines) return; } oldbg = getbkgd(listwin); wattr_on(listwin, style_fg(sy_entry), NULL); wbkgdset(listwin, style_bg(sy_entry)); for (; cline < nlines; cline++) { wmove(listwin, cline, 0); wclrtoeol(listwin); } wattr_off(listwin, style_fg(sy_entry), NULL); wbkgdset(listwin, oldbg); } int load() { FILE *f; char input[4096]; WCHAR line[4096]; entry_t *en; TTS_TAILQ_FOREACH(en, &entries, en_entries) entry_free(en); if ((f = fopen(statfile, "r")) == NULL) { if (errno == ENOENT) return 0; errbox(WIDE("Can't read %s: %s"), statfile, strerror(errno)); exit(1); } if (fgets(input, sizeof(input), f) == NULL) { errbox(WIDE("Can't read %s: %s"), statfile, strerror(errno)); fclose(f); exit(1); } MBSTOWCS(line, input, WSIZEOF(line)); if (STRCMP(line, WIDE("#%RT/TTS V1\n"))) { errbox(WIDE("Can't read %s: invalid magic signature"), statfile); fclose(f); exit(1); } while (fgets(input, sizeof(input), f)) { unsigned long cre, secs; WCHAR flags[10], desc[4096], *p; entry_t *en; int i; MBSTOWCS(line, input, WSIZEOF(line)); if (SSCANF(line, WIDE("#%%showinv %d\n"), &i) == 1) { showinv = i ? 1 : 0; continue; } if (SSCANF(line, WIDE("%lu %lu %9"FMT_L"[in-] %4095"FMT_L"[^\n]\n"), &cre, &secs, flags, desc) != 4) { errbox(WIDE("Can't read %s: invalid entry format"), statfile); fclose(f); exit(1); } en = entry_new(desc); en->en_created = cre; en->en_secs = secs; for (p = flags; *p; p++) { switch (*p) { case '-': break; case 'i': en->en_flags.efl_invoiced = 1; break; case 'n': en->en_flags.efl_nonbillable = 1; break; default: errbox(WIDE("Can't read %s: invalid flag"), statfile); fclose(f); exit(1); } } } if (ferror(f)) { errbox(WIDE("Can't read %s: %s"), statfile, strerror(errno)); fclose(f); exit(1); } fclose(f); return 0; } int save() { FILE *f; int fd; char p[PATH_MAX + 1]; entry_t *en; snprintf(p, sizeof(p), "%s_", statfile); if ((fd = open(p, O_WRONLY | O_CREAT, 0600)) == -1) { errbox(WIDE("%s_: %s"), statfile, strerror(errno)); endwin(); exit(1); } if ((f = fdopen(fd, "w")) == NULL) { errbox(WIDE("%s: %s"), p, strerror(errno)); endwin(); exit(1); } if (FPRINTF(f, WIDE("#%%RT/TTS V1\n")) == -1) { fclose(f); unlink(p); errbox(WIDE("%s: write error (header): %s"), p, strerror(errno)); endwin(); exit(1); } if (FPRINTF(f, WIDE("#%%showinv %d\n"), showinv) == -1) { fclose(f); unlink(p); errbox(WIDE("%s: write error (showinv): %s"), p, strerror(errno)); endwin(); exit(1); } TTS_TAILQ_FOREACH_REVERSE(en, &entries, entrylist, en_entries) { char flags[10], *fp = flags, wdesc[4096] = {}; time_t n; WCSTOMBS(wdesc, en->en_desc, sizeof(wdesc)); memset(flags, 0, sizeof(flags)); if (en->en_flags.efl_invoiced) *fp++ = 'i'; if (en->en_flags.efl_nonbillable) *fp++ = 'n'; n = en->en_secs; if (en->en_started) n += time(NULL) - en->en_started; if (FPRINTF(f, WIDE("%lu %lu %s %s\n"), (unsigned long) en->en_created, (unsigned long) n, *flags ? flags : "-", wdesc) == -1) { errbox(WIDE("%s: write error (entry): %s"), p, strerror(errno)); fclose(f); unlink(p); endwin(); exit(1); } } if (fclose(f) == EOF) { unlink(p); errbox(WIDE("%s: write error (closing): %s"), p, strerror(errno)); endwin(); exit(1); } if (rename(p, statfile) == -1) { unlink(p); errbox(WIDE("%s: rename: %s"), statfile, strerror(errno)); endwin(); exit(1); } time(&lastsave); return 0; } void errbox(const WCHAR *msg, ...) { va_list ap; va_start(ap, msg); verrbox(msg, ap); va_end(ap); } void verrbox(msg, ap) const WCHAR *msg; va_list ap; { WCHAR text[4096]; WINDOW *ewin; #define ETITLE WIDE(" Error ") #define ECONT WIDE(" ") int width; INT c; VSNPRINTF(text, WSIZEOF(text), msg, ap); width = STRLEN(text); ewin = newwin(6, width + 4, (LINES / 2) - ((1 + 2)/ 2), (COLS / 2) - ((width + 2) / 2)); leaveok(ewin, TRUE); wborder(ewin, 0, 0, 0, 0, 0, 0, 0, 0); wattron(ewin, A_REVERSE | A_BOLD); wmove(ewin, 0, (width / 2) - (WSIZEOF(ETITLE) - 1)/2); WADDSTR(ewin, ETITLE); wattroff(ewin, A_REVERSE | A_BOLD); wmove(ewin, 2, 2); WADDSTR(ewin, text); wattron(ewin, A_REVERSE | A_BOLD); wmove(ewin, 4, (width / 2) - ((WSIZEOF(ECONT) - 1) / 2)); WADDSTR(ewin, ECONT); wattroff(ewin, A_REVERSE | A_BOLD); for (;;) { if (WGETCH(ewin, &c) == ERR) continue; if (c == '\r') break; } delwin(ewin); } history_t * hist_new() { history_t *hi; if ((hi = calloc(1, sizeof(*hi))) == NULL) return NULL; TTS_TAILQ_INIT(&hi->hi_ents); return hi; } void hist_add(hi, text) history_t *hi; const WCHAR *text; { histent_t *hent; if (!*text) return; if ((hent = calloc(1, sizeof(*hent))) == NULL) return; if ((hent->he_text = STRDUP(text)) == NULL) { free(hent); return; } TTS_TAILQ_INSERT_TAIL(&hi->hi_ents, hent, he_entries); if (hi->hi_nents == 50) TTS_TAILQ_REMOVE(&hi->hi_ents, TTS_TAILQ_FIRST(&hi->hi_ents), he_entries); else ++hi->hi_nents; } /* * Return the tkey_t for the key called .name, or NULL if such a key doesn't * exist. */ tkey_t * find_key(name) const WCHAR *name; { size_t i; for (i = 0; i < sizeof(keys) / sizeof(*keys); i++) if (STRCMP(name, keys[i].ky_name) == 0) return &keys[i]; return NULL; } /* * Return the function_t for the function called .name, or NULL if such a * function doesn't exist. */ function_t * find_func(name) const WCHAR *name; { function_t *f; for (f = funcs; f->fn_name; f++) if (STRCMP(name, f->fn_name) == 0) return f; return NULL; } /* * Bind .keyname to run the function .funcname. If a binding for .keyname * already exists, overwrite it. * * If .keyname is a single character, e.g. 'a', it is used as a key name * directly, rather than being looked up in the key table. */ void bind_key(keyname, funcname) const WCHAR *keyname, *funcname; { tkey_t *key = NULL; function_t *func; binding_t *binding; INT code; /* Find the key and the function */ if (STRLEN(keyname) > 1) { if ((key = find_key(keyname)) == NULL) { errbox(WIDE("Unknown key \"%"FMT_L"s\""), keyname); return; } code = key->ky_code; } else code = *keyname; if ((func = find_func(funcname)) == NULL) { errbox(WIDE("Unknown function \"%"FMT_L"s\""), funcname); return; } /* Do we already have a binding for this key? */ TTS_TAILQ_FOREACH(binding, &bindings, bi_entries) { if (binding->bi_code == code) { binding->bi_func = func; return; } } /* No, add a new one */ if ((binding = calloc(1, sizeof(*binding))) == NULL) return; binding->bi_key = key; binding->bi_func = func; binding->bi_code = code; TTS_TAILQ_INSERT_TAIL(&bindings, binding, bi_entries); } variable_t * find_variable(name) WCHAR const *name; { variable_t *v; for (v = variables; v->va_name; v++) if (STRCMP(name, v->va_name) == 0) return v; return NULL; } int attr_find(name, result) const WCHAR *name; attr_t *result; { size_t i; for (i = 0; i < sizeof(attrnames) / sizeof(*attrnames); i++) { if (STRCMP(attrnames[i].an_name, name) == 0) { *result = attrnames[i].an_value; return 0; } } return -1; } int colour_find(name, result) const WCHAR *name; short *result; { size_t i; for (i = 0; i < sizeof(colours) / sizeof(*colours); i++) { if (STRCMP(colours[i].co_name, name) == 0) { *result = colours[i].co_value; return 0; } } return -1; } void style_clear(sy) style_t *sy; { init_pair(sy->sy_pair, default_fg, default_bg); sy->sy_attrs = WA_NORMAL; } int style_set(sy, fg, bg) style_t *sy; const WCHAR *fg, *bg; { sy->sy_attrs = WA_NORMAL; init_pair(sy->sy_pair, default_fg, default_bg); return style_add(sy, fg, bg); } int style_add(sy, fg, bg) style_t *sy; const WCHAR *fg, *bg; { attr_t at; short colfg, colbg = default_bg; if (colour_find(fg, &colfg) == 0) { if (bg && (colour_find(bg, &colbg) == -1)) return -1; init_pair(sy->sy_pair, colfg, colbg); return 0; } if (attr_find(fg, &at) == -1) return -1; sy->sy_attrs |= at; return 0; } void apply_styles() { wbkgd(statwin, style_bg(sy_status)); wattr_on(statwin, style_fg(sy_status), NULL); drawstatus(WIDE("")); wbkgd(titwin, style_bg(sy_header)); wattr_on(titwin, style_fg(sy_header), NULL); drawheader(); } static char *curfile; static int lineno, nerr; void cmderr(const WCHAR *msg, ...) { va_list ap; va_start(ap, msg); vcmderr(msg, ap); va_end(ap); } void vcmderr(msg, ap) const WCHAR *msg; va_list ap; { nerr++; if (curfile) { WCHAR s[1024]; char t[1024]; VSNPRINTF(s, WSIZEOF(t), msg, ap); WCSTOMBS(t, s, sizeof(t)); if (in_curses) { endwin(); in_curses = 0; } fprintf(stderr, "\"%s\", line %d: %s\n", curfile, lineno, t); } else vdrawstatus(msg, ap); } /* * Load configuration commands from the file .name. Expects to be called * inside curses mode; it will endwin() if an error occurs. * * Returns 0 on success, or -1 if any errors occur. */ int load_file(name) const char *name; { FILE *s; char input[1024]; nerr = 0; if ((s = fopen(name, "r")) == NULL) { if (errno == ENOENT) return 0; fprintf(stderr, "%s: %s", name, strerror(errno)); return -1; } curfile = strdup(name); while (fgets(input, sizeof(input), s)) { size_t nargs; WCHAR **args; command_t *cmds; WCHAR line[1024]; ++lineno; MBSTOWCS(line, input, WSIZEOF(line)); if (line[0] == '#') continue; nargs = tokenise(line, &args); if (nargs == 0) { tokfree(&args); continue; } if ((cmds = find_command(args[0])) == NULL) { cmderr(WIDE("Unknown command \"%"FMT_L"s\"."), args[0]); nerr++; tokfree(&args); continue; } cmds->cm_hdl(nargs, args); tokfree(&args); } fclose(s); free(curfile); curfile = NULL; if (nerr) return -1; return 0; } int prduration(pr, hh, mm, ss) WCHAR *pr; int *hh, *mm, *ss; { WCHAR *tstr; int h, m, s; if ((tstr = prompt(pr, WIDE("00:00:00"), NULL)) == NULL) return -1; if (!*tstr) { drawstatus(WIDE("No duration entered")); free(tstr); return -1; } if (SSCANF(tstr, WIDE("%d:%d:%d"), &h, &m, &s) != 3) { h = 0; if (SSCANF(tstr, WIDE("%d:%d"), &m, &s) != 2) { m = 0; if (SSCANF(tstr, WIDE("%d"), &s) != 1) { free(tstr); drawstatus(WIDE("Invalid time format.")); return -1; } } } free(tstr); if (m >= 60) { drawstatus(WIDE("Minutes cannot be more than 59.")); return -1; } if (s >= 60) { drawstatus(WIDE("Seconds cannot be more than 59.")); return -1; } *hh = h; *mm = m; *ss = s; return 0; } #ifdef USE_DARWIN_POWER static void prompt_sleep() { /* * We woke from sleep. If there's a running entry, prompt the user to * subtract the time spent sleeping, in case they forgot to turn off * the timer. */ WCHAR pr[128]; int h, m, s = 0; donesleep = 0; /* Only prompt if an entry is running */ if (!running) return; /* Draw the prompt */ s = sleeptime; h = s / (60 * 60); s %= (60 * 60); m = s / 60; s %= 60; SNPRINTF(pr, WSIZEOF(pr), WIDE("Remove %02d:%02d:%02d time asleep from running entry?"), h, m, s); if (!yesno(pr)) { sleeptime = 0; return; } /* * This is a bit of a fudge, but it has the desired effect. Alternatively * we could merge en_started into en_secs, then subtract that. */ running->en_started += sleeptime; sleeptime = 0; } #endif /* USE_DARWIN_POWER */