#include #include #include #include #include #include #include #include #include #include #include "ul.h" #include "cbi.h" #include "lock.h" #include "conflib.h" #include "currlib.h" #include "deconst.h" #define TAB 9 #define CTRL_L 12 #define ESC 27 #define TAGCACHE_VERSION 2 extern const char *__progname; static const char *cat_file; static const char *db_file; static const char *curr_file; static const char *def_curr; static const char *tagcache; static const char **tags_files; static int n_tags_files; static LOCK_HANDLE db_lock; static void *conf_if; static void *conf_db; static int debugging; enum catdir { CD_IN = 1, CD_OUT, CD_TRANS } ; typedef enum catdir CATDIR; typedef struct date DATE; typedef struct cat CAT; typedef struct explist EXPLIST; typedef struct explistent EXPLISTENT; struct date { unsigned int year : 12; #define MAXYEAR ((1<<12)-1) unsigned int month : 4; unsigned int day : 5; } ; struct explistent { const char *str; int len; double freq; } ; struct explist { EXPLIST *chain; int nexp; EXPLISTENT **ents; } ; struct cat { const char *name; unsigned int len; CATDIR dir; EXPLIST *subtags; } ; static DATE todaydate; static int ncat; static CAT *cats; static int maxcatlen; static EXPLIST ftexp; static EXPLIST currexp; static EXPLIST *allexplists = 0; static double explist_decay_factor = 1; static void *free_ele = 0; static DATE date; static char tagstr[80]; static int tagcatno; static int taglen; static char fromstr[80]; static int fromlen; static char tostr[80]; static int tolen; static long int amount; static char currstr[80]; static int currlen; static int cathave; static int have; #define HAVE_DATE 0x00000001 #define HAVE_TAG 0x00000002 #define HAVE_FROM 0x00000004 #define HAVE_TO 0x00000008 #define HAVE_AMOUNT 0x00000010 #define HAVE_ALL 0x0000001f #define CATHAVE(x) ((cathave&HAVE_##x)==HAVE_##x) #define HAVE(x) (((have|cathave)&HAVE_##x)==HAVE_##x) #define SETHAVE(x) (have|=HAVE_##x) #define CLRHAVE(x) (have&=~HAVE_##x) static int xflags; #define XF_SHARED 0x00000001 #define XF_CONFMASK 0x0000000e #define XF_CONF_nil 0x00000000 #define XF_CONF_c 0x00000002 #define XF_CONF_cc 0x00000004 #define XF_CONF_C 0x00000006 #define XF_CONF_Cc 0x00000008 #define XF_CONF_cC 0x0000000a #define XF_CONF_CC 0x0000000c #define XF_XCURR 0x00000010 #define XF_PROTECT 0x00000020 static int autoprompt = 1; static int firstmatch = 1; static int tabtodot = 0; static double expthresh = 0; #define ABS(x) (((x)<0)?-(x):(x)) static unsigned int intlog10(unsigned long int amt) { unsigned int n; for (n=1;amt>=10;n++,amt/=10) ; return(n); } static char *bindex(const char *str, int len, char c) { char *s; s = deconst(str); for (;len>0;s++,len--) if (*s == c) return(s); return(0); } static char *brindex(const char *str, int len, char c) { char *s; if (len < 1) return(0); s = deconst(str) + len; for (;len>0;len--) if (*--s == c) return(s); return(0); } static int printable(int c) { return(((c >= 0x20) && (c <= 0x7e)) || ((c >= 0xa0) && (c <= 0xff))); } static void config_bool(void *c, const char *tag, int *vp) { const char *val; const char *from; int v; v = -1; val = conf_value(c,tag,&from); if (! val) return; if (val[0] && !val[1]) { switch (*val) { case '1': case 't': case 'T': case 'y': case 'Y': v = 1; break; case '0': case 'f': case 'F': case 'n': case 'N': v = 0; break; } } else { if ( !strcmp(val,"no") || !strcmp(val,"No") || !strcmp(val,"NO") || !strcmp(val,"false") || !strcmp(val,"False") || !strcmp(val,"FALSE") ) { v = 0; } else if ( !strcmp(val,"yes") || !strcmp(val,"Yes") || !strcmp(val,"YES") || !strcmp(val,"true") || !strcmp(val,"True") || !strcmp(val,"TRUE") ) { v = 1; } } if (v < 0) { fprintf(stderr,"%s: invalid boolean value in %s: %s\n",__progname,from,val); exit(1); } *vp = v; } static void config_double(void *c, const char *tag, double *vp) { const char *val; val = conf_value(c,tag,0); if (! val) return; *vp = atof(val); } static int readline(FILE *f, char **linep, int *lenp, const char *fmt, ...) { va_list ap; char *line; int have; int len; int c; line = 0; have = 0; len = 0; while (1) { c = getc(f); if (c == EOF) { if (len != 0) { if (fmt) { fprintf(stderr,"%s: warning: partial line at end of",__progname); va_start(ap,fmt); vfprintf(stderr,fmt,ap); va_end(ap); fprintf(stderr," ignored\n"); } *lenp = -1; } else { *lenp = 0; } free(line); return(0); } if (c == '\n') { *linep = line; *lenp = len; return(1); } while (len >= have) line = realloc(line,have+=8); line[len++] = c; } } static void handleargs(int ac, char **av, int early) { int skip; int errs; skip = 0; errs = 0; for (ac--,av++;ac;ac--,av++) { if (skip > 0) { skip --; continue; } if (**av != '-') { fprintf(stderr,"%s: stray argument %s\n",__progname,*av); errs ++; continue; } if (0) { needarg:; fprintf(stderr,"%s: %s needs a following argument\n",__progname,*av); errs ++; continue; } #define WANTARG() do { if (++skip >= ac) goto needarg; } while (0) if (!strcmp(*av,"-config-if") || !strcmp(*av,"-if")) { WANTARG(); if (! early) conf_read(conf_if,av[skip],0); continue; } if (!strcmp(*av,"-config-db") || !strcmp(*av,"-db")) { WANTARG(); if (! early) conf_read(conf_db,av[skip],0); continue; } if (!strcmp(*av,"-configline") || !strcmp(*av,"-set")) { WANTARG(); if (early) { conf_line(conf_if,av[skip],0); conf_line(conf_db,av[skip],0); } continue; } if (!strcmp(*av,"-debug")) { debugging = 1; continue; } #undef WANTARG fprintf(stderr,"%s: unrecognized option `%s'\n",__progname,*av); errs ++; } if (errs) { fprintf(stderr,"Usage: %s [options] [config-file]\n",__progname); exit(1); } } static EXPLISTENT *new_explistent(void) #define ELE_ALLOC 60 { static EXPLISTENT *freeblk; static int nfree = 0; if (free_ele) { EXPLISTENT *e; e = free_ele; bcopy(free_ele,&free_ele,sizeof(void *)); return(e); } if (nfree < 1) { nfree = ELE_ALLOC; freeblk = malloc(ELE_ALLOC*sizeof(EXPLISTENT)); } return(freeblk+--nfree); } static void free_explistent(EXPLISTENT *e) { bcopy(&free_ele,(void *)e,sizeof(void *)); free_ele = e; } static void bubble_exp(EXPLIST *e, int i) { while ((i > 0) && (e->ents[i]->freq > e->ents[i-1]->freq)) { EXPLISTENT *t; t = e->ents[i]; e->ents[i] = e->ents[i-1]; e->ents[i-1] = t; i --; } } static void init_explist(EXPLIST *e) { e->chain = allexplists; allexplists = e; e->nexp = 0; e->ents = 0; } static void add_expansion(EXPLIST *el, const char *str, int len) { int i; EXPLISTENT *e; char *t; if (len < 1) return; for (i=0;inexp;i++) { e = el->ents[i]; if ((e->len == len) && !bcmp(e->str,str,len)) { e->freq += 1; bubble_exp(el,i); return; } } el->nexp ++; el->ents = realloc(el->ents,el->nexp*sizeof(*el->ents)); e = new_explistent(); el->ents[el->nexp-1] = e; t = malloc(len); bcopy(str,t,len); e->str = t; e->len = len; e->freq = 1; bubble_exp(el,el->nexp-1); } static void set_expansion(EXPLIST *el, const char *str, int len, double freq) { int i; EXPLISTENT *e; char *t; if (len < 1) return; for (i=0;inexp;i++) { e = el->ents[i]; if ((e->len == len) && !bcmp(e->str,str,len)) { e->freq = freq; return; } } el->nexp ++; el->ents = realloc(el->ents,el->nexp*sizeof(*el->ents)); e = new_explistent(); el->ents[el->nexp-1] = e; t = malloc(len); bcopy(str,t,len); e->str = t; e->len = len; e->freq = freq; } static void clear_expansions(EXPLIST *l) { EXPLISTENT *e; int i; for (i=0;inexp;i++) { e = l->ents[i]; free(deconst(e->str)); free_explistent(e); } free(l->ents); l->nexp = 0; l->ents = 0; } static void explist_decay(void) { EXPLIST *e; int i; for (e=allexplists;e;e=e->chain) { for (i=0;inexp;i++) { e->ents[i]->freq *= explist_decay_factor; } } } static void process_explist(EXPLIST *e __attribute__((__unused__))) { } static int add_curr_exp(CURR *c) { add_expansion(&currexp,c->name,strlen(c->name)); return(0); } static void check_config(void) { int errs; errs = 0; cat_file = conf_path(conf_db,"categories",0); if (! cat_file) { fprintf(stderr,"%s: need a categories file\n",__progname); errs ++; } db_file = conf_path(conf_db,"db",0); if (! db_file) { fprintf(stderr,"%s: need a database file\n",__progname); errs ++; } curr_file = conf_path(conf_db,"currencies",0); if (curr_file) { load_currencies(curr_file,0); walk_currencies(&add_curr_exp); } else { fake_currencies(); } def_curr = conf_value(conf_db,"defcurrency",0); if (def_curr && !find_currency(def_curr)) { fprintf(stderr,"%s: default currency of %s isn't a known currency!\n",__progname,def_curr); errs ++; } n_tags_files = conf_paths(conf_db,"tags",0,0,0); if (n_tags_files) { tags_files = malloc(n_tags_files*sizeof(*tags_files)); conf_paths(conf_db,"tags",tags_files,0,n_tags_files); } tagcache = conf_path(conf_db,"tagcache",0); config_bool(conf_if,"autoprompt",&autoprompt); config_bool(conf_if,"firstmatch",&firstmatch); config_bool(conf_if,"tabtodot",&tabtodot); config_double(conf_if,"expthresh",&expthresh); config_double(conf_db,"decay",&explist_decay_factor); if (errs) exit(1); } static void init_categories(void) { ncat = 0; cats = 0; maxcatlen = 0; } static void load_categories(const char *file) { FILE *f; char *buf; unsigned int have; unsigned int len; int c; f = fopen(file,"r"); if (f == 0) { fprintf(stderr,"%s: can't open categories file %s\n",__progname,file); exit(1); } buf = 0; have = 0; len = 0; while (1) { c = getc(f); switch (c) { case EOF: if (len > 0) fprintf(stderr,"%s: warning: unterminated trailing line in categories file %s ignored\n",__progname,file); free(buf); return; break; case '\n': if (len > 1) { char *t; switch (buf[0]) { case '#': break; case 'i': case 'o': case 't': ncat ++; cats = realloc(cats,ncat*sizeof(*cats)); t = malloc(len-1); bcopy(buf+1,t,len-1); cats[ncat-1].name = t; cats[ncat-1].len = len - 1; switch (buf[0]) { case 'i': cats[ncat-1].dir = CD_IN; break; case 'o': cats[ncat-1].dir = CD_OUT; break; case 't': cats[ncat-1].dir = CD_TRANS; break; } cats[ncat-1].subtags = malloc(sizeof(EXPLIST)); init_explist(cats[ncat-1].subtags); if (len > maxcatlen) maxcatlen = len; break; default: fprintf(stderr,"%s: invalid category direction tag char %c in %s\n",__progname,buf[0],file); exit(1); break; } } len = 0; break; default: while (have <= len) buf = realloc(buf,have+=8); buf[len++] = c; break; } } } static void finish_category_load(void) { int i; int j; int errs; errs = 0; for (i=0;i='0')&&(buf[i]<='9')))) return; } sscanf(&buf[0],"%4d%2d%2d",&y,&m,&d); if ( (y > date.year) || ( (y == date.year) && ( (m > date.month) || ( (m == date.month) && (d > date.day) ) ) ) ) { date.year = y; date.month = m; date.day = d; } l -= 18; c0 = bindex(buf+18,l,':'); if (c0 == 0) return; l -= c0 - (buf+18); c1 = bindex(c0+1,l-1,':'); if (c1 == 0) return; l -= c1 - c0; c2 = bindex(c1+1,l-1,':'); if (c2 == 0) return; l -= c2 - c1; c3 = bindex(c2+1,l-1,':'); if (c3 == 0) return; l -= c3 - c2; if (l < 1) return; c4 = bindex(c3+1,l-1,':'); if (! c4) { if (def_curr) { add_expansion(&currexp,def_curr,strlen(def_curr)); } else { fprintf(stderr,"%s: no default currency, but found an entry with no currency\n",__progname); exit(1); } } else { char *t; t = malloc(l); l -= c4 - c3; bcopy(c4+1,t,l-1); t[l-1] = '\0'; if (find_currency(t)) { add_expansion(&currexp,c4+1,l-1); } else { fprintf(stderr,"%s: entry with unknown currency `",__progname); fwrite(c4+1,1,l-1,stderr); fprintf(stderr,"' found\n"); } free(t); } for (i=0;i cats[i].len) && !bcmp(c0+1,cats[i].name,cats[i].len) && (c0[cats[i].len+1] == '.') ) { add_expansion(cats[i].subtags,c0+cats[i].len+2,(c1-c0)-cats[i].len-2); } } add_expansion(&ftexp,c1+1,(c2-c1)-1); add_expansion(&ftexp,c2+1,(c3-c2)-1); } static void scan_db(const char *path, int tagsonly) { int fd; FILE *f; char *line; int len; int i; fd = open(path,O_RDONLY,0); if (fd < 0) return; f = fdopen(fd,"r"); while (readline(f,&line,&len,0)) { explist_decay(); scan_db_line(line,len,tagsonly); free(line); } if (len < 0) { fprintf(stderr,"%s: warning: partial line at end of %s ignored\n",__progname,db_file); if (! tagsonly) fprintf(stderr,"%s: warning: writing new entries will probably corrupt the database\n",__progname); } fclose(f); for (i=0;i= 0) && (len > 1) && (line[0] == '*')) { if ( (n >= ncat) || (len-1 != cats[n].len) || bcmp(line+1,cats[n].name,len-1) ) BADCACHE("unexpected category"); elist = cats[n++].subtags; continue; } lp = bindex(line,len,':'); if (lp == 0) BADCACHE("missing colon"); set_expansion(elist,lp+1,len-(lp-line)-1,atof(line)); } #undef BADCACHE } static void void_tags(void) { int i; clear_expansions(&ftexp); clear_expansions(&currexp); for (i=0;inexp;i++) { e = l->ents[i]; fprintf(f,"%g:",e->freq); fwrite(e->str,1,e->len,f); putc('\n',f); } } static void write_tagcache(void) { int fd; FILE *f; int i; fd = open(tagcache,O_WRONLY|O_CREAT|O_TRUNC,0666); if (fd < 0) return; f = fdopen(fd,"w"); fprintf(f,"%d\n",TAGCACHE_VERSION); write_tags(f,&ftexp); fprintf(f,".\n"); write_tags(f,&currexp); for (i=0;i= 0) && (tagstat.st_mtime >= cachestat.st_mtime) ) { if (debugging) { printf("[check_tags: cache %s [%.24s]",tagcache,ctime(&cachestat.st_mtime)); printf(" older than %s [%.24s]]\n",tags_files[i],ctime(&tagstat.st_mtime)); } goto updcache; } } if (load_tagcache(tagcache)) { if (debugging) printf("[check_tags: cache load successful]\n"); return; } if (debugging) printf("[check_tags: cache load failed]\n"); void_tags(); } updcache:; if (debugging) printf("[check_tags: scanning tags files]\n"); scan_tags(); if (debugging) printf("[check_tags: done scanning]\n"); if (tagcache) write_tagcache(); } static inline unsigned int const monthdays(unsigned int m, unsigned int y) { if (m == 2) { if (y % 4) return(28); if (y % 100) return(29); if (y % 400) return(28); return(29); } /* JanFebMarAprMayJunJulAugSepOctNovDec */ return(" \37\00\37\36\37\36\37\37\36\37\36\37"[m]); } static void add_days(DATE *d, int nd) { for (;nd>0;nd--) { if (d->day >= monthdays(d->month,d->year)) { d->day = 1; if (d->month >= 12) { d->month = 1; d->year ++; } else { d->month ++; } } else { d->day ++; } } for (;nd<0;nd++) { d->day --; if (d->day < 1) { d->month --; if (d->month < 1) { d->year --; d->month = 12; } d->day = monthdays(d->month,d->year); } } } static void init_dates(void) { long int now; struct tm *tm; time(&now); tm = localtime(&now); todaydate.year = tm->tm_year + 1900; todaydate.month = tm->tm_mon + 1; todaydate.day = tm->tm_mday; date.year = 0; date.month = 0; date.day = 0; } static void init_currency(void) { currlen = strlen(def_curr); bcopy(def_curr,&currstr[0],currlen+1); } static void set_dates(void) { DATE t; if (date.year == 0) { date = todaydate; return; } t = date; add_days(&t,90); if ( (t.year > todaydate.year) || ( (t.year == todaydate.year) && ( (t.month > todaydate.month) || ( (t.month == todaydate.month) && (t.day >= todaydate.day) ) ) ) ) { date = todaydate; } } static int def_rls_approve(int c) { return(printable(c)); } static void ESC_confirm(const char *pfx, int pfxlen) { char *tmp; int c; tmp = malloc(pfxlen+17); bcopy(pfx,tmp,pfxlen); bcopy(" Quit now? [yn] ",tmp+pfxlen,17); while (1) { ul_update(tmp,pfxlen+17,pfxlen+17); c = cbi_getc(); switch (c) { case 'y': unset_lock(db_lock); ul_update(0,0,0); ul_end(); cbi_end(); exit(0); break; case 'n': free(tmp); return; break; default: ul_beep(); break; } } } static unsigned int ncmatch(const char *s1, int s1l, const char *s2, int s2l) { int i; int l; l = (s1l < s2l) ? s1l : s2l; for (i=0;(inexp;i++) { if (ncmatch(dbp,curs,exp->ents[i]->str,exp->ents[i]->len) == curs) { if (tab0 < 0) { tab0 = i; tabpt = exp->ents[i]->len; } else { tabpt = ncmatch(exp->ents[i]->str,exp->ents[i]->len,exp->ents[tab0]->str,tabpt); } if (exp->ents[i]->freq >= expthresh) { if (! any_r) { any_r = 1; nmatch = 0; } } else { if (any_r) continue; } if (nmatch == 0) { ent0 = i; explen = exp->ents[i]->len; } else if (! firstmatch) { explen = ncmatch(exp->ents[i]->str,exp->ents[i]->len,exp->ents[ent0]->str,explen); } nmatch ++; } } } if (nmatch > 0) { bcopy(exp->ents[ent0]->str,dbp,explen); len = explen; } else { if ((curs > 0) && (flags & RES_RESTRICT)) { curs --; ul_beep(); continue; } if (len > curs) { len = curs; } } ul_update(dbuf,pfxlen+len,pfxlen+curs); c = cbi_getc(); if (imap) c = (*imap)(c); switch (c) { case ESC: ESC_confirm(dbuf,pfxlen+len); break; case CTRL_L: ul_new(); break; case TAB: if ((tabpt < 0) || tabtodot) tabpt = len; while ((curs < tabpt) && (dbp[curs] != '.')) curs ++; if (curs < tabpt) curs ++; break; case 31: /* ^_ */ ul_new(); if (exp) { int n; for (i=0;inexp;i++) { if (ncmatch(dbp,curs,exp->ents[i]->str,exp->ents[i]->len) == curs) { n ++; printf("%12.5f ",exp->ents[i]->freq); fwrite(exp->ents[i]->str,1,exp->ents[i]->len,stdout); putchar('\n'); } } if (n == 0) { printf(" No matching expansions.\n"); } } else { printf(" No expansions.\n"); } break; case '\b': case '\177': if (curs > 0) { curs --; len = curs; } else if (flags & RES_DELBACK) { *term = c; *lenp = 0; return; } break; case ':': /* all uses want to suppress :s */ ul_beep(); break; case '?': case '*': ul_new(); if (help) (*help)(); if (exp) { int n; int longest; int col; int gcol; int dotoff; char *dot; int l; const char **pstrs; int *plens; int np; int p; const char *s; n = 0; longest = 0; if (c == '?') { dot = brindex(dbp,curs,'.'); dotoff = dot ? (dot+1)-dbp : 0; } else { dotoff = 0; } pstrs = malloc(exp->nexp*sizeof(const char *)); plens = malloc(exp->nexp*sizeof(int)); np = 0; for (i=0;inexp;i++) { s = exp->ents[i]->str; l = exp->ents[i]->len; if (ncmatch(dbp,curs,s,l) == curs) { n ++; if (c == '?') { dot = bindex(s+curs,l-curs,'.'); l = (dot ? dot-s : l) - dotoff; } if (l > longest) longest = l; } } if (n > 0) { printf("Possible expansions:"); if (debugging) { putchar('\n'); for (i=0;inexp;i++) { s = exp->ents[i]->str; l = exp->ents[i]->len; if (ncmatch(dbp,curs,s,l) == curs) { printf(" "); fwrite(s,1,l,stdout); printf(" %g\n",exp->ents[i]->freq); } } } else { gcol = 80; for (i=0;inexp;i++) { s = exp->ents[i]->str; l = exp->ents[i]->len; if (ncmatch(dbp,curs,s,l) == curs) { if (c == '?') { dot = bindex(s+curs,l-curs,'.'); l = (dot ? dot-s : l) - dotoff; } s += dotoff; for (p=np-1;p>=0;p--) if ((l == plens[p]) && !bcmp(s,pstrs[p],l)) break; if (p >= 0) continue; plens[np] = l; pstrs[np] = s; np ++; if (gcol+longest+2 > 80) { printf("\n "); col = 4; gcol = 4; } for (;col= maxlen) { if (flags & RES_SCROLL) { bcopy(dbp+1,dbp,maxlen-1); dbp[maxlen-1] = c; } else { ul_beep(); } } else { dbp[curs++] = c; } } else { case EOF: case '\r': case '\n': *term = c; *lenp = len; bcopy(dbp,buf,len); free(dbuf); return; } break; } if (len < curs) len = curs; } } static inline void read_limited_str( const char *pfx, int pfxlen, char *buf, int *lenp, int maxlen, int (*imap)(int), int (*approve)(int), void (*help)(void), int *term, unsigned int flags ) { read_expanded_str(pfx,pfxlen,buf,lenp,maxlen,imap,approve,help,term,0,flags); } static int imap_uc(int c) { return((isascii(c)&&islower(c))?toupper(c):c); } static int digitck(int c) { return((c >= '0') && (c <= '9')); } static int nonwhiteck(int c) { return((c != ' ') && printable(c)); } static int parse_datestr(int *dp, int *mp, int *yp, const DATE *def, const char *s, int l) { char tmp[5]; int err; int d; int m; int y; d = def->day; m = def->month; y = def->year; if ((l > 8) || (l < 1)) { *dp = d; *mp = m; *yp = y; return(1); } err = 0; if (l > 4) { sprintf(&tmp[0],"%04d",def->year); bcopy(s,&tmp[8-l],l-4); y = atoi(&tmp[0]); if (y > MAXYEAR) { y = MAXYEAR; err ++; } s += l - 4; l = 4; } if (l > 2) { m = s[l-3] - '0'; if (l > 3) m += (s[l-4] - '0') * 10; if (m < 1) { m = 1; err ++; } else if (m > 12) { m = 12; err ++; } s += l - 2; l = 2; } d = s[l-1] - '0'; if (l > 1) d += (s[l-2] - '0') * 10; if (d < 1) { d = 1; err ++; } else if (d > monthdays(m,y)) { d = monthdays(m,y); err ++; } *dp = d; *mp = m; *yp = y; return(err); } static void read_date(const char *pfx, int pfxlen, DATE *dp) { char *lbuf; char *lp; int c; lbuf = malloc(pfxlen+15); bcopy(pfx,lbuf,pfxlen); lp = lbuf + pfxlen; while (1) { sprintf(lp,"%04d %02d %02d -> ",dp->year,dp->month,dp->day); ul_update(lbuf,pfxlen+10,pfxlen+10); c = cbi_getc(); switch (c) { case EOF: case ' ': case '\r': case '\n': free(lbuf); return; break; case ESC: ESC_confirm(lbuf,pfxlen+10); break; case CTRL_L: ul_new(); break; case '+': case '=': add_days(dp,1); break; case '-': case '_': add_days(dp,-1); break; case '0' ... '9': { int y; int m; int d; int l; int err; char strbuf[8]; strbuf[0] = c; l = 1; read_limited_str(lbuf,pfxlen+14,&strbuf[0],&l,8,0,&digitck,0,&c,RES_SCROLL); err = parse_datestr(&d,&m,&y,dp,&strbuf[0],l); dp->year = y; dp->month = m; dp->day = d; if (err) ul_beep(); } break; default: ul_beep(); break; } } } static void read_currency(char *buf, int len) { int c; char *t; char curedit[80]; int celen; if (currexp.nexp <= 1) { ul_beep(); return; } t = malloc(len+12); bcopy(buf,t,len); bcopy(" Currency> ",t+len,12); celen = 0; #if 0 celen = currlen; bcopy(&currstr[0],&curedit[0],celen); #endif read_expanded_str(t,len+12,&curedit[0],&celen,sizeof(curedit),imap_uc,nonwhiteck,0,&c,&currexp,RES_DELBACK|RES_RESTRICT); if ((c == '\b') || (c == '\177')) { } else if (celen == 0) { if (def_curr) { currlen = strlen(def_curr); bcopy(def_curr,&currstr[0],currlen+1); } else { ul_beep(); } } else { currlen = celen; bcopy(&curedit[0],&currstr[0],celen); currstr[currlen] = '\0'; } free(t); } static void read_amount(const char *pfx, int pfxlen, long int *amtp) { char *buf; char *bp; int bufhave; int len; unsigned long int amt; int neg; int amtlen; int c; buf = 0; bufhave = 0; amt = ABS(*amtp); neg = *amtp < 0; while (1) { amtlen = format_amount(0,neg,amt,&currstr[0]); if (bufhave < pfxlen+amtlen) { bufhave = pfxlen + amtlen; free(buf); buf = malloc(bufhave+1); bcopy(pfx,buf,pfxlen); bp = buf + pfxlen; } format_amount(bp,neg,amt,&currstr[0]); len = pfxlen + strlen(bp); ul_update(buf,len,len); c = cbi_getc(); switch (c) { case '0' ... '9': amt = (amt * 10) + (c - '0'); break; case '-': neg = ! neg; break; case '$': read_currency(buf,len); break; case '\b': case '\177': amt /= 10; break; case '\r': case '\n': case ' ': *amtp = neg ? -(long int)amt : amt; return; break; case ESC: ESC_confirm(buf,len); break; case CTRL_L: ul_new(); break; } } } static const char *flagchars(int f) { static char buf[32]; char *bp; bp = &buf[0]; if (f & XF_PROTECT) *bp++ = 'p'; if (f & XF_SHARED) *bp++ = 's'; switch (xflags & XF_CONFMASK) { case XF_CONF_nil: break; case XF_CONF_c: *bp++ = 'c'; break; case XF_CONF_cc: *bp++ = 'c'; *bp++ = 'c'; break; case XF_CONF_C: *bp++ = 'C'; break; case XF_CONF_Cc: *bp++ = 'C'; *bp++ = 'c'; break; case XF_CONF_cC: *bp++ = 'c'; *bp++ = 'C'; break; case XF_CONF_CC: *bp++ = 'C'; *bp++ = 'C'; break; default: sprintf(bp,"?<%x>",xflags&XF_CONFMASK); bp += strlen(bp); break; } if (f & XF_XCURR) *bp++ = 'x'; *bp = '\0'; return(&buf[0]); } static int nflagchars(int f) { return(strlen(flagchars(f))); } static void write_entry(void) { char *buf; int len; char *bp; int fd; int wrote; if (debugging) { printf("\n[writing: %d%02d%02d:%d%02d%02d:%s:",date.year,date.month,date.day,todaydate.year,todaydate.month,todaydate.day,flagchars(xflags)); fwrite(&tagstr[0],1,taglen,stdout); putchar(':'); fwrite(&fromstr[0],1,fromlen,stdout); putchar(':'); fwrite(&tostr[0],1,tolen,stdout); printf(":%ld:",amount); fwrite(&currstr[0],1,currlen,stdout); putchar(']'); fflush(stdout); } explist_decay(); len = 8 + 1 + 8 + 1 + nflagchars(xflags) + 1 + taglen + 1 + ((cats[tagcatno].dir == CD_IN) ? 0 : fromlen) + 1 + ((cats[tagcatno].dir == CD_OUT) ? 0 : tolen) + 1 + (amount<0) + intlog10(ABS(amount)) + 1 + currlen + 1; buf = malloc(len+1); sprintf(buf,"%d%02d%02d:%d%02d%02d:%s:",date.year,date.month,date.day,todaydate.year,todaydate.month,todaydate.day,flagchars(xflags)); bp = buf + strlen(buf); bcopy(&tagstr[0],bp,taglen); bp += taglen; add_expansion(cats[tagcatno].subtags,&tagstr[cats[tagcatno].len+1],taglen-cats[tagcatno].len-1); *bp++ = ':'; if (cats[tagcatno].dir != CD_IN) { bcopy(&fromstr[0],bp,fromlen); bp += fromlen; add_expansion(&ftexp,&fromstr[0],fromlen); } *bp++ = ':'; if (cats[tagcatno].dir != CD_OUT) { bcopy(&tostr[0],bp,tolen); bp += tolen; add_expansion(&ftexp,&tostr[0],tolen); } sprintf(bp,":%ld:",amount); bp += strlen(bp); bcopy(&currstr[0],bp,currlen); bp += currlen; add_expansion(&currexp,&currstr[0],currlen); *bp++ = '\n'; if (bp-buf != len) { ul_new(); printf("[Warning: mis-estimated entry length: est %d act %d]\n",len,(int)(bp-buf)); len = bp - buf; } fd = open(db_file,O_WRONLY|O_APPEND|O_CREAT,0666); if (fd < 0) { fprintf(stderr,"%s: can't record transaction: can't open db file %s: %s\n",__progname,db_file,strerror(errno)); exit(1); } else { wrote = write(fd,buf,len); if (wrote < 0) { fprintf(stderr,"%s: error writing to %s: %s\n",__progname,db_file,strerror(errno)); exit(1); } else if (wrote != len) { fprintf(stderr,"%s: error writing to %s: wrote %d not %d\n",__progname,db_file,wrote,len); exit(1); } if (close(fd) < 0) { fprintf(stderr,"%s: error closing %s: %s\n",__progname,db_file,strerror(errno)); exit(1); } } } static void format_entry(char *buf, int *lenp, int maxlen __attribute__((__unused__))) { char *bp; /* XXX should check all against maxlen */ bp = buf; if (! HAVE(DATE)) *bp++ = '('; sprintf(bp,"%04d %02d %02d",date.year,date.month,date.day); bp += 10; if (! HAVE(DATE)) *bp++ = ')'; *bp++ = ':'; *bp++ = ' '; if (xflags) { *bp++ = '['; strcpy(bp,flagchars(xflags)); bp += strlen(bp); *bp++ = ']'; *bp++ = ' '; } if (HAVE(TAG)) { bcopy(&tagstr[0],bp,taglen); bp += taglen; } else { bcopy("(category?)",bp,11); bp += 11; } *bp++ = ':'; *bp++ = ' '; if ( (HAVE(TAG) && (cats[tagcatno].dir == CD_IN)) || (HAVE(FROM) && (fromlen == 0)) ) { bcopy("(world)",bp,7); bp += 7; } else if (HAVE(FROM)) { bcopy(&fromstr[0],bp,fromlen); bp += fromlen; } else { bcopy("(from?)",bp,7); bp += 7; } *bp++ = ' '; *bp++ = '-'; *bp++ = '>'; *bp++ = ' '; if (HAVE(AMOUNT)) { format_amount(bp,amount<0,(amount<0)?-amount:amount,&currstr[0]); } else { format_unknown_amount(bp,&currstr[0]); } bp += strlen(bp); *bp++ = ' '; *bp++ = '-'; *bp++ = '>'; *bp++ = ' '; if ( (HAVE(TAG) && (cats[tagcatno].dir == CD_OUT)) || (HAVE(TO) && (tolen == 0)) ) { bcopy("(world)",bp,7); bp += 7; } else if (HAVE(TO)) { bcopy(&tostr[0],bp,tolen); bp += tolen; } else { bcopy("(to?)",bp,5); bp += 5; } *lenp = bp - buf; } static void read_tag( const char *pfx, int pfxlen, char *tagbuf, int *taglenp, int *catnop, int maxlen ) { char *buf; char *buf0; int buflen; int l; int ch; int c; int i; int n; int nmatch; int cat0; int explen; buflen = pfxlen + maxlen + 1; buf0 = malloc(buflen); bcopy(pfx,buf0,pfxlen); buf = buf0 + pfxlen; l = *taglenp; bcopy(tagbuf,buf,l); c = l; while (1) { explen = 0; nmatch = 0; cat0 = -1; for (i=0;i explen) explen = n; } } else { if (nmatch == 0) { cat0 = i; explen = cats[i].len; } else if (! firstmatch) { explen = ncmatch(cats[i].name,cats[i].len,cats[cat0].name,explen); } nmatch ++; } } if (nmatch == 0) { if (cat0 < 0) { ul_beep(); l = explen; c = explen; continue; } else { l -= cats[cat0].len + 1; read_expanded_str( buf0, pfxlen+cats[cat0].len+1, buf+cats[cat0].len+1, &l, maxlen-cats[0].len-1, 0, 0, 0, &i, cats[cat0].subtags, RES_DELBACK ); if ((i == '\b') || (i == '\177')) { c = cats[cat0].len; l = c; continue; } else { l += cats[cat0].len + 1; while ((l > 0) && (buf[l-1] == ' ')) l --; bcopy(buf,tagbuf,l); *taglenp = l; *catnop = cat0; return; } } } else if ((c == 0) || (buf[c-1] == '.')) { l = c; } else { char *dotp; dotp = bindex(cats[cat0].name+c,cats[cat0].len-c,'.'); if (dotp && (explen > dotp-cats[cat0].name)) explen = (dotp - cats[cat0].name) + 1; bcopy(cats[cat0].name,buf,explen); l = explen; } ul_update(buf0,pfxlen+l,pfxlen+c); ch = cbi_getc(); switch (ch) { case ' ': case '.': case '\r': case '\n': case '\t': if (l == cats[cat0].len) { c = l; buf[c++] = '.'; } else if (l > c) { c = l; } else { ul_beep(); } break; case '?': case '*': { char *matches; int *partlen; int dotbase; char *cp; int maxpart; int col; int gcol; cp = brindex(buf,c,'.'); dotbase = cp ? ((cp+1)-buf) : 0; matches = malloc(ncat*sizeof(*matches)); partlen = malloc(ncat*sizeof(*partlen)); for (i=0;i= c); } maxpart = 0; if (ch == '?') { int j; for (i=0;i maxpart) maxpart = partlen[i]; for (j=0;j maxpart) maxpart = partlen[i]; } } } ul_new(); printf("Possibilities:"); gcol = 80; for (i=0;i 80) { printf("\n "); col = 4; gcol = 4; } for (;col 0) { c --; } else { *taglenp = 0; return; } break; case ':': ul_beep(); break; default: if (printable(ch)) buf[c++] = ch; else ul_beep(); break; } if (c > maxlen) { c = maxlen; ul_beep(); } if (l < c) l = c; } } static void accept_entry(void) { char lsf[256]; int lsfl; int c; int autoprompt_cmd; ul_new(); cathave = 0; have = 0; xflags = 0; autoprompt_cmd = 0; while (1) { format_entry(&lsf[0],&lsfl,sizeof(lsf)); c = '\0'; if (autoprompt) { if (! HAVE(AMOUNT)) c = 'a'; if (! HAVE(TO)) c = 't'; if (! HAVE(FROM)) c = 'f'; if (! HAVE(TAG)) c = 'c'; if (! HAVE(DATE)) c = 'd'; if (autoprompt_cmd) c = autoprompt_cmd; autoprompt_cmd = 0; } if (c == '\0') { int l; sprintf(&lsf[lsfl]," [Choose: dcfatSCX%s?]",HAVE(ALL)?"./!":""); l = lsfl + strlen(&lsf[lsfl]); ul_update(&lsf[0],l,lsfl+1); c = cbi_getc(); } switch (c) { case '?': ul_new(); printf("Choose:\n"); printf("d - to %s date\n",HAVE(DATE)?"change":"set"); printf("c - to %s category tag\n",HAVE(TAG)?"change":"set"); printf("f - to %s \"from\" column heading\n",HAVE(FROM)?"change":"set"); printf("a - to %s amount\n",HAVE(AMOUNT)?"change":"set"); printf("t - to %s \"to\" column heading\n",HAVE(TO)?"change":"set"); if (currexp.nexp > 1) printf("$ - to change currency\n"); printf("S - to toggle \"shared\" flag\n"); printf("C - to cycle confirmation flags\n"); printf("X - to toggle \"exchange\" flag\n"); if (HAVE(ALL)) { printf(". - to enter the transaction\n"); printf("/ - to split the transaction\n"); printf("! - to clone the transaction\n"); } printf("Or ESC at any point to quit\n"); break; case 'd': bcopy(" ",&lsf[lsfl],2); read_date(&lsf[0],lsfl+2,&date); SETHAVE(DATE); break; case 'c': bcopy(" Category> ",&lsf[lsfl],12); if (! HAVE(TAG)) taglen = 0; read_tag(&lsf[0],lsfl+12,&tagstr[0],&taglen,&tagcatno,sizeof(tagstr)); if (taglen > 0) { SETHAVE(TAG); switch (cats[tagcatno].dir) { case CD_IN: cathave = HAVE_FROM; break; case CD_OUT: cathave = HAVE_TO; break; case CD_TRANS: cathave = 0; break; } } else { CLRHAVE(TAG); cathave = 0; autoprompt_cmd = 'd'; } break; case 'f': bcopy(" From> ",&lsf[lsfl],8); if (! HAVE(FROM)) fromlen = 0; read_expanded_str(&lsf[0],lsfl+8,&fromstr[0],&fromlen,sizeof(fromstr),0,nonwhiteck,0,&c,&ftexp,RES_DELBACK); if ((c == '\b') || (c == '\177')) { CLRHAVE(FROM); autoprompt_cmd = 'c'; } else { SETHAVE(FROM); } break; case 'a': bcopy(" ",&lsf[lsfl],2); if (! HAVE(AMOUNT)) amount = 0; read_amount(&lsf[0],lsfl+2,&amount); if (amount != 0) { SETHAVE(AMOUNT); } else { CLRHAVE(AMOUNT); autoprompt_cmd = CATHAVE(TO) ? 'f' : 't'; } break; case 't': bcopy(" To> ",&lsf[lsfl],6); if (! HAVE(TO)) tolen = 0; read_expanded_str(&lsf[0],lsfl+6,&tostr[0],&tolen,sizeof(tostr),0,nonwhiteck,0,&c,&ftexp,RES_DELBACK); if ((c == '\b') || (c == '\177')) { CLRHAVE(TO); autoprompt_cmd = CATHAVE(FROM) ? 'c' : 'f'; } else { SETHAVE(TO); } break; case '$': read_currency(&lsf[0],lsfl); break; case 'S': xflags ^= XF_SHARED; break; case 'C': { int f; switch (xflags & XF_CONFMASK) { case XF_CONF_nil: f = XF_CONF_c; break; case XF_CONF_c: f = XF_CONF_cc; break; case XF_CONF_cc: f = XF_CONF_C; break; case XF_CONF_C: f = XF_CONF_Cc; break; case XF_CONF_Cc: f = XF_CONF_cC; break; case XF_CONF_cC: f = XF_CONF_CC; break; case XF_CONF_CC: f = XF_CONF_nil; break; default: f = XF_CONF_nil; break; } xflags = (xflags & ~XF_CONFMASK) | f; } break; case 'P': xflags ^= XF_PROTECT; break; case 'X': xflags ^= XF_XCURR; break; case '.': if (HAVE(ALL)) { ul_update(&lsf[0],lsfl,lsfl); write_entry(); return; } else { ul_beep(); } break; case '/': if (HAVE(ALL)) { long int splitamt; bcopy(" Split off ",&lsf[lsfl],12); splitamt = 0; read_amount(&lsf[0],lsfl+12,&splitamt); if (splitamt && (ABS(splitamt) < amount)) { if (splitamt < 0) splitamt += amount; amount -= splitamt; format_entry(&lsf[0],&lsfl,sizeof(lsf)); ul_update(&lsf[0],lsfl,lsfl); ul_new(); write_entry(); amount = splitamt; } else { ul_beep(); } } else { ul_beep(); } break; case '!': if (HAVE(ALL)) { ul_update(&lsf[0],lsfl,lsfl); ul_new(); write_entry(); } break; case ESC: ESC_confirm(&lsf[0],lsfl); break; case CTRL_L: ul_new(); break; default: ul_beep(); break; } } } int main(int, char **); int main(int ac, char **av) { init_explist(&ftexp); init_explist(&currexp); conf_if = conf_newstate(); conf_db = conf_newstate(); handleargs(ac,av,1); handleargs(ac,av,0); conf_check_env(conf_if,"MDB_CONFIG_IF",0); conf_check_env(conf_db,"MDB_CONFIG_DB",0); check_config(); init_categories(); load_categories(cat_file); finish_category_load(); init_dates(); check_tags(); db_lock = set_lock(db_file,getpid(),1); scan_db(db_file,0); init_currency(); set_dates(); cbi_init(0,0); ul_init(1,0); while (1) accept_entry(); }