diff --git a/Makefile.in b/Makefile.in index 8ee1584..dd1b3a1 100644 --- a/Makefile.in +++ b/Makefile.in @@ -1,13 +1,15 @@ VPATH = @top_srcdir@ CC = @CC@ -CPPFLAGS = @CPPFLAGS@ +# _GNU_SOURCE is required for wcsdup() on older version of glibc that don't +# implement XPG7. +CPPFLAGS = @CPPFLAGS@ -D_XOPEN_SOURCE=700 -D_XOPEN_SOURCE_EXTENDED -D__EXTENSIONS__ CFLAGS = @CFLAGS@ -I@top_srcdir@ -I@top_builddir@ LDFLAGS = @LDFLAGS@ LIBS = @LIBS@ INSTALL = @INSTALL@ -OBJS = tts.o +OBJS = tts.o wide.o entry.o ui.o functions.o commands.o bindings.o str.o prefix = @prefix@ exec_prefix = @exec_prefix@ diff --git a/bindings.c b/bindings.c new file mode 100644 index 0000000..0132f65 --- /dev/null +++ b/bindings.c @@ -0,0 +1,13 @@ +/* + * 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 "bindings.h" + +binding_list_t bindings = TAILQ_HEAD_INITIALIZER(bindings); diff --git a/bindings.h b/bindings.h new file mode 100644 index 0000000..8b7df24 --- /dev/null +++ b/bindings.h @@ -0,0 +1,36 @@ +/* + * 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. + */ + +#ifndef TTS_BINDINGS_H +#define TTS_BINDINGS_H + +#include "wide.h" +#include "functions.h" +#include "queue.h" + +typedef struct tkey { + INT ky_code; + const WCHAR *ky_name; +} tkey_t; + +typedef struct binding { + INT bi_code; + tkey_t *bi_key; + function_t *bi_func; + TAILQ_ENTRY(binding) bi_entries; +} binding_t; + +typedef TAILQ_HEAD(bindlist, binding) binding_list_t; +extern binding_list_t bindings; + +tkey_t *find_key(const WCHAR *name); +void bind_key(const WCHAR *key, const WCHAR *func); + +#endif /* !TTS_BINDINGS_H */ diff --git a/commands.c b/commands.c new file mode 100644 index 0000000..906cabc --- /dev/null +++ b/commands.c @@ -0,0 +1,133 @@ +/* + * 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 "commands.h" +#include "style.h" +#include "bindings.h" +#include "variable.h" + +static command_t commands[] = { + { WIDE("bind"), c_bind }, + { WIDE("style"), c_style }, + { WIDE("set"), c_set }, + { } +}; + +command_t * +find_command(name) + const WCHAR *name; +{ +size_t i; + for (i = 0; i < sizeof(commands) / sizeof(*commands); i++) + if (STRCMP(name, commands[i].cm_name) == 0) + return &commands[i]; + return NULL; +} + +void +c_style(argc, argv) + size_t argc; + WCHAR **argv; +{ +style_t *sy; +WCHAR *last, *tok; + + if (argc < 3 || argc > 4) { + cmderr(WIDE("Usage: style [background]")); + return; + } + + if (STRCMP(argv[1], WIDE("header")) == 0) + sy = &sy_header; + else if (STRCMP(argv[1], WIDE("status")) == 0) + sy = &sy_status; + else if (STRCMP(argv[1], WIDE("entry")) == 0) + sy = &sy_entry; + else if (STRCMP(argv[1], WIDE("selected")) == 0) + sy = &sy_selected; + else if (STRCMP(argv[1], WIDE("running")) == 0) + sy = &sy_running; + else if (STRCMP(argv[1], WIDE("date")) == 0) + sy = &sy_date; + else { + cmderr(WIDE("Unknown style item.")); + return; + } + + style_clear(sy); + for (tok = STRTOK(argv[2], WIDE(","), &last); tok != NULL; + tok = STRTOK(NULL, WIDE(","), &last)) { + style_add(sy, tok, argv[3]); + } + + apply_styles(); +} + +void +c_bind(argc, argv) + size_t argc; + WCHAR **argv; +{ + if (argc != 3) { + cmderr(WIDE("Usage: bind ")); + return; + } + + bind_key(argv[1], argv[2]); +} + +void +c_set(argc, argv) + size_t argc; + WCHAR **argv; +{ +variable_t *var; +int val; + + if (argc != 3) { + cmderr(WIDE("Usage: set ")); + return; + } + + if ((var = find_variable(argv[1])) == NULL) { + cmderr(WIDE("Unknown variable \"%"FMT_L"s\"."), argv[1]); + return; + } + + switch (var->va_type) { + case VTYPE_BOOL: + if (STRCMP(argv[2], WIDE("true")) == 0 || + STRCMP(argv[2], WIDE("yes")) == 0 || + STRCMP(argv[2], WIDE("on")) == 0 || + STRCMP(argv[2], WIDE("1")) == 0) { + val = 1; + } else if (STRCMP(argv[2], WIDE("false")) == 0 || + STRCMP(argv[2], WIDE("no")) == 0 || + STRCMP(argv[2], WIDE("off")) == 0 || + STRCMP(argv[2], WIDE("0")) == 0) { + val = 0; + } else { + cmderr(WIDE("Invalid value for boolean: \"%"FMT_L"s\"."), argv[2]); + return; + } + + *(int *)var->va_addr = val; + break; + + case VTYPE_STRING: + *(WCHAR **)var->va_addr = STRDUP(argv[2]); + break; + + case VTYPE_INT: + *(int *)var->va_addr = STRTOL(argv[2], NULL, 0); + break; + } +} + diff --git a/commands.h b/commands.h new file mode 100644 index 0000000..cad450b --- /dev/null +++ b/commands.h @@ -0,0 +1,32 @@ +/* + * 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. + */ + +#ifndef TTS_COMMANDS_H +#define TTS_COMMANDS_H + +#include + +#include "wide.h" + +typedef struct command { + const WCHAR *cm_name; + void (*cm_hdl) (size_t, WCHAR **); +} command_t; + +command_t *find_command(const WCHAR *); + +void c_bind (size_t, WCHAR **); +void c_style (size_t, WCHAR **); +void c_set (size_t, WCHAR **); + +void cmderr (const WCHAR *, ...); +void vcmderr (const WCHAR *, va_list); + +#endif /* !TTS_COMMANDS_H */ diff --git a/entry.c b/entry.c new file mode 100644 index 0000000..b6e4ce6 --- /dev/null +++ b/entry.c @@ -0,0 +1,124 @@ +/* + * 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 "entry.h" +#include "wide.h" +#include "tts.h" + +entry_list entries = TAILQ_HEAD_INITIALIZER(entries); + +entry_t *running; + +entry_t * +entry_new(desc) + const WCHAR *desc; +{ +entry_t *en; + if ((en = calloc(1, sizeof(*en))) == NULL) + return NULL; + + if (auto_nonbillable && STRSTR(desc, auto_nonbillable)) + en->en_flags.efl_nonbillable = 1; + + TAILQ_INSERT_HEAD(&entries, en, en_entries); + + en->en_desc = STRDUP(desc); + time(&en->en_created); + + return en; +} + +void +entry_start(en) + entry_t *en; +{ + if (running) + entry_stop(running); + time(&en->en_started); + running = en; +} + +void +entry_stop(en) + entry_t *en; +{ + if (running == en) + running = NULL; + en->en_secs += time(NULL) - en->en_started; + en->en_started = 0; +} + +void +entry_free(en) + entry_t *en; +{ + if (en == running) + entry_stop(en); + free(en->en_desc); +} + +void +entry_account(en) + entry_t *en; +{ + if (!en->en_started) + return; + en->en_secs += time(NULL) - en->en_started; + time(&en->en_started); +} + +/* + * Return the amount of time for the day on which the timestamp .when falls. + * If .inv is 0, sum non-invoiced entries; if 1, sum invoiced entries; if + * 2, sum billable entries; if -1, sum all entries. If .incr is non-zero, + * individual entry time will be rounded up to intervals of that many minutes. + */ +time_t +entry_time_for_day(when, inv, incr) + time_t when; +{ +time_t day = time_day(when); +time_t sum = 0; +entry_t *en; +int rnd = incr * 60; + TAILQ_FOREACH(en, &entries, en_entries) { + time_t n; + + if (entry_day(en) > day) + continue; + if (entry_day(en) < day) + break; + + if (inv == 0 && en->en_flags.efl_invoiced) + continue; + if (inv == 1 && !en->en_flags.efl_invoiced) + continue; + if (inv == 2 && en->en_flags.efl_nonbillable) + continue; + + n = en->en_secs; + if (en->en_started) + n += time(NULL) - en->en_started; + + if (!n) + continue; + + if (rnd) + n = (1 + round((n - 1) / rnd)) * rnd; + sum += n; + } + return sum; +} + diff --git a/entry.h b/entry.h new file mode 100644 index 0000000..68b45a1 --- /dev/null +++ b/entry.h @@ -0,0 +1,59 @@ +/* + * 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. + */ + +#ifndef TTS_ENTRY_H +#define TTS_ENTRY_H + +#include + +#include "queue.h" +#include "wide.h" + +typedef struct entry { + WCHAR *en_desc; + int en_secs; + time_t en_started; + time_t en_created; + + struct { + int efl_visible:1; + int efl_invoiced:1; + int efl_marked:1; + int efl_deleted:1; + int efl_nonbillable:1; + } en_flags; + TAILQ_ENTRY(entry) en_entries; +} entry_t; + +typedef TAILQ_HEAD(entrylist, entry) entry_list; +extern entry_list entries; + +extern entry_t *running; + +entry_t *entry_new (const WCHAR *); +void entry_start (entry_t *); +void entry_stop (entry_t *); +void entry_free (entry_t *); +void entry_account (entry_t *); +time_t entry_time_for_day (time_t, int, int); + +#define time_day(t) (((t) / (60 * 60 * 24)) * (60 * 60 * 24)) +#define entry_day(e) (time_day((e)->en_created)) + +#define time_to_hms(t, h, m, s) do { \ + time_t n = t; \ + h = n / (60 * 60); \ + n %= (60 * 60); \ + m = n / 60; \ + n %= 60; \ + s = n; \ + } while (0) + +#endif /* !TTS_ENTRY_H */ diff --git a/functions.c b/functions.c new file mode 100644 index 0000000..3b8631a --- /dev/null +++ b/functions.c @@ -0,0 +1,800 @@ +/* + * 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 "functions.h" +#include "tts.h" +#include "entry.h" +#include "ui.h" +#include "commands.h" +#include "tts_curses.h" +#include "bindings.h" +#include "str.h" + +function_t funcs[] = { + { WIDE("help"), khelp, WIDE("display help screen") }, + { WIDE("add"), kadd, WIDE("add a new entry and start the timer") }, + { WIDE("add-old"), kaddold, WIDE("add a new entry and specify its duration") }, + { WIDE("delete"), kmarkdel, WIDE("delete the current entry") }, + { WIDE("undelete"), kundel, WIDE("undelete the current entry") }, + { WIDE("quit"), kquit, WIDE("exit TTS") }, + { WIDE("invoice"), kinvoiced, WIDE("toggle current entry as invoiced") }, + { WIDE("billable"), kbillable, WIDE("toggle current entry as billable") }, + { WIDE("mark"), kmark, WIDE("mark the current entry") }, + { WIDE("unmarkall"), kunmarkall, WIDE("unmark all entries") }, + { WIDE("startstop"), ktoggle, WIDE("start or stop the timer") }, + { WIDE("edit-desc"), keddesc, WIDE("edit the current entry's description") }, + { WIDE("edit-time"), kedtime, WIDE("edit the current entry's duration") }, + { WIDE("showhide-inv"), ktoggleinv, WIDE("show or hide invoiced entries") }, + { WIDE("copy"), kcopy, WIDE("copy the current entry's description to a new entry") }, + { WIDE("add-time"), kaddtime, WIDE("add time to the current entry") }, + { WIDE("sub-time"), kdeltime, WIDE("subtract time from the current entry") }, + { WIDE("search"), ksearch, WIDE("search for an entry by name") }, + { WIDE("sync"), ksync, WIDE("purge all deleted entries") }, + { WIDE("prev"), kup, WIDE("move to the previous entry") }, + { WIDE("next"), kdown, WIDE("move to the next entry") }, + { WIDE("execute"), kexec, WIDE("execute a configuration command") }, + { WIDE("merge"), kmerge, WIDE("merge marked entries into current entry") }, + { WIDE("interrupt"), kint, WIDE("split current entry into new entry")}, + { } +}; + +void +kquit() +{ +entry_t *en; +int ndel = 0; + + TAILQ_FOREACH(en, &entries, en_entries) { + if (en->en_flags.efl_deleted) + ndel++; + } + + if (ndel) { + WCHAR s[128]; + SNPRINTF(s, WSIZEOF(s), WIDE("Purge %d deleted entries?"), ndel); + if (yesno(s)) { + ksync(); + } + } + + doexit = 1; +} + +void +kadd() +{ +WCHAR *name; +entry_t *en; + name = prompt(WIDE("Description:"), NULL, NULL); + if (!name || !*name) { + free(name); + return; + } + en = entry_new(name); + entry_start(en); + curent = en; + save(); +} + +void +kaddold() +{ +WCHAR *name; +entry_t *en; + name = prompt(WIDE("Description:"), NULL, NULL); + + if (!name || !*name) { + free(name); + return; + } + + en = entry_new(name); + curent = en; + kedtime(); + save(); +} + +void +ktoggle() +{ + itime = 0; + + if (!curent) + return; + + if (curent == running) { + entry_stop(curent); + save(); + return; + } + + if (running) + entry_stop(running); + entry_start(curent); + save(); +} + +void +kundel() +{ + if (!curent) + return; + + curent->en_flags.efl_deleted = 0; + if (delete_advance) + cursadvance(); +} + +void +kmarkdel() +{ +entry_t *en; +int nmarked = 0; + + TAILQ_FOREACH(en, &entries, en_entries) { + if (en->en_flags.efl_marked) { + nmarked++; + en->en_flags.efl_deleted = 1; + } + } + + if (nmarked) + return; + + if (!curent) { + drawstatus(WIDE("No entries to delete.")); + return; + } + + curent->en_flags.efl_deleted = 1; + + if (delete_advance) + cursadvance(); +} + +void +ksync() +{ +entry_t *en, *ten; + + TAILQ_FOREACH_SAFE(en, &entries, en_entries, ten) { + if (!en->en_flags.efl_deleted) + continue; + if (en == curent) + curent = NULL; + TAILQ_REMOVE(&entries, en, en_entries); + entry_free(en); + } + + if (curent == NULL) + curent = TAILQ_FIRST(&entries); + save(); +} + +void +kup() +{ +entry_t *prev = curent; + if (!curent) + return; + + do { + if ((prev = TAILQ_PREV(prev, entrylist, en_entries)) == NULL) + break; + } while (!showinv && prev->en_flags.efl_invoiced); + + if (prev == NULL) { + drawstatus(WIDE("Already at first entry.")); + return; + } + + curent = prev; + if (!curent->en_flags.efl_visible) + pagestart--; +} + +void +kdown() +{ +entry_t *next = curent; + if (!curent) + return; + + do { + if ((next = TAILQ_NEXT(next, en_entries)) == NULL) + break; + } while (!showinv && next->en_flags.efl_invoiced); + + if (next == NULL) { + drawstatus(WIDE("Already at last entry.")); + return; + } + + curent = next; + if (!curent->en_flags.efl_visible) + pagestart++; +} + +void +kinvoiced() +{ +entry_t *en; +int anymarked = 0; + + TAILQ_FOREACH(en, &entries, en_entries) { + if (!en->en_flags.efl_marked) + continue; + anymarked = 1; + en->en_flags.efl_invoiced = !en->en_flags.efl_invoiced; + en->en_flags.efl_marked = 0; + } + + if (anymarked) { + save(); + return; + } + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + curent->en_flags.efl_invoiced = !curent->en_flags.efl_invoiced; + save(); + + en = curent; + + if (showinv) { + if (TAILQ_NEXT(curent, en_entries) != NULL) + curent = TAILQ_NEXT(curent, en_entries); + return; + } + + /* + * Try to find the next uninvoiced request to move the cursor to. + */ + for (;;) { + if ((curent = TAILQ_NEXT(curent, en_entries)) == NULL) + break; /* end of list */ + if (!curent->en_flags.efl_invoiced) + return; + } + + /* + * We didn't find any, so try searching backwards instead. + */ + for (curent = en;;) { + if ((curent = TAILQ_PREV(curent, entrylist, en_entries)) == NULL) + break; /* end of list */ + if (!curent->en_flags.efl_invoiced) + return; + } +} + +void +kbillable() +{ +entry_t *en; +int anymarked = 0; + + TAILQ_FOREACH(en, &entries, en_entries) { + if (!en->en_flags.efl_marked) + continue; + anymarked = 1; + en->en_flags.efl_nonbillable = !en->en_flags.efl_nonbillable; + en->en_flags.efl_marked = 0; + } + + if (anymarked) { + save(); + return; + } + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + curent->en_flags.efl_nonbillable = !curent->en_flags.efl_nonbillable; + save(); + + if (bill_advance) + cursadvance(); +} + +void +keddesc() +{ +WCHAR *new; + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + if ((new = prompt(WIDE("Description:"), curent->en_desc, NULL)) == NULL) + return; + + free(curent->en_desc); + curent->en_desc = new; + save(); +} + +void +kedtime() +{ +WCHAR *new, old[64]; +time_t n; +int h, m, s; + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + n = curent->en_secs; + if (curent->en_started) + n += time(NULL) - curent->en_started; + h = n / (60 * 60); + n %= (60 * 60); + m = n / 60; + n %= 60; + s = n; + + SNPRINTF(old, WSIZEOF(old), WIDE("%02d:%02d:%02d"), h, m, s); + if ((new = prompt(WIDE("Duration [HH:MM:SS]:"), old, NULL)) == NULL) + return; + + if (!SSCANF(new, WIDE("%d:%d:%d"), &h, &m, &s)) { + free(new); + drawstatus(WIDE("Invalid duration.")); + } + + curent->en_secs = (h * 60 * 60) + (m * 60) + s; + if (curent->en_started) + time(&curent->en_started); + + save(); +} + +void +ktoggleinv() +{ +entry_t *en = curent; + showinv = !showinv; + drawstatus(WIDE("%"FMT_L"s invoiced entries."), + showinv ? L"Showing" : L"Hiding"); + + if (curent && !curent->en_flags.efl_invoiced) + return; + + if (!curent) { + curent = TAILQ_FIRST(&entries); + return; + } + + /* + * Try to find the next uninvoiced request to move the cursor to. + */ + for (;;) { + if ((curent = TAILQ_NEXT(curent, en_entries)) == NULL) + break; /* end of list */ + if (!curent->en_flags.efl_invoiced) + return; + } + + /* + * We didn't find any, so try searching backwards instead. + */ + for (curent = en;;) { + if ((curent = TAILQ_PREV(curent, entrylist, en_entries)) == NULL) + break; /* end of list */ + if (!curent->en_flags.efl_invoiced) + return; + } +} + +void +kcopy() +{ +entry_t *en; + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + en = entry_new(curent->en_desc); + curent = en; + entry_start(en); + save(); +} + +void +kaddtime() +{ +WCHAR *tstr; +int h = 0, m = 0, s = 0, secs; + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + if ((tstr = prompt(WIDE("Time to add ([[HH:]MM:]SS):"), NULL, NULL)) == NULL) + return; + + if (!*tstr) { + drawstatus(WIDE("")); + free(tstr); + return; + } + + 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; + } + } + } + + free(tstr); + + if (m >= 60) { + drawstatus(WIDE("Minutes cannot be more than 59.")); + return; + } + + if (s >= 60) { + drawstatus(WIDE("Seconds cannot be more than 59.")); + return; + } + + secs = s + m*60 + h*60*60; + curent->en_secs += secs; + save(); +} + +void +kdeltime() +{ +WCHAR *tstr; +int h = 0, m = 0, s = 0, secs; + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + if ((tstr = prompt(WIDE("Time to subtract, ([[HH:]MM:]SS):"), NULL, NULL)) == NULL) + return; + + if (!*tstr) { + drawstatus(WIDE("")); + free(tstr); + return; + } + + 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; + } + } + } + + free(tstr); + if (m >= 60) { + drawstatus(WIDE("Minutes cannot be more than 59.")); + return; + } + + if (s >= 60) { + drawstatus(WIDE("Seconds cannot be more than 59.")); + return; + } + + entry_account(curent); + + secs = s + m*60 + h*60*60; + if (curent->en_secs - secs < 0) { + drawstatus(WIDE("Remaining time cannot be less than zero.")); + return; + } + + curent->en_secs -= secs; + save(); +} + +void +kmerge() +{ +entry_t *en, *ten; +int nmarked = 0; +WCHAR pr[128]; +int h, m, s = 0; + + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + /* + * Count number of marked entries and the summed time. + */ + TAILQ_FOREACH(en, &entries, en_entries) { + if (!en->en_flags.efl_marked || en == curent) + continue; + nmarked++; + s += en->en_secs; + if (en->en_started) + s += time(NULL) - en->en_started; + } + + if (nmarked == 0) { + drawstatus(WIDE("No marked entries.")); + return; + } + + h = s / (60 * 60); + s %= (60 * 60); + m = s / 60; + s %= 60; + + SNPRINTF(pr, WSIZEOF(pr), WIDE("Merge %d marked entries [%02d:%02d:%02d] into current entry?"), + nmarked, h, m, s); + if (!yesno(pr)) + return; + + TAILQ_FOREACH_SAFE(en, &entries, en_entries, ten) { + if (!en->en_flags.efl_marked || en == curent) + continue; + if (en->en_started) + entry_stop(en); + curent->en_secs += en->en_secs; + TAILQ_REMOVE(&entries, en, en_entries); + entry_free(en); + } + save(); +} + +void +khelp() +{ +WINDOW *hwin; +size_t nhelp = 0; +WCHAR **help; +#define HTITLE WIDE(" TTS keys ") +size_t width = 0; +size_t i; +INT c; +binding_t *bi; + + /* Count the number of bindings */ + TAILQ_FOREACH(bi, &bindings, bi_entries) + nhelp++; + help = calloc(nhelp, sizeof(const WCHAR *)); + + i = 0; + TAILQ_FOREACH(bi, &bindings, bi_entries) { + WCHAR s[128]; + if (bi->bi_key) + SNPRINTF(s, WSIZEOF(s), WIDE("%-10"FMT_L"s %"FMT_L"s (%"FMT_L"s)"), + bi->bi_key->ky_name, bi->bi_func->fn_desc, + bi->bi_func->fn_name); + else + SNPRINTF(s, WSIZEOF(s), WIDE("%"FMT_L"c %"FMT_L"s (%"FMT_L"s)"), + bi->bi_code, bi->bi_func->fn_desc, bi->bi_func->fn_name); + help[i] = STRDUP(s); + i++; + } + + if (nhelp > (LINES - 6)) + nhelp = LINES - 6; + + for (i = 0; i < nhelp; i++) + if (STRLEN(help[i]) > width) + width = STRLEN(help[i]); + + hwin = newwin(nhelp + 4, width + 4, + (LINES / 2) - ((nhelp + 2) / 2), + (COLS / 2) - ((width + 2) / 2)); + wborder(hwin, 0, 0, 0, 0, 0, 0, 0, 0); + + wattron(hwin, A_REVERSE | A_BOLD); + wmove(hwin, 0, (width / 2) - (WSIZEOF(HTITLE) - 1)/2); + WADDSTR(hwin, HTITLE); + wattroff(hwin, A_REVERSE | A_BOLD); + + for (i = 0; i < nhelp; i++) { + wmove(hwin, i + 2, 2); + WADDSTR(hwin, help[i]); + } + + wrefresh(hwin); + + while (WGETCH(hwin, &c) == ERR +#ifdef KEY_RESIZE + || (c == KEY_RESIZE) +#endif + ) + ; + + delwin(hwin); + + for (i = 0; i < nhelp; i++) + free(help[i]); + free(help); +} + +void +kmark() +{ + if (!curent) { + drawstatus(WIDE("No entry selected.")); + return; + } + + curent->en_flags.efl_marked = !curent->en_flags.efl_marked; + + if (mark_advance) + cursadvance(); +} + +void +kunmarkall() +{ +entry_t *en; + TAILQ_FOREACH(en, &entries, en_entries) + en->en_flags.efl_marked = 0; +} + +void +kint() +{ +time_t duration; +entry_t *en; +WCHAR *name; + + if (!itime) { + if (!running) { + drawstatus(WIDE("No running entry.")); + return; + } + + itime = time(NULL); + return; + } + + if (!running) { + drawstatus(WIDE("No running entry.")); + return; + } + + name = prompt(WIDE("Description:"), NULL, NULL); + + if (!name || !*name) { + itime = 0; + free(name); + return; + } + + if (itime) { + duration = time(NULL) - itime; + } else { + int h, m, s; + if (prduration(WIDE("Duration [HH:MM:SS]:"), &h, &m, &s) == -1) + return; + + duration = (h * 60 * 60) + (m * 60) + s; + } + + itime = 0; + running->en_secs += (time(NULL) - running->en_started); + running->en_started = time(NULL); + running->en_secs -= duration; + + en = entry_new(name); + en->en_created = time(NULL) - duration; + en->en_secs = duration; + save(); + + free(name); +} + +void +ksearch() +{ +static WCHAR *lastsearch; +WCHAR *term; +entry_t *start, *cur; + + if (!curent) { + drawstatus(WIDE("No entries.")); + return; + } + + if ((term = prompt(WIDE("Search:"), NULL, NULL)) == NULL) + return; + + if (!*term) { + free(term); + if (!lastsearch) + return; + + term = lastsearch; + } else { + free(lastsearch); + lastsearch = term; + } + + cur = start = curent; + + for (;;) { + cur = TAILQ_NEXT(cur, en_entries); + if (cur == NULL) { + drawstatus(WIDE("Search reached last entry, continuing from top.")); + cur = TAILQ_FIRST(&entries); + } + + if (cur == start) { + drawstatus(WIDE("No matches.")); + break; + } + + if (STRSTR(cur->en_desc, term)) { + curent = cur; + if (!showinv && cur->en_flags.efl_invoiced) + showinv = 1; + return; + } + + } +} + +void +kexec() +{ +WCHAR *cmd; +WCHAR **args; +command_t *cmds; +size_t nargs; + + if ((cmd = prompt(WIDE(":"), NULL, NULL)) == NULL || !*cmd) { + free(cmd); + return; + } + + nargs = tokenise(cmd, &args); + free(cmd); + + if (nargs == 0) { + tokfree(&args); + return; + } + + if ((cmds = find_command(args[0])) == NULL) { + drawstatus(WIDE("Unknown command.")); + tokfree(&args); + return; + } + + cmds->cm_hdl(nargs, args); + tokfree(&args); +} + diff --git a/functions.h b/functions.h new file mode 100644 index 0000000..1ef87b2 --- /dev/null +++ b/functions.h @@ -0,0 +1,51 @@ +/* + * 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. + */ + +#ifndef TTS_FUNCTIONS_H +#define TTS_FUNCTIONS_H + +#include "wide.h" + +void kadd(void); +void kaddold(void); +void kquit(void); +void kup(void); +void kdown(void); +void ktoggle(void); +void kinvoiced(void); +void kbillable(void); +void keddesc(void); +void kedtime(void); +void ktoggleinv(void); +void kcopy(void); +void kaddtime(void); +void kdeltime(void); +void khelp(void); +void kmark(void); +void kunmarkall(void); +void ksearch(void); +void kmarkdel(void); +void kundel(void); +void ksync(void); +void kexec(void); +void kmerge(void); +void kint(void); + +typedef struct function { + const WCHAR *fn_name; + void (*fn_hdl) (void); + const WCHAR *fn_desc; +} function_t; + +extern function_t funcs[]; + +function_t *find_func(const WCHAR *name); + +#endif /* !TTS_FUNCTIONS_H */ diff --git a/str.c b/str.c new file mode 100644 index 0000000..4b89814 --- /dev/null +++ b/str.c @@ -0,0 +1,107 @@ +/* + * 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 "str.h" + +size_t +tokenise(str, res) + const WCHAR *str; + WCHAR ***res; +{ +int ntoks = 0; +const WCHAR *p, *q; + + *res = NULL; + p = str; + + for (;;) { + ptrdiff_t sz; + int qskip = 0; + + /* Skip leading whitespace */ + while (ISSPACE(*p)) + p++; + + /* End of string - no more arguments */ + if (!*p) + break; + + q = p; + + if (*q == '"') { + /* Quoted string - scan for end of string */ + int isbsl = 0; + p++; + + while (*++q) { + /* Handle escaping with backslash; currently works but the \ isn't + * removed from the string. + */ + if (*q == '\\') { + isbsl = 1; + continue; + } + + if (!isbsl && (*q == '"')) + break; + + isbsl = 0; + } + /* At this point, *q == '"'. If it's NUL instead, then the + * string was not terminated with a closing '"' before the end + * of the line. We could give an error here, but it seems + * more useful to just accept it. + */ + if (*q == '"') + qskip = 1; + } else { + /* Not quoted - just find the next whitespace */ + while (!ISSPACE(*q) && *q) + q++; + } + + /* Copy the argument (which is sz bytes long) into the result array */ + sz = (q - p); + *res = realloc(*res, sizeof(WCHAR *) * (ntoks + 1)); + (*res)[ntoks] = malloc(sizeof(WCHAR) * (sz + 1)); + MEMCPY((*res)[ntoks], p, sz); + (*res)[ntoks][sz] = 0; + ntoks++; + + if (qskip) + q += qskip; + + while (ISSPACE(*q)) + q++; + + /* + * q is the start of the next token (with leading whitespace); reset + * p to process the next argument. + */ + if (!*q) + break; + p = q; + } + + *res = realloc(*res, sizeof(WCHAR *) * (ntoks + 1)); + (*res)[ntoks] = NULL; + return ntoks; +} + +void +tokfree(vec) + WCHAR ***vec; +{ +WCHAR **p; + for (p = (*vec); *p; p++) + free(*p); + free(*vec); +} + diff --git a/str.h b/str.h new file mode 100644 index 0000000..c7b88e6 --- /dev/null +++ b/str.h @@ -0,0 +1,21 @@ +/* + * 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. + */ + +#ifndef TTS_STR_H +#define TTS_STR_H + +#include + +#include "wide.h" + +size_t tokenise(const WCHAR *, WCHAR ***result); +void tokfree(WCHAR ***); + +#endif /* !TTS_STR_H */ diff --git a/style.h b/style.h new file mode 100644 index 0000000..9afac1b --- /dev/null +++ b/style.h @@ -0,0 +1,50 @@ +/* + * 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. + */ + +#ifndef TTS_STYLE_H +#define TTS_STYLE_H + +#include "tts_curses.h" + +typedef struct style { + short sy_pair; + attr_t sy_attrs; +} style_t; + +#define style_fg(s) (COLOR_PAIR((s).sy_pair) | (s).sy_attrs) +#define style_bg(s) ((INT) ' ' | COLOR_PAIR((s).sy_pair) | ((s).sy_attrs & ~WA_UNDERLINE)) + +typedef struct attrname { + const WCHAR *an_name; + attr_t an_value; +} attrname_t; + +typedef struct colour { + const WCHAR *co_name; + short co_value; +} colour_t; + +extern style_t sy_header, + sy_status, + sy_entry, + sy_running, + sy_selected, + sy_date; + +int attr_find(const WCHAR *name, attr_t *result); +int colour_find(const WCHAR *name, short *result); + +void style_clear(style_t *); +int style_set(style_t *, const WCHAR *fg, const WCHAR *bg); +int style_add(style_t *, const WCHAR *fg, const WCHAR *bg); + +void apply_styles(void); + +#endif /* !TTS_STYLE_H */ diff --git a/tts.c b/tts.c index 495e385..b6da855 100644 --- a/tts.c +++ b/tts.c @@ -8,15 +8,6 @@ * warranty. */ -#define __EXTENSIONS__ -/* - * Older versions of glibc don't supporte _XOPEN_SOURCE==700 and require - * this for wcsdup() prototype. - */ -#define _GNU_SOURCE -#define _XOPEN_SOURCE 700 -#define _XOPEN_SOURCE_EXTENDED - #include #include @@ -52,244 +43,38 @@ # include #endif -#if defined HAVE_NCURSESW_CURSES_H -# include -#elif defined HAVE_NCURSESW_H -# include -#elif defined HAVE_NCURSES_CURSES_H -# include -#elif defined HAVE_NCURSES_H -# include -#elif defined HAVE_CURSES_H -# include -#else -# error "SVR4 or XSI compatible curses header file required" -#endif - #include "queue.h" - -#ifdef HAVE_CURSES_ENHANCED -# define WPFX(x) wcs##x -# define WIDE(x) L##x -# define ISX(x) isw##x -# define WCHAR wchar_t -# define FMT_L "l" -# define SNPRINTF swprintf -# define VSNPRINTF vswprintf -# define SSCANF swscanf -# define MEMCPY wmemcpy -# define MEMMOVE wmemmove -# define MBSTOWCS mbstowcs -# define WCSTOMBS wcstombs -# define FPRINTF fwprintf -# define STRTOK wcstok - -# define GETCH get_wch -# define WGETCH wget_wch -# define ADDSTR addwstr -# define WADDSTR waddwstr -# define INT wint_t -#else -# define WPFX(x) str##x -# define WIDE(x) x -# define ISX(x) is##x -# define WCHAR char -# define FMT_L -# define SNPRINTF snprintf -# define VSNPRINTF vsnprintf -# define SSCANF sscanf -# define MEMCPY memcpy -# define MEMMOVE memmove -# define MBSTOWCS strncpy -# define WCSTOMBS strncpy -# define FPRINTF fprintf -# define STRTOK strtok_r - -# define ADDSTR addstr -# define WADDSTR waddstr -# define INT int - -static int -tss_wgetch(win, d) - WINDOW *win; - int *d; -{ -int c; - if ((c = wgetch(win)) == ERR) - return ERR; - *d = c; - return OK; -} -# define WGETCH tss_wgetch -# define GETCH(c) tss_wgetch(stdscr,c) -#endif - -#define STRLEN WPFX(len) -#define STRCMP WPFX(cmp) -#define STRNCMP WPFX(ncmp) -#define STRCPY WPFX(cpy) -#define STRNCPY WPFX(ncpy) -#define STRSTR WPFX(str) -#define STRFTIME WPFX(ftime) -#define STRDUP WPFX(dup) -#define STRTOL WPFX(tol) - -#define ISSPACE ISX(space) - -#define WSIZEOF(s) (sizeof(s) / sizeof(WCHAR)) +#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" extern char const *tts_version; -static volatile sig_atomic_t doexit; - -static WINDOW *titwin, *statwin, *listwin; - -static int in_curses; - -static void drawstatus(const WCHAR *msg, ...); -static void vdrawstatus(const WCHAR *msg, va_list); -static void drawheader(void); -static void drawentries(void); +volatile sig_atomic_t doexit; static time_t laststatus; -typedef struct entry { - WCHAR *en_desc; - int en_secs; - time_t en_started; - time_t en_created; - - struct { - int efl_visible:1; - int efl_invoiced:1; - int efl_marked:1; - int efl_deleted:1; - int efl_nonbillable:1; - } en_flags; - TAILQ_ENTRY(entry) en_entries; -} entry_t; - -static TAILQ_HEAD(entrylist, entry) entries = TAILQ_HEAD_INITIALIZER(entries); - -static entry_t *running; - -static entry_t *entry_new(const WCHAR *); -static void entry_start(entry_t *); -static void entry_stop(entry_t *); -static void entry_free(entry_t *); -static void entry_account(entry_t *); -static time_t entry_time_for_day(time_t, int, int); - -#define time_day(t) (((t) / (60 * 60 * 24)) * (60 * 60 * 24)) -#define entry_day(e) (time_day((e)->en_created)) - -#define time_to_hms(t, h, m, s) do { \ - time_t n = t; \ - h = n / (60 * 60); \ - n %= (60 * 60); \ - m = n / 60; \ - n %= 60; \ - s = n; \ - } while (0) - -#define NHIST 50 -typedef struct histent { - WCHAR *he_text; - TAILQ_ENTRY(histent) he_entries; -} histent_t; -typedef TAILQ_HEAD(hentlist, histent) hentlist_t; - -typedef struct history { - int hi_nents; - hentlist_t hi_ents; -} history_t; - -static history_t *hist_new(void); -static void hist_add(history_t *, WCHAR const *); - -static history_t *searchhist, *prompthist; - -static WCHAR *prompt(WCHAR const *, WCHAR const *, history_t *); -static int prduration(WCHAR *prompt, int *h, int *m, int *s); -static int yesno(WCHAR const *); -static void errbox(WCHAR const *, ...); -static void verrbox(WCHAR const *, va_list); +history_t *searchhist; +history_t *prompthist; #define STATFILE ".rttts" #define RCFILE ".ttsrc" -static int load(void); -static int save(void); +int load(void); +int save(void); static time_t lastsave; static char statfile[PATH_MAX + 1]; static int load_file(const char *); -static void cursadvance(void); - -static void kadd(void); -static void kaddold(void); -static void kquit(void); -static void kup(void); -static void kdown(void); -static void ktoggle(void); -static void kinvoiced(void); -static void kbillable(void); -static void keddesc(void); -static void kedtime(void); -static void ktoggleinv(void); -static void kcopy(void); -static void kaddtime(void); -static void kdeltime(void); -static void khelp(void); -static void kmark(void); -static void kunmarkall(void); -static void ksearch(void); -static void kmarkdel(void); -static void kundel(void); -static void ksync(void); -static void kexec(void); -static void kmerge(void); -static void kint(void); - -typedef struct function { - const WCHAR *fn_name; - void (*fn_hdl) (void); - const WCHAR *fn_desc; -} function_t; - -static function_t funcs[] = { - { WIDE("help"), khelp, WIDE("display help screen") }, - { WIDE("add"), kadd, WIDE("add a new entry and start the timer") }, - { WIDE("add-old"), kaddold, WIDE("add a new entry and specify its duration") }, - { WIDE("delete"), kmarkdel, WIDE("delete the current entry") }, - { WIDE("undelete"), kundel, WIDE("undelete the current entry") }, - { WIDE("quit"), kquit, WIDE("exit TTS") }, - { WIDE("invoice"), kinvoiced, WIDE("toggle current entry as invoiced") }, - { WIDE("billable"), kbillable, WIDE("toggle current entry as billable") }, - { WIDE("mark"), kmark, WIDE("mark the current entry") }, - { WIDE("unmarkall"), kunmarkall, WIDE("unmark all entries") }, - { WIDE("startstop"), ktoggle, WIDE("start or stop the timer") }, - { WIDE("edit-desc"), keddesc, WIDE("edit the current entry's description") }, - { WIDE("edit-time"), kedtime, WIDE("edit the current entry's duration") }, - { WIDE("showhide-inv"), ktoggleinv, WIDE("show or hide invoiced entries") }, - { WIDE("copy"), kcopy, WIDE("copy the current entry's description to a new entry") }, - { WIDE("add-time"), kaddtime, WIDE("add time to the current entry") }, - { WIDE("sub-time"), kdeltime, WIDE("subtract time from the current entry") }, - { WIDE("search"), ksearch, WIDE("search for an entry by name") }, - { WIDE("sync"), ksync, WIDE("purge all deleted entries") }, - { WIDE("prev"), kup, WIDE("move to the previous entry") }, - { WIDE("next"), kdown, WIDE("move to the next entry") }, - { WIDE("execute"), kexec, WIDE("execute a configuration command") }, - { WIDE("merge"), kmerge, WIDE("merge marked entries into current entry") }, - { WIDE("interrupt"), kint, WIDE("split current entry into new entry")}, -}; - -typedef struct tkey { - INT ky_code; - const WCHAR *ky_name; -} tkey_t; - static tkey_t keys[] = { { KEY_BREAK, WIDE("") }, { KEY_DOWN, WIDE("") }, @@ -358,58 +143,10 @@ static tkey_t keys[] = { { KEY_DC, WIDE("") } }; -typedef struct binding { - INT bi_code; - tkey_t *bi_key; - function_t *bi_func; - TAILQ_ENTRY(binding) bi_entries; -} binding_t; +int pagestart; +entry_t *curent; -typedef struct command { - const WCHAR *cm_name; - void (*cm_hdl) (size_t, WCHAR **); -} command_t; - -static command_t *find_command(const WCHAR *); -static void c_bind(size_t, WCHAR **); -static void c_style(size_t, WCHAR **); -static void c_set(size_t, WCHAR **); - -static command_t commands[] = { - { WIDE("bind"), c_bind }, - { WIDE("style"), c_style }, - { WIDE("set"), c_set }, -}; - -static void cmderr(const WCHAR *, ...); -static void vcmderr(const WCHAR *, va_list); - -static size_t tokenise(const WCHAR *, WCHAR ***result); -static void tokfree(WCHAR ***); - -static TAILQ_HEAD(bindlist, binding) bindings = TAILQ_HEAD_INITIALIZER(bindings); - -static tkey_t *find_key(const WCHAR *name); -static function_t *find_func(const WCHAR *name); -static void bind_key(const WCHAR *key, const WCHAR *func); - -static int pagestart; -static entry_t *curent; - -static int showinv = 0; - -typedef struct style { - short sy_pair; - attr_t sy_attrs; -} style_t; - -#define style_fg(s) (COLOR_PAIR((s).sy_pair) | (s).sy_attrs) -#define style_bg(s) ((INT) ' ' | COLOR_PAIR((s).sy_pair) | ((s).sy_attrs & ~WA_UNDERLINE)) - -typedef struct attrname { - const WCHAR *an_name; - attr_t an_value; -} attrname_t; +int showinv = 0; static attrname_t attrnames[] = { { WIDE("normal"), WA_NORMAL }, @@ -421,11 +158,6 @@ static attrname_t attrnames[] = { { WIDE("standout"), WA_STANDOUT } }; -typedef struct colour { - const WCHAR *co_name; - short co_value; -} colour_t; - static colour_t colours[] = { { WIDE("black"), COLOR_BLACK }, { WIDE("red"), COLOR_RED }, @@ -437,55 +169,34 @@ static colour_t colours[] = { { WIDE("white"), COLOR_WHITE } }; -static int attr_find(const WCHAR *name, attr_t *result); -static int colour_find(const WCHAR *name, short *result); - -static void style_clear(style_t *); -static int style_set(style_t *, const WCHAR *fg, const WCHAR *bg); -static int style_add(style_t *, const WCHAR *fg, const WCHAR *bg); - static short default_fg, default_bg; -static void apply_styles(void); +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 }; -static 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; -static 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; -static int show_billable = 0; -static int delete_advance = 1; -static int mark_advance = 1; -static int bill_advance = 0; -static int bill_increment = 0; -static WCHAR *auto_nonbillable; - -#define VTYPE_INT 1 -#define VTYPE_BOOL 2 -#define VTYPE_STRING 3 - -typedef struct variable { - WCHAR const *va_name; - int va_type; - void *va_addr; -} variable_t; - -static variable_t variables[] = { +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 } + { WIDE("bill_increment"), VTYPE_INT, &bill_increment }, + { } }; -static variable_t *find_variable(const WCHAR *name); - #ifdef USE_DARWIN_POWER static pthread_t power_thread; static io_connect_t root_port; @@ -751,805 +462,6 @@ char rcfile[PATH_MAX + 1]; return 0; } -void -kquit() -{ -entry_t *en; -int ndel = 0; - - TAILQ_FOREACH(en, &entries, en_entries) { - if (en->en_flags.efl_deleted) - ndel++; - } - - if (ndel) { - WCHAR s[128]; - SNPRINTF(s, WSIZEOF(s), WIDE("Purge %d deleted entries?"), ndel); - if (yesno(s)) { - ksync(); - } - } - - doexit = 1; -} - -void -kadd() -{ -WCHAR *name; -entry_t *en; - name = prompt(WIDE("Description:"), NULL, NULL); - if (!name || !*name) { - free(name); - return; - } - en = entry_new(name); - entry_start(en); - curent = en; - save(); -} - -void -kaddold() -{ -WCHAR *name; -entry_t *en; - name = prompt(WIDE("Description:"), NULL, NULL); - - if (!name || !*name) { - free(name); - return; - } - - en = entry_new(name); - curent = en; - kedtime(); - save(); -} - -void -ktoggle() -{ - itime = 0; - - if (!curent) - return; - - if (curent == running) { - entry_stop(curent); - save(); - return; - } - - if (running) - entry_stop(running); - entry_start(curent); - save(); -} - -void -kundel() -{ - if (!curent) - return; - - curent->en_flags.efl_deleted = 0; - if (delete_advance) - cursadvance(); -} - -void -kmarkdel() -{ -entry_t *en; -int nmarked = 0; - - TAILQ_FOREACH(en, &entries, en_entries) { - if (en->en_flags.efl_marked) { - nmarked++; - en->en_flags.efl_deleted = 1; - } - } - - if (nmarked) - return; - - if (!curent) { - drawstatus(WIDE("No entries to delete.")); - return; - } - - curent->en_flags.efl_deleted = 1; - - if (delete_advance) - cursadvance(); -} - -/* - * Move the cursor to the next entry after an operation like mark or deleted. - * If there are no suitable entries after this one, move it backwards instead. - */ -void -cursadvance() -{ -entry_t *en; - - if (!curent) { - curent = TAILQ_FIRST(&entries); - return; - } - - /* - * Try to find the next suitable entry to move the cursor to. - */ - for (en = TAILQ_NEXT(curent, en_entries); en; en = TAILQ_NEXT(en, en_entries)) { - if (!showinv && en->en_flags.efl_invoiced) - continue; - curent = en; - if (!curent->en_flags.efl_visible) - pagestart++; - return; - } - - /* - * No entries; if the current entry is visible, stay here, otherwise - * try moving backwards instead. - */ - if (showinv || !curent->en_flags.efl_invoiced) - return; - - for (en = TAILQ_PREV(curent, entrylist, en_entries); en; - en = TAILQ_PREV(en, entrylist, en_entries)) { - if (!showinv && en->en_flags.efl_invoiced) - continue; - curent = en; - if (!curent->en_flags.efl_visible) - pagestart--; - return; - } - - /* - * Couldn't find any entries at all? - */ - curent = NULL; -} - -void -ksync() -{ -entry_t *en, *ten; - - TAILQ_FOREACH_SAFE(en, &entries, en_entries, ten) { - if (!en->en_flags.efl_deleted) - continue; - if (en == curent) - curent = NULL; - TAILQ_REMOVE(&entries, en, en_entries); - entry_free(en); - } - - if (curent == NULL) - curent = TAILQ_FIRST(&entries); - save(); -} - -void -kup() -{ -entry_t *prev = curent; - if (!curent) - return; - - do { - if ((prev = TAILQ_PREV(prev, entrylist, en_entries)) == NULL) - break; - } while (!showinv && prev->en_flags.efl_invoiced); - - if (prev == NULL) { - drawstatus(WIDE("Already at first entry.")); - return; - } - - curent = prev; - if (!curent->en_flags.efl_visible) - pagestart--; -} - -void -kdown() -{ -entry_t *next = curent; - if (!curent) - return; - - do { - if ((next = TAILQ_NEXT(next, en_entries)) == NULL) - break; - } while (!showinv && next->en_flags.efl_invoiced); - - if (next == NULL) { - drawstatus(WIDE("Already at last entry.")); - return; - } - - curent = next; - if (!curent->en_flags.efl_visible) - pagestart++; -} - -void -kinvoiced() -{ -entry_t *en; -int anymarked = 0; - - TAILQ_FOREACH(en, &entries, en_entries) { - if (!en->en_flags.efl_marked) - continue; - anymarked = 1; - en->en_flags.efl_invoiced = !en->en_flags.efl_invoiced; - en->en_flags.efl_marked = 0; - } - - if (anymarked) { - save(); - return; - } - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - curent->en_flags.efl_invoiced = !curent->en_flags.efl_invoiced; - save(); - - en = curent; - - if (showinv) { - if (TAILQ_NEXT(curent, en_entries) != NULL) - curent = TAILQ_NEXT(curent, en_entries); - return; - } - - /* - * Try to find the next uninvoiced request to move the cursor to. - */ - for (;;) { - if ((curent = TAILQ_NEXT(curent, en_entries)) == NULL) - break; /* end of list */ - if (!curent->en_flags.efl_invoiced) - return; - } - - /* - * We didn't find any, so try searching backwards instead. - */ - for (curent = en;;) { - if ((curent = TAILQ_PREV(curent, entrylist, en_entries)) == NULL) - break; /* end of list */ - if (!curent->en_flags.efl_invoiced) - return; - } -} - -void -kbillable() -{ -entry_t *en; -int anymarked = 0; - - TAILQ_FOREACH(en, &entries, en_entries) { - if (!en->en_flags.efl_marked) - continue; - anymarked = 1; - en->en_flags.efl_nonbillable = !en->en_flags.efl_nonbillable; - en->en_flags.efl_marked = 0; - } - - if (anymarked) { - save(); - return; - } - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - curent->en_flags.efl_nonbillable = !curent->en_flags.efl_nonbillable; - save(); - - if (bill_advance) - cursadvance(); -} - -void -keddesc() -{ -WCHAR *new; - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - if ((new = prompt(WIDE("Description:"), curent->en_desc, NULL)) == NULL) - return; - - free(curent->en_desc); - curent->en_desc = new; - save(); -} - -void -kedtime() -{ -WCHAR *new, old[64]; -time_t n; -int h, m, s; - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - n = curent->en_secs; - if (curent->en_started) - n += time(NULL) - curent->en_started; - h = n / (60 * 60); - n %= (60 * 60); - m = n / 60; - n %= 60; - s = n; - - SNPRINTF(old, WSIZEOF(old), WIDE("%02d:%02d:%02d"), h, m, s); - if ((new = prompt(WIDE("Duration [HH:MM:SS]:"), old, NULL)) == NULL) - return; - - if (!SSCANF(new, WIDE("%d:%d:%d"), &h, &m, &s)) { - free(new); - drawstatus(WIDE("Invalid duration.")); - } - - curent->en_secs = (h * 60 * 60) + (m * 60) + s; - if (curent->en_started) - time(&curent->en_started); - - save(); -} - -void -ktoggleinv() -{ -entry_t *en = curent; - showinv = !showinv; - drawstatus(WIDE("%"FMT_L"s invoiced entries."), - showinv ? L"Showing" : L"Hiding"); - - if (curent && !curent->en_flags.efl_invoiced) - return; - - if (!curent) { - curent = TAILQ_FIRST(&entries); - return; - } - - /* - * Try to find the next uninvoiced request to move the cursor to. - */ - for (;;) { - if ((curent = TAILQ_NEXT(curent, en_entries)) == NULL) - break; /* end of list */ - if (!curent->en_flags.efl_invoiced) - return; - } - - /* - * We didn't find any, so try searching backwards instead. - */ - for (curent = en;;) { - if ((curent = TAILQ_PREV(curent, entrylist, en_entries)) == NULL) - break; /* end of list */ - if (!curent->en_flags.efl_invoiced) - return; - } -} - -void -kcopy() -{ -entry_t *en; - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - en = entry_new(curent->en_desc); - curent = en; - entry_start(en); - save(); -} - -void -kaddtime() -{ -WCHAR *tstr; -int h = 0, m = 0, s = 0, secs; - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - if ((tstr = prompt(WIDE("Time to add ([[HH:]MM:]SS):"), NULL, NULL)) == NULL) - return; - - if (!*tstr) { - drawstatus(WIDE("")); - free(tstr); - return; - } - - 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; - } - } - } - - free(tstr); - - if (m >= 60) { - drawstatus(WIDE("Minutes cannot be more than 59.")); - return; - } - - if (s >= 60) { - drawstatus(WIDE("Seconds cannot be more than 59.")); - return; - } - - secs = s + m*60 + h*60*60; - curent->en_secs += secs; - save(); -} - -void -kdeltime() -{ -WCHAR *tstr; -int h = 0, m = 0, s = 0, secs; - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - if ((tstr = prompt(WIDE("Time to subtract, ([[HH:]MM:]SS):"), NULL, NULL)) == NULL) - return; - - if (!*tstr) { - drawstatus(WIDE("")); - free(tstr); - return; - } - - 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; - } - } - } - - free(tstr); - if (m >= 60) { - drawstatus(WIDE("Minutes cannot be more than 59.")); - return; - } - - if (s >= 60) { - drawstatus(WIDE("Seconds cannot be more than 59.")); - return; - } - - entry_account(curent); - - secs = s + m*60 + h*60*60; - if (curent->en_secs - secs < 0) { - drawstatus(WIDE("Remaining time cannot be less than zero.")); - return; - } - - curent->en_secs -= secs; - save(); -} - -void -kmerge() -{ -entry_t *en, *ten; -int nmarked = 0; -WCHAR pr[128]; -int h, m, s = 0; - - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - /* - * Count number of marked entries and the summed time. - */ - TAILQ_FOREACH(en, &entries, en_entries) { - if (!en->en_flags.efl_marked || en == curent) - continue; - nmarked++; - s += en->en_secs; - if (en->en_started) - s += time(NULL) - en->en_started; - } - - if (nmarked == 0) { - drawstatus(WIDE("No marked entries.")); - return; - } - - h = s / (60 * 60); - s %= (60 * 60); - m = s / 60; - s %= 60; - - SNPRINTF(pr, WSIZEOF(pr), WIDE("Merge %d marked entries [%02d:%02d:%02d] into current entry?"), - nmarked, h, m, s); - if (!yesno(pr)) - return; - - TAILQ_FOREACH_SAFE(en, &entries, en_entries, ten) { - if (!en->en_flags.efl_marked || en == curent) - continue; - if (en->en_started) - entry_stop(en); - curent->en_secs += en->en_secs; - TAILQ_REMOVE(&entries, en, en_entries); - entry_free(en); - } - save(); -} - -void -khelp() -{ -WINDOW *hwin; -size_t nhelp = 0; -WCHAR **help; -#define HTITLE WIDE(" TTS keys ") -size_t width = 0; -size_t i; -INT c; -binding_t *bi; - /* Count the number of bindings */ - TAILQ_FOREACH(bi, &bindings, bi_entries) - nhelp++; - help = calloc(nhelp, sizeof(const WCHAR *)); - - i = 0; - TAILQ_FOREACH(bi, &bindings, bi_entries) { - WCHAR s[128]; - if (bi->bi_key) - SNPRINTF(s, WSIZEOF(s), WIDE("%-10"FMT_L"s %"FMT_L"s (%"FMT_L"s)"), - bi->bi_key->ky_name, bi->bi_func->fn_desc, - bi->bi_func->fn_name); - else - SNPRINTF(s, WSIZEOF(s), WIDE("%"FMT_L"c %"FMT_L"s (%"FMT_L"s)"), - bi->bi_code, bi->bi_func->fn_desc, bi->bi_func->fn_name); - help[i] = STRDUP(s); - i++; - } - - if (nhelp > (LINES - 6)) - nhelp = LINES - 6; - - for (i = 0; i < nhelp; i++) - if (STRLEN(help[i]) > width) - width = STRLEN(help[i]); - - hwin = newwin(nhelp + 4, width + 4, - (LINES / 2) - ((nhelp + 2) / 2), - (COLS / 2) - ((width + 2) / 2)); - wborder(hwin, 0, 0, 0, 0, 0, 0, 0, 0); - - wattron(hwin, A_REVERSE | A_BOLD); - wmove(hwin, 0, (width / 2) - (WSIZEOF(HTITLE) - 1)/2); - WADDSTR(hwin, HTITLE); - wattroff(hwin, A_REVERSE | A_BOLD); - - for (i = 0; i < nhelp; i++) { - wmove(hwin, i + 2, 2); - WADDSTR(hwin, help[i]); - } - - wrefresh(hwin); - - while (WGETCH(hwin, &c) == ERR -#ifdef KEY_RESIZE - || (c == KEY_RESIZE) -#endif - ) - ; - - delwin(hwin); - - for (i = 0; i < nhelp; i++) - free(help[i]); - free(help); -} - -void -kmark() -{ - if (!curent) { - drawstatus(WIDE("No entry selected.")); - return; - } - - curent->en_flags.efl_marked = !curent->en_flags.efl_marked; - - if (mark_advance) - cursadvance(); -} - -void -kunmarkall() -{ -entry_t *en; - TAILQ_FOREACH(en, &entries, en_entries) - en->en_flags.efl_marked = 0; -} - -void -kint() -{ -time_t duration; -entry_t *en; -WCHAR *name; - - if (!itime) { - if (!running) { - drawstatus(WIDE("No running entry.")); - return; - } - - itime = time(NULL); - return; - } - - if (!running) { - drawstatus(WIDE("No running entry.")); - return; - } - - name = prompt(WIDE("Description:"), NULL, NULL); - - if (!name || !*name) { - itime = 0; - free(name); - return; - } - - if (itime) { - duration = time(NULL) - itime; - } else { - int h, m, s; - if (prduration(WIDE("Duration [HH:MM:SS]:"), &h, &m, &s) == -1) - return; - - duration = (h * 60 * 60) + (m * 60) + s; - } - - itime = 0; - running->en_secs += (time(NULL) - running->en_started); - running->en_started = time(NULL); - running->en_secs -= duration; - - en = entry_new(name); - en->en_created = time(NULL) - duration; - en->en_secs = duration; - save(); - - free(name); -} - -void -ksearch() -{ -static WCHAR *lastsearch; -WCHAR *term; -entry_t *start, *cur; - - if (!curent) { - drawstatus(WIDE("No entries.")); - return; - } - - if ((term = prompt(WIDE("Search:"), NULL, NULL)) == NULL) - return; - - if (!*term) { - free(term); - if (!lastsearch) - return; - - term = lastsearch; - } else { - free(lastsearch); - lastsearch = term; - } - - cur = start = curent; - - for (;;) { - cur = TAILQ_NEXT(cur, en_entries); - if (cur == NULL) { - drawstatus(WIDE("Search reached last entry, continuing from top.")); - cur = TAILQ_FIRST(&entries); - } - - if (cur == start) { - drawstatus(WIDE("No matches.")); - break; - } - - if (STRSTR(cur->en_desc, term)) { - curent = cur; - if (!showinv && cur->en_flags.efl_invoiced) - showinv = 1; - return; - } - - } -} - -void -kexec() -{ -WCHAR *cmd; -WCHAR **args; -command_t *cmds; -size_t nargs; - - if ((cmd = prompt(WIDE(":"), NULL, NULL)) == NULL || !*cmd) { - free(cmd); - return; - } - - nargs = tokenise(cmd, &args); - free(cmd); - - if (nargs == 0) { - tokfree(&args); - return; - } - - if ((cmds = find_command(args[0])) == NULL) { - drawstatus(WIDE("Unknown command.")); - tokfree(&args); - return; - } - - cmds->cm_hdl(nargs, args); - tokfree(&args); -} - void drawheader() { @@ -1982,107 +894,6 @@ chtype oldbg; wbkgdset(listwin, oldbg); } -entry_t * -entry_new(desc) - const WCHAR *desc; -{ -entry_t *en; - if ((en = calloc(1, sizeof(*en))) == NULL) - return NULL; - - if (auto_nonbillable && STRSTR(desc, auto_nonbillable)) - en->en_flags.efl_nonbillable = 1; - - TAILQ_INSERT_HEAD(&entries, en, en_entries); - - en->en_desc = STRDUP(desc); - time(&en->en_created); - - return en; -} - -void -entry_start(en) - entry_t *en; -{ - if (running) - entry_stop(running); - time(&en->en_started); - running = en; -} - -void -entry_stop(en) - entry_t *en; -{ - if (running == en) - running = NULL; - en->en_secs += time(NULL) - en->en_started; - en->en_started = 0; -} - -void -entry_free(en) - entry_t *en; -{ - if (en == running) - entry_stop(en); - free(en->en_desc); -} - -void -entry_account(en) - entry_t *en; -{ - if (!en->en_started) - return; - en->en_secs += time(NULL) - en->en_started; - time(&en->en_started); -} - -/* - * Return the amount of time for the day on which the timestamp .when falls. - * If .inv is 0, sum non-invoiced entries; if 1, sum invoiced entries; if - * 2, sum billable entries; if -1, sum all entries. If .incr is non-zero, - * individual entry time will be rounded up to intervals of that many minutes. - */ -time_t -entry_time_for_day(when, inv, incr) - time_t when; -{ -time_t day = time_day(when); -time_t sum = 0; -entry_t *en; -int rnd = incr * 60; - TAILQ_FOREACH(en, &entries, en_entries) { - time_t n; - - if (entry_day(en) > day) - continue; - if (entry_day(en) < day) - break; - - if (inv == 0 && en->en_flags.efl_invoiced) - continue; - if (inv == 1 && !en->en_flags.efl_invoiced) - continue; - if (inv == 2 && en->en_flags.efl_nonbillable) - continue; - - n = en->en_secs; - if (en->en_started) - n += time(NULL) - en->en_started; - - if (!n) - continue; - - if (rnd) - n = (1 + round((n - 1) / rnd)) * rnd; - sum += n; - } - return sum; -} - int load() { @@ -2368,10 +1179,10 @@ function_t * find_func(name) const WCHAR *name; { -size_t i; - for (i = 0; i < sizeof(funcs) / sizeof(*funcs); i++) - if (STRCMP(name, funcs[i].fn_name) == 0) - return &funcs[i]; +function_t *f; + for (f = funcs; f->fn_name; f++) + if (STRCMP(name, f->fn_name) == 0) + return f; return NULL; } @@ -2424,223 +1235,17 @@ INT code; TAILQ_INSERT_TAIL(&bindings, binding, bi_entries); } -size_t -tokenise(str, res) - const WCHAR *str; - WCHAR ***res; -{ -int ntoks = 0; -const WCHAR *p, *q; - - *res = NULL; - p = str; - - for (;;) { - ptrdiff_t sz; - int qskip = 0; - - /* Skip leading whitespace */ - while (ISSPACE(*p)) - p++; - - /* End of string - no more arguments */ - if (!*p) - break; - - q = p; - - if (*q == '"') { - /* Quoted string - scan for end of string */ - int isbsl = 0; - p++; - - while (*++q) { - /* Handle escaping with backslash; currently works but the \ isn't - * removed from the string. - */ - if (*q == '\\') { - isbsl = 1; - continue; - } - - if (!isbsl && (*q == '"')) - break; - - isbsl = 0; - } - /* At this point, *q == '"'. If it's NUL instead, then the - * string was not terminated with a closing '"' before the end - * of the line. We could give an error here, but it seems - * more useful to just accept it. - */ - if (*q == '"') - qskip = 1; - } else { - /* Not quoted - just find the next whitespace */ - while (!ISSPACE(*q) && *q) - q++; - } - - /* Copy the argument (which is sz bytes long) into the result array */ - sz = (q - p); - *res = realloc(*res, sizeof(WCHAR *) * (ntoks + 1)); - (*res)[ntoks] = malloc(sizeof(WCHAR) * (sz + 1)); - MEMCPY((*res)[ntoks], p, sz); - (*res)[ntoks][sz] = 0; - ntoks++; - - if (qskip) - q += qskip; - - while (ISSPACE(*q)) - q++; - - /* - * q is the start of the next token (with leading whitespace); reset - * p to process the next argument. - */ - if (!*q) - break; - p = q; - } - - *res = realloc(*res, sizeof(WCHAR *) * (ntoks + 1)); - (*res)[ntoks] = NULL; - return ntoks; -} - -void -tokfree(vec) - WCHAR ***vec; -{ -WCHAR **p; - for (p = (*vec); *p; p++) - free(*p); - free(*vec); -} - -command_t * -find_command(name) - const WCHAR *name; -{ -size_t i; - for (i = 0; i < sizeof(commands) / sizeof(*commands); i++) - if (STRCMP(name, commands[i].cm_name) == 0) - return &commands[i]; - return NULL; -} - -void -c_style(argc, argv) - size_t argc; - WCHAR **argv; -{ -style_t *sy; -WCHAR *last, *tok; - - if (argc < 3 || argc > 4) { - cmderr(WIDE("Usage: style [background]")); - return; - } - - if (STRCMP(argv[1], WIDE("header")) == 0) - sy = &sy_header; - else if (STRCMP(argv[1], WIDE("status")) == 0) - sy = &sy_status; - else if (STRCMP(argv[1], WIDE("entry")) == 0) - sy = &sy_entry; - else if (STRCMP(argv[1], WIDE("selected")) == 0) - sy = &sy_selected; - else if (STRCMP(argv[1], WIDE("running")) == 0) - sy = &sy_running; - else if (STRCMP(argv[1], WIDE("date")) == 0) - sy = &sy_date; - else { - cmderr(WIDE("Unknown style item.")); - return; - } - - style_clear(sy); - for (tok = STRTOK(argv[2], WIDE(","), &last); tok != NULL; - tok = STRTOK(NULL, WIDE(","), &last)) { - style_add(sy, tok, argv[3]); - } - - apply_styles(); -} - -void -c_bind(argc, argv) - size_t argc; - WCHAR **argv; -{ - if (argc != 3) { - cmderr(WIDE("Usage: bind ")); - return; - } - - bind_key(argv[1], argv[2]); -} - variable_t * find_variable(name) WCHAR const *name; { -size_t i; - for (i = 0; i < sizeof(variables) / sizeof(*variables); i++) - if (STRCMP(name, variables[i].va_name) == 0) - return &variables[i]; +variable_t *v; + for (v = variables; v->va_name; v++) + if (STRCMP(name, v->va_name) == 0) + return v; return NULL; } -void -c_set(argc, argv) - size_t argc; - WCHAR **argv; -{ -variable_t *var; -int val; - - if (argc != 3) { - cmderr(WIDE("Usage: set ")); - return; - } - - if ((var = find_variable(argv[1])) == NULL) { - cmderr(WIDE("Unknown variable \"%"FMT_L"s\"."), argv[1]); - return; - } - - switch (var->va_type) { - case VTYPE_BOOL: - if (STRCMP(argv[2], WIDE("true")) == 0 || - STRCMP(argv[2], WIDE("yes")) == 0 || - STRCMP(argv[2], WIDE("on")) == 0 || - STRCMP(argv[2], WIDE("1")) == 0) { - val = 1; - } else if (STRCMP(argv[2], WIDE("false")) == 0 || - STRCMP(argv[2], WIDE("no")) == 0 || - STRCMP(argv[2], WIDE("off")) == 0 || - STRCMP(argv[2], WIDE("0")) == 0) { - val = 0; - } else { - cmderr(WIDE("Invalid value for boolean: \"%"FMT_L"s\"."), argv[2]); - return; - } - - *(int *)var->va_addr = val; - break; - - case VTYPE_STRING: - *(WCHAR **)var->va_addr = STRDUP(argv[2]); - break; - - case VTYPE_INT: - *(int *)var->va_addr = STRTOL(argv[2], NULL, 0); - break; - } -} - int attr_find(name, result) const WCHAR *name; @@ -2672,6 +1277,7 @@ size_t i; return -1; } + void style_clear(sy) style_t *sy; diff --git a/tts.h b/tts.h new file mode 100644 index 0000000..318f8ba --- /dev/null +++ b/tts.h @@ -0,0 +1,65 @@ +/* + * 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. + */ + +#ifndef TTS_H +#define TTS_H + +#include + +#include "wide.h" +#include "queue.h" +#include "entry.h" + +/* + * Configuration options. + */ + +extern int show_billable; +extern int delete_advance; +extern int mark_advance; +extern int bill_advance; +extern int bill_increment; +extern WCHAR *auto_nonbillable; + +/* + * Global state. + */ + +extern entry_t *curent; +extern volatile sig_atomic_t doexit; +extern int pagestart; +extern time_t itime; + +int load(void); +int save(void); + +/* + * Command history. + */ + +#define NHIST 50 +typedef struct histent { + WCHAR *he_text; + TAILQ_ENTRY(histent) he_entries; +} histent_t; +typedef TAILQ_HEAD(hentlist, histent) hentlist_t; + +typedef struct history { + int hi_nents; + hentlist_t hi_ents; +} history_t; + +history_t *hist_new(void); +void hist_add(history_t *, WCHAR const *); + +extern history_t *searchhist; +extern history_t *prompthist; + +#endif /* !TTS_H */ diff --git a/tts_curses.h b/tts_curses.h new file mode 100644 index 0000000..79f4e20 --- /dev/null +++ b/tts_curses.h @@ -0,0 +1,30 @@ +/* + * 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. + */ + +#ifndef TTS_CURSES_H +#define TTS_CURSES_H + +#include "config.h" + +#if defined HAVE_NCURSESW_CURSES_H +# include +#elif defined HAVE_NCURSESW_H +# include +#elif defined HAVE_NCURSES_CURSES_H +# include +#elif defined HAVE_NCURSES_H +# include +#elif defined HAVE_CURSES_H +# include +#else +# error "SVR4 or XSI compatible curses header file required" +#endif + +#endif /* !TTS_CURSES_H */ diff --git a/ui.c b/ui.c new file mode 100644 index 0000000..1ac4965 --- /dev/null +++ b/ui.c @@ -0,0 +1,65 @@ +/* + * 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 "ui.h" +#include "tts.h" + +WINDOW *titwin, *statwin, *listwin; +int in_curses; + +/* + * Move the cursor to the next entry after an operation like mark or deleted. + * If there are no suitable entries after this one, move it backwards instead. + */ +void +cursadvance() +{ +entry_t *en; + + if (!curent) { + curent = TAILQ_FIRST(&entries); + return; + } + + /* + * Try to find the next suitable entry to move the cursor to. + */ + for (en = TAILQ_NEXT(curent, en_entries); en; en = TAILQ_NEXT(en, en_entries)) { + if (!showinv && en->en_flags.efl_invoiced) + continue; + curent = en; + if (!curent->en_flags.efl_visible) + pagestart++; + return; + } + + /* + * No entries; if the current entry is visible, stay here, otherwise + * try moving backwards instead. + */ + if (showinv || !curent->en_flags.efl_invoiced) + return; + + for (en = TAILQ_PREV(curent, entrylist, en_entries); en; + en = TAILQ_PREV(en, entrylist, en_entries)) { + if (!showinv && en->en_flags.efl_invoiced) + continue; + curent = en; + if (!curent->en_flags.efl_visible) + pagestart--; + return; + } + + /* + * Couldn't find any entries at all? + */ + curent = NULL; +} + diff --git a/ui.h b/ui.h new file mode 100644 index 0000000..005ac7e --- /dev/null +++ b/ui.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#ifndef TTS_UI_H +#define TTS_UI_H + +#include + +#include "tts.h" +#include "tts_curses.h" +#include "wide.h" + +extern WINDOW *titwin, *statwin, *listwin; +extern int in_curses; +extern int showinv; + +void cursadvance(void); + +void drawstatus (const WCHAR *msg, ...); +void vdrawstatus (const WCHAR *msg, va_list); +void drawheader (void); +void drawentries (void); + +WCHAR *prompt (WCHAR const *, WCHAR const *, history_t *); +int prduration (WCHAR *prompt, int *h, int *m, int *s); +int yesno (WCHAR const *); +void errbox (WCHAR const *, ...); +void verrbox (WCHAR const *, va_list); + +#endif /* !TTS_UI_H */ diff --git a/variable.h b/variable.h new file mode 100644 index 0000000..8cb8447 --- /dev/null +++ b/variable.h @@ -0,0 +1,28 @@ +/* + * 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. + */ + +#ifndef TTS_VARIABLE_H +#define TTS_VARIABLE_H + +#include "wide.h" + +typedef struct variable { + WCHAR const *va_name; + int va_type; + void *va_addr; +} variable_t; + +#define VTYPE_INT 1 +#define VTYPE_BOOL 2 +#define VTYPE_STRING 3 + +variable_t *find_variable(const WCHAR *name); + +#endif /* !TTS_VARIABLE_H */ diff --git a/wide.c b/wide.c new file mode 100644 index 0000000..7e339c5 --- /dev/null +++ b/wide.c @@ -0,0 +1,26 @@ +/* + * 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 "wide.h" +#include "tts_curses.h" + +#ifdef NEED_TTS_WGETCH +static int +tts_wgetch(win, d) + WINDOW *win; + int *d; +{ +int c; + if ((c = wgetch(win)) == ERR) + return ERR; + *d = c; + return OK; +} +#endif /* !NEED_TTS_WGETCH */ diff --git a/wide.h b/wide.h new file mode 100644 index 0000000..85df84d --- /dev/null +++ b/wide.h @@ -0,0 +1,81 @@ +/* + * 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. + */ + +#ifndef TTS_WIDE_H +#define TTS_WIDE_H + +#include "config.h" +#include "tts_curses.h" + +#ifdef HAVE_CURSES_ENHANCED +# include + +# define WPFX(x) wcs##x +# define WIDE(x) L##x +# define ISX(x) isw##x +# define WCHAR wchar_t +# define FMT_L "l" +# define SNPRINTF swprintf +# define VSNPRINTF vswprintf +# define SSCANF swscanf +# define MEMCPY wmemcpy +# define MEMMOVE wmemmove +# define MBSTOWCS mbstowcs +# define WCSTOMBS wcstombs +# define FPRINTF fwprintf +# define STRTOK wcstok + +# define GETCH get_wch +# define WGETCH wget_wch +# define ADDSTR addwstr +# define WADDSTR waddwstr +# define INT wint_t +#else +# define WPFX(x) str##x +# define WIDE(x) x +# define ISX(x) is##x +# define WCHAR char +# define FMT_L +# define SNPRINTF snprintf +# define VSNPRINTF vsnprintf +# define SSCANF sscanf +# define MEMCPY memcpy +# define MEMMOVE memmove +# define MBSTOWCS strncpy +# define WCSTOMBS strncpy +# define FPRINTF fprintf +# define STRTOK strtok_r + +# define ADDSTR addstr +# define WADDSTR waddstr +# define INT int + +# define NEED_TTS_WGETCH +extern int tts_wgetch(WINDOW *, int *); + +# define WGETCH tts_wgetch +# define GETCH(c) tts_wgetch(stdscr,c) +#endif + +#define STRLEN WPFX(len) +#define STRCMP WPFX(cmp) +#define STRNCMP WPFX(ncmp) +#define STRCPY WPFX(cpy) +#define STRNCPY WPFX(ncpy) +#define STRSTR WPFX(str) +#define STRFTIME WPFX(ftime) +#define STRDUP WPFX(dup) +#define STRTOL WPFX(tol) + +#define ISSPACE ISX(space) + +#define WSIZEOF(s) (sizeof(s) / sizeof(WCHAR)) + +#endif /* !TTS_WIDE_H */