tts/tts.c
Felicity Tarnell 38021971e3 Rework input handling; no functional changes.
Instead of using halfdelay() and relying on GETCH() to return, poll for
input ourselves using select() and only call GETCH() when input is ready.

On Darwin, use kqueue instead of select.  This allows us to receive IOKit
notifications directly to the main thread, instead of having a separate
thread dedicated to that.  We also no longer link to CoreFoundation.
2014-03-07 21:54:26 +00:00

1354 lines
27 KiB
C

/*
* 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 <sys/types.h>
#include <sys/select.h>
#include <wchar.h>
#include <ctype.h>
#include <wctype.h>
#include <fcntl.h>
#include <time.h>
#include <signal.h>
#include <stdlib.h>
#include <strings.h>
#include <string.h>
#include <pwd.h>
#include <limits.h>
#include <errno.h>
#include <unistd.h>
#include <locale.h>
#include <stdarg.h>
#include <inttypes.h>
#include <math.h>
#include "config.h"
#ifdef HAVE_IOREGISTERFORSYSTEMPOWER
# define USE_DARWIN_POWER
# include <mach/mach_port.h>
# include <mach/mach_interface.h>
# include <mach/mach_init.h>
# include <mach/mach_error.h>
# include <sys/event.h>
# include <IOKit/pwr_mgt/IOPMLib.h>
# include <IOKit/IOMessage.h>
# include <alloca.h>
#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("<BREAK>") },
{ KEY_DOWN, WIDE("<DOWN>") },
{ KEY_UP, WIDE("<UP>") },
{ KEY_LEFT, WIDE("<LEFT>") },
{ KEY_RIGHT, WIDE("<RIGHT>") },
{ KEY_HOME, WIDE("<HOME>") },
{ KEY_BACKSPACE, WIDE("<BACKSPACE>") },
{ 0x7F, WIDE("<BACKSPACE>") }, /* DEL */
{ KEY_F(0), WIDE("<F0>") },
{ KEY_F(1), WIDE("<F1>") },
{ KEY_F(2), WIDE("<F2>") },
{ KEY_F(3), WIDE("<F3>") },
{ KEY_F(4), WIDE("<F4>") },
{ KEY_F(5), WIDE("<F5>") },
{ KEY_F(6), WIDE("<F6>") },
{ KEY_F(7), WIDE("<F7>") },
{ KEY_F(8), WIDE("<F8>") },
{ KEY_F(9), WIDE("<F9>") },
{ KEY_F(10), WIDE("<F10>") },
{ KEY_F(11), WIDE("<F1l>") },
{ KEY_F(12), WIDE("<F12>") },
{ KEY_F(13), WIDE("<F13>") },
{ KEY_F(14), WIDE("<F14>") },
{ KEY_F(15), WIDE("<F15>") },
{ KEY_F(16), WIDE("<F16>") },
{ KEY_F(17), WIDE("<F17>") },
{ KEY_F(18), WIDE("<F18>") },
{ KEY_F(19), WIDE("<F19>") },
{ KEY_F(20), WIDE("<F20>") },
{ KEY_F(21), WIDE("<F21>") },
{ KEY_F(22), WIDE("<F22>") },
{ KEY_F(23), WIDE("<F23>") },
{ KEY_F(24), WIDE("<F24>") },
{ KEY_NPAGE, WIDE("<NEXT>") },
{ KEY_PPAGE, WIDE("<PREV>") },
{ '\001', WIDE("<CTRL-A>") },
{ '\002', WIDE("<CTRL-B>") },
{ '\003', WIDE("<CTRL-C>") },
{ '\004', WIDE("<CTRL-D>") },
{ '\005', WIDE("<CTRL-E>") },
{ '\006', WIDE("<CTRL-F>") },
{ '\007', WIDE("<CTRL-G>") },
{ '\010', WIDE("<CTRL-H>") },
{ '\011', WIDE("<CTRL-I>") },
{ '\011', WIDE("<TAB>") },
{ '\012', WIDE("<CTRL-J>") },
{ '\013', WIDE("<CTRL-K>") },
{ '\014', WIDE("<CTRL-L>") },
{ '\015', WIDE("<CTRL-N>") },
{ '\016', WIDE("<CTRL-O>") },
{ '\017', WIDE("<CTRL-P>") },
{ '\020', WIDE("<CTRL-Q>") },
{ '\021', WIDE("<CTRL-R>") },
{ '\022', WIDE("<CTRL-S>") },
{ '\023', WIDE("<CTRL-T>") },
{ '\024', WIDE("<CTRL-U>") },
{ '\025', WIDE("<CTRL-V>") },
{ '\026', WIDE("<CTRL-W>") },
{ '\027', WIDE("<CTRL-X>") },
{ '\030', WIDE("<CTRL-Y>") },
{ '\031', WIDE("<CTRL-Z>") },
{ ' ', WIDE("<SPACE>") },
{ KEY_ENTER, WIDE("<ENTER>") },
{ KEY_BACKSPACE, WIDE("<BACKSPACE>") },
{ KEY_DC, WIDE("<DELETE>") }
};
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 kevent64_s *);
static void power_handle(struct kevent64_s *);
static void power_event(void *, io_service_t, natural_t, void *);
static void prompt_sleep(void);
static void
power_setup(ev)
struct kevent64_s *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,
&notifier);
ioport = IONotificationPortGetMachPort(port_ref);
EV_SET64(ev, pset, EVFILT_MACHPORT, EV_ADD, 0, 0, 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 kevent64_s *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 kevent64_s 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_SET64(&evs[STDIN_EV], STDIN_FILENO, EVFILT_READ, EV_ADD, 0, 0, 0, 0, 0);
power_setup(&evs[IOKIT_EV]);
if (kevent64(kq, evs, 2, NULL, 0, 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("<CTRL-C>"), 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("<SPACE>"), WIDE("startstop"));
bind_key(WIDE("e"), WIDE("edit-desc"));
bind_key(WIDE("\\"), WIDE("edit-time"));
bind_key(WIDE("<TAB>"), 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("<UP>"), WIDE("prev"));
bind_key(WIDE("<DOWN>"), 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 = kevent64(kq, NULL, 0, &rev, 1, 0,
running ? &timeout : NULL)) == -1) {
perror("kevent");
return 1;
}
if (nev == 0)
continue;
if (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 there's a running entry, wake up in 0.5 seconds time to update
* the display. Otherwise, we can sleep forever.
*/
select(STDIN_FILENO + 1, &in_set, NULL, NULL,
running ? &timeout : NULL);
if (!FD_ISSET(STDIN_FILENO, &in_set))
continue;
#endif
if (GETCH(&c) == ERR) {
if (doexit)
break;
if (time(NULL) - laststatus >= 2)
drawstatus(WIDE(""));
if (time(NULL) - lastsave > 60)
save();
continue;
}
#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: ;
}
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(" <OK> ")
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 */