Currently, if the computer goes to sleep and wakes up later, TTS will count all the time spent asleep against the currently running entry (if any). If (like me) your computer being asleep means you aren't working, this probably means you just forgot to stop the time, and now you need to somehow calculate (or guess) how much time to subtract. With this change, TTS will instead prompt on wake-up whether to remove the time spent sleeping from the current entry.
2790 lines
53 KiB
C
2790 lines
53 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.
|
|
*/
|
|
|
|
#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 <sys/types.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 "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 <IOKit/pwr_mgt/IOPMLib.h>
|
|
# include <IOKit/IOMessage.h>
|
|
|
|
# include <pthread.h>
|
|
#endif
|
|
|
|
#if defined HAVE_NCURSESW_CURSES_H
|
|
# include <ncursesw/curses.h>
|
|
#elif defined HAVE_NCURSESW_H
|
|
# include <ncursesw.h>
|
|
#elif defined HAVE_NCURSES_CURSES_H
|
|
# include <ncurses/curses.h>
|
|
#elif defined HAVE_NCURSES_H
|
|
# include <ncurses.h>
|
|
#elif defined HAVE_CURSES_H
|
|
# include <curses.h>
|
|
#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(cmp)
|
|
#define STRCPY WPFX(cpy)
|
|
#define STRNCPY WPFX(ncpy)
|
|
#define STRSTR WPFX(str)
|
|
#define STRFTIME WPFX(ftime)
|
|
#define STRDUP WPFX(dup)
|
|
|
|
#define ISSPACE ISX(space)
|
|
|
|
#define WSIZEOF(s) (sizeof(s) / sizeof(WCHAR))
|
|
|
|
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);
|
|
|
|
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;
|
|
} 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);
|
|
|
|
#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);
|
|
|
|
#define STATFILE ".rttts"
|
|
#define RCFILE ".ttsrc"
|
|
|
|
static int load(void);
|
|
static 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 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);
|
|
static void kmarkint(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("set the current entry as invoiced") },
|
|
{ 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")},
|
|
{ WIDE("mark-interrupt"), kmarkint, WIDE("start interrupt timer for current entry")}
|
|
};
|
|
|
|
typedef struct tkey {
|
|
INT ky_code;
|
|
const WCHAR *ky_name;
|
|
} tkey_t;
|
|
|
|
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>") }
|
|
};
|
|
|
|
typedef struct binding {
|
|
INT bi_code;
|
|
tkey_t *bi_key;
|
|
function_t *bi_func;
|
|
TAILQ_ENTRY(binding) bi_entries;
|
|
} binding_t;
|
|
|
|
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;
|
|
|
|
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 }
|
|
};
|
|
|
|
typedef struct colour {
|
|
const WCHAR *co_name;
|
|
short co_value;
|
|
} colour_t;
|
|
|
|
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 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);
|
|
|
|
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 };
|
|
|
|
static time_t itime = 0;
|
|
|
|
static int delete_advance = 1;
|
|
static int mark_advance = 1;
|
|
|
|
#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[] = {
|
|
{ WIDE("delete_advance"), VTYPE_BOOL, &delete_advance },
|
|
{ WIDE("mark_advance"), VTYPE_BOOL, &mark_advance }
|
|
};
|
|
|
|
static variable_t *find_variable(const WCHAR *name);
|
|
|
|
#ifdef USE_DARWIN_POWER
|
|
static pthread_t power_thread;
|
|
static io_connect_t root_port;
|
|
static volatile sig_atomic_t donesleep;
|
|
static time_t sleeptime;
|
|
|
|
static void *power_thread_run(void *);
|
|
static void power_event(void *, io_service_t, natural_t, void *);
|
|
static void prompt_sleep(void);
|
|
|
|
static void
|
|
sigsleep(sig)
|
|
{
|
|
/* Delivered from the power thread as SIGUSR1 */
|
|
donesleep = 1;
|
|
}
|
|
|
|
/*
|
|
* Darwin power notifications are delivered from IOKit via Mach ports, which
|
|
* is incompatible with TTS's curses-based main loop. We therefore spawn a
|
|
* separate thread to listen for these events, and when we receive one, we
|
|
* translate it into a signal (SIGUSR1) which is delivered to the main thread
|
|
* to handle. The signal will interrupt getch().
|
|
*/
|
|
static void *
|
|
power_thread_run(arg)
|
|
void *arg;
|
|
{
|
|
sigset_t ss;
|
|
IONotificationPortRef port_ref;
|
|
io_object_t notifier;
|
|
|
|
/* Block SIGUSR1 so it's always delivered to the main thread, not us */
|
|
sigemptyset(&ss);
|
|
sigaddset(&ss, SIGUSR1);
|
|
pthread_sigmask(SIG_BLOCK, &ss, NULL);
|
|
|
|
/* Register a handler for sleep and wake events */
|
|
root_port = IORegisterForSystemPower(NULL, &port_ref, power_event,
|
|
¬ifier);
|
|
CFRunLoopAddSource(CFRunLoopGetCurrent(),
|
|
IONotificationPortGetRunLoopSource(port_ref),
|
|
kCFRunLoopCommonModes);
|
|
CFRunLoopRun();
|
|
|
|
/*NOTREACHED*/
|
|
return NULL;
|
|
}
|
|
|
|
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
|
|
* notify the main thread.
|
|
*/
|
|
time(&diff);
|
|
diff -= sleep_started;
|
|
sleeptime += diff;
|
|
raise(SIGUSR1);
|
|
break;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
static void
|
|
sigexit(sig)
|
|
{
|
|
doexit = 1;
|
|
}
|
|
|
|
int
|
|
main(argc, argv)
|
|
char **argv;
|
|
{
|
|
struct passwd *pw;
|
|
int i;
|
|
char rcfile[PATH_MAX + 1];
|
|
|
|
setlocale(LC_ALL, "");
|
|
|
|
signal(SIGTERM, sigexit);
|
|
signal(SIGINT, sigexit);
|
|
|
|
#ifdef USE_DARWIN_POWER
|
|
signal(SIGUSR1, sigsleep);
|
|
pthread_create(&power_thread, NULL, power_thread_run, NULL);
|
|
#endif
|
|
|
|
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();
|
|
halfdelay(5);
|
|
|
|
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("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("mark-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 (!TAILQ_EMPTY(&entries)) {
|
|
curent = TAILQ_FIRST(&entries);
|
|
while (!showinv && curent->en_flags.efl_invoiced)
|
|
if ((curent = TAILQ_NEXT(curent, en_entries)) == NULL)
|
|
break;
|
|
}
|
|
|
|
for (;;) {
|
|
INT c;
|
|
size_t s;
|
|
binding_t *bi;
|
|
|
|
if (doexit)
|
|
break;
|
|
|
|
if (donesleep)
|
|
prompt_sleep();
|
|
|
|
drawheader();
|
|
drawentries();
|
|
wrefresh(listwin);
|
|
|
|
if (GETCH(&c) == ERR) {
|
|
if (doexit)
|
|
break;
|
|
if (donesleep)
|
|
prompt_sleep();
|
|
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(""));
|
|
|
|
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
|
|
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 *newcur;
|
|
entry_t *en, *ten;
|
|
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
|
|
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;
|
|
curent->en_secs += en->en_secs;
|
|
if (en->en_started) {
|
|
entry_stop(en);
|
|
curent->en_secs += time(NULL) - en->en_started;
|
|
}
|
|
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()
|
|
{
|
|
entry_t *next;
|
|
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 (!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
|
|
kmarkint()
|
|
{
|
|
if (itime) {
|
|
kint();
|
|
} else {
|
|
if (!running) {
|
|
drawstatus(WIDE("No running entry."));
|
|
return;
|
|
}
|
|
itime = time(NULL);
|
|
}
|
|
}
|
|
|
|
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()
|
|
{
|
|
wmove(titwin, 0, 0);
|
|
waddstr(titwin, "TTS " PACKAGE_VERSION " - Type '?' for help");
|
|
if (itime > 0) {
|
|
WCHAR str[128];
|
|
int h, m, s;
|
|
time_t passed = time(NULL) - itime;
|
|
|
|
time_to_hms(passed, h, m, s);
|
|
SNPRINTF(str, WSIZEOF(str), WIDE(" *** MARK INTERRUPT: %02d:%02d:%02d ***"),
|
|
h, m, s);
|
|
|
|
wattron(titwin, A_BOLD);
|
|
WADDSTR(titwin, str);
|
|
wattroff(titwin, A_BOLD);
|
|
}
|
|
wclrtoeol(titwin);
|
|
wrefresh(titwin);
|
|
}
|
|
|
|
void
|
|
vdrawstatus(msg, ap)
|
|
const WCHAR *msg;
|
|
va_list ap;
|
|
{
|
|
WCHAR s[1024];
|
|
VSNPRINTF(s, WSIZEOF(s), msg, ap);
|
|
|
|
wmove(statwin, 0, 0);
|
|
WADDSTR(statwin, s);
|
|
wclrtoeol(statwin);
|
|
wrefresh(statwin);
|
|
time(&laststatus);
|
|
}
|
|
|
|
void
|
|
drawstatus(const WCHAR *msg, ...)
|
|
{
|
|
va_list ap;
|
|
va_start(ap, msg);
|
|
vdrawstatus(msg, ap);
|
|
va_end(ap);
|
|
}
|
|
|
|
int
|
|
yesno(msg)
|
|
const WCHAR *msg;
|
|
{
|
|
WINDOW *pwin;
|
|
INT c;
|
|
|
|
pwin = newwin(1, COLS, LINES - 2, 0);
|
|
keypad(pwin, TRUE);
|
|
wattron(pwin, A_BOLD);
|
|
wmove(pwin, 0, 0);
|
|
WADDSTR(pwin, msg);
|
|
WADDSTR(pwin, WIDE(" [y/N]? "));
|
|
wattroff(pwin, A_BOLD);
|
|
|
|
while (WGETCH(pwin, &c) == ERR
|
|
#ifdef KEY_RESIZE
|
|
|| (c == KEY_RESIZE)
|
|
#endif
|
|
)
|
|
;
|
|
|
|
return (c == 'Y' || c == 'y') ? 1 : 0;
|
|
}
|
|
|
|
WCHAR *
|
|
prompt(msg, def, hist)
|
|
const WCHAR *msg, *def;
|
|
history_t *hist;
|
|
{
|
|
WINDOW *pwin;
|
|
WCHAR input[256];
|
|
size_t pos = 0;
|
|
histent_t *histpos = NULL;
|
|
|
|
if (hist == NULL)
|
|
hist = prompthist;
|
|
|
|
memset(input, 0, sizeof(input));
|
|
if (def) {
|
|
STRNCPY(input, def, WSIZEOF(input) - 1);
|
|
pos = STRLEN(input);
|
|
}
|
|
|
|
pwin = newwin(1, COLS, LINES - 2, 0);
|
|
keypad(pwin, TRUE);
|
|
|
|
wattr_on(pwin, style_fg(sy_status), NULL);
|
|
wbkgd(pwin, style_bg(sy_status));
|
|
|
|
wattron(pwin, A_BOLD);
|
|
wmove(pwin, 0, 0);
|
|
WADDSTR(pwin, msg);
|
|
wattroff(pwin, A_BOLD);
|
|
|
|
curs_set(1);
|
|
|
|
for (;;) {
|
|
INT c;
|
|
wmove(pwin, 0, STRLEN(msg) + 1);
|
|
WADDSTR(pwin, input);
|
|
wclrtoeol(pwin);
|
|
wmove(pwin, 0, STRLEN(msg) + 1 + pos);
|
|
wrefresh(pwin);
|
|
|
|
if (WGETCH(pwin, &c) == ERR)
|
|
continue;
|
|
|
|
switch (c) {
|
|
case '\n':
|
|
case '\r':
|
|
goto end;
|
|
|
|
case KEY_BACKSPACE:
|
|
case 0x7F:
|
|
case 0x08:
|
|
if (pos) {
|
|
if (pos == STRLEN(input))
|
|
input[--pos] = 0;
|
|
else {
|
|
int i = STRLEN(input);
|
|
pos--;
|
|
MEMCPY(input + pos, input + pos + 1, STRLEN(input) - pos);
|
|
input[i] = 0;
|
|
}
|
|
}
|
|
break;
|
|
|
|
case KEY_DC:
|
|
if (pos < STRLEN(input)) {
|
|
int i = STRLEN(input);
|
|
MEMCPY(input + pos, input + pos + 1, STRLEN(input) - pos);
|
|
input[i] = 0;
|
|
}
|
|
break;
|
|
|
|
case KEY_LEFT:
|
|
if (pos)
|
|
pos--;
|
|
break;
|
|
|
|
case KEY_RIGHT:
|
|
if (pos < STRLEN(input))
|
|
pos++;
|
|
break;
|
|
|
|
case KEY_HOME:
|
|
case 0x01: /* ^A */
|
|
pos = 0;
|
|
break;
|
|
|
|
case KEY_END:
|
|
case 0x05: /* ^E */
|
|
pos = STRLEN(input);
|
|
break;
|
|
|
|
case 0x07: /* ^G */
|
|
case 0x1B: /* ESC */
|
|
curs_set(0);
|
|
delwin(pwin);
|
|
return NULL;
|
|
|
|
case 0x15: /* ^U */
|
|
input[0] = 0;
|
|
pos = 0;
|
|
break;
|
|
|
|
#ifdef KEY_RESIZE
|
|
case KEY_RESIZE:
|
|
break;
|
|
#endif
|
|
|
|
case KEY_UP:
|
|
if (histpos == NULL) {
|
|
if ((histpos = TAILQ_LAST(&hist->hi_ents, hentlist)) == NULL) {
|
|
beep();
|
|
break;
|
|
}
|
|
} else {
|
|
if (TAILQ_PREV(histpos, hentlist, he_entries) == NULL) {
|
|
beep();
|
|
break;
|
|
} else
|
|
histpos = TAILQ_PREV(histpos, hentlist, he_entries);
|
|
}
|
|
|
|
|
|
STRNCPY(input, histpos->he_text, WSIZEOF(input) - 1);
|
|
pos = STRLEN(input);
|
|
break;
|
|
|
|
case KEY_DOWN:
|
|
if (histpos == NULL) {
|
|
beep();
|
|
break;
|
|
}
|
|
|
|
if (TAILQ_NEXT(histpos, he_entries) == NULL) {
|
|
beep();
|
|
break;
|
|
} else
|
|
histpos = TAILQ_NEXT(histpos, he_entries);
|
|
|
|
|
|
STRNCPY(input, histpos->he_text, WSIZEOF(input) - 1);
|
|
pos = STRLEN(input);
|
|
break;
|
|
|
|
default:
|
|
if (pos != STRLEN(input)) {
|
|
MEMMOVE(input + pos + 1, input + pos, STRLEN(input) - pos);
|
|
input[pos++] = c;
|
|
} else {
|
|
input[pos++] = c;
|
|
input[pos] = 0;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
end: ;
|
|
|
|
curs_set(0);
|
|
delwin(pwin);
|
|
wtouchln(statwin, 1, 1, 1);
|
|
hist_add(hist, input);
|
|
return STRDUP(input);
|
|
}
|
|
|
|
void
|
|
drawentries()
|
|
{
|
|
int i, nlines;
|
|
int cline = 0;
|
|
time_t lastday = 0;
|
|
entry_t *en;
|
|
chtype oldbg;
|
|
|
|
getmaxyx(listwin, nlines, i);
|
|
|
|
TAILQ_FOREACH(en, &entries, en_entries)
|
|
en->en_flags.efl_visible = 0;
|
|
|
|
en = TAILQ_FIRST(&entries);
|
|
for (i = 0; i < pagestart; i++)
|
|
if ((en = TAILQ_NEXT(en, en_entries)) == NULL)
|
|
return;
|
|
|
|
for (; en; en = 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),
|
|
ntime = entry_time_for_day(entry_day(en), 0);
|
|
int hi, mi, si,
|
|
hn, mn, sn,
|
|
ht, mt, st;
|
|
WCHAR hdrtime;
|
|
WCHAR hdrtext[256];
|
|
|
|
time_to_hms(itime, hi, mi, si);
|
|
time_to_hms(ntime, hn, mn, sn);
|
|
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);
|
|
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_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);
|
|
}
|
|
|
|
entry_t *
|
|
entry_new(desc)
|
|
const WCHAR *desc;
|
|
{
|
|
entry_t *en;
|
|
if ((en = calloc(1, sizeof(*en))) == NULL)
|
|
return NULL;
|
|
|
|
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
|
|
* -1, sum all entries.
|
|
*/
|
|
time_t
|
|
entry_time_for_day(when, inv)
|
|
time_t when;
|
|
{
|
|
time_t day = time_day(when);
|
|
time_t sum = 0;
|
|
entry_t *en;
|
|
TAILQ_FOREACH(en, &entries, en_entries) {
|
|
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;
|
|
sum += en->en_secs;
|
|
if (en->en_started)
|
|
sum += time(NULL) - en->en_started;
|
|
}
|
|
return sum;
|
|
}
|
|
|
|
int
|
|
load()
|
|
{
|
|
FILE *f;
|
|
char input[4096];
|
|
WCHAR line[4096];
|
|
entry_t *en;
|
|
|
|
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"[i-] %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;
|
|
|
|
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);
|
|
}
|
|
|
|
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';
|
|
|
|
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;
|
|
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;
|
|
}
|
|
|
|
TAILQ_INSERT_TAIL(&hi->hi_ents, hent, he_entries);
|
|
|
|
if (hi->hi_nents == 50)
|
|
TAILQ_REMOVE(&hi->hi_ents, 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;
|
|
{
|
|
size_t i;
|
|
for (i = 0; i < sizeof(funcs) / sizeof(*funcs); i++)
|
|
if (STRCMP(name, funcs[i].fn_name) == 0)
|
|
return &funcs[i];
|
|
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? */
|
|
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;
|
|
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;
|
|
|
|
while (ISSPACE(*p))
|
|
p++;
|
|
|
|
if (!*p)
|
|
break;
|
|
|
|
q = p;
|
|
|
|
/* Find the next seperator */
|
|
while (!ISSPACE(*q) && *q)
|
|
q++;
|
|
|
|
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++;
|
|
|
|
while (ISSPACE(*q))
|
|
q++;
|
|
|
|
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 <item> <foreground> [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 <key> <function>"));
|
|
return;
|
|
}
|
|
|
|
bind_key(argv[1], argv[2]);
|
|
}
|
|
|
|
variable_t *
|
|
find_variable(name)
|
|
WCHAR const *name;
|
|
{
|
|
variable_t *v;
|
|
size_t i;
|
|
for (i = 0; i < sizeof(variables) / sizeof(*variables); i++)
|
|
if (STRCMP(name, variables[i].va_name) == 0)
|
|
return &variables[i];
|
|
return NULL;
|
|
}
|
|
|
|
void
|
|
c_set(argc, argv)
|
|
size_t argc;
|
|
WCHAR **argv;
|
|
{
|
|
variable_t *var;
|
|
|
|
if (argc != 3) {
|
|
cmderr(WIDE("Usage: set <variable> <value>"));
|
|
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: {
|
|
int val;
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
|
|
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.
|
|
*/
|
|
entry_t *en, *ten;
|
|
int nmarked = 0;
|
|
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 */
|