/* $Header: /cvs/src/tdl/main.c,v 1.39.2.6 2004/02/03 22:17:22 richard Exp $ tdl - A console program for managing to-do lists Copyright (C) 2001-2004 Richard P. Curnow This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 2 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA */ #include "tdl.h" #include #include #include #include #include #include #include #include #include #ifdef USE_DOTLOCK #include #include #endif /* The name of the database file (in whichever directory it may be) */ #define DBNAME ".tdldb" /* Set if db doesn't exist in this directory */ static char *current_database_path = NULL; /* The currently loaded database */ struct links top; /* Flag for whether data is actually loaded yet */ static int is_loaded = 0; #ifdef USE_DOTLOCK static char *lock_file_name = NULL; #endif /* Flag if currently loaded database has been changed and needs writing back to * the filesystem */ static int currently_dirty = 0; /* Whether to complain about problems with file operations */ static int is_noisy = 1; static int is_interactive = 0; static void set_descendent_priority(struct node *x, enum Priority priority)/*{{{*/ { struct node *y; for (y = x->kids.next; y != (struct node *) &x->kids; y = y->chain.next) { y->priority = priority; set_descendent_priority(y, priority); } } /*}}}*/ /* This will be variable eventually */ static char default_database_path[] = "./" DBNAME; #ifdef USE_DOTLOCK static void unlock_database(void)/*{{{*/ { if (lock_file_name) unlink(lock_file_name); return; } /*}}}*/ static volatile void unlock_and_exit(int code)/*{{{*/ { unlock_database(); exit(code); } /*}}}*/ static void lock_database(char *path)/*{{{*/ { struct utsname uu; struct passwd *pw; int pid; int len; char *tname; struct stat sb; FILE *out; if (uname(&uu) < 0) { perror("uname"); exit(1); } pw = getpwuid(getuid()); if (!pw) { perror("getpwuid"); exit(1); } pid = getpid(); len = 1 + strlen(path) + 5; lock_file_name = new_array(char, len); sprintf(lock_file_name, "%s.lock", path); len += strlen(uu.nodename); /* add on max width of pid field (allow up to 32 bit pid_t) + 2 '.' chars */ len += (10 + 2); tname = new_array(char, len); sprintf(tname, "%s.%d.%s", lock_file_name, pid, uu.nodename); out = fopen(tname, "w"); if (!out) { fprintf(stderr, "Cannot open lock file %s for writing\n", tname); exit(1); } fprintf(out, "%d,%s,%s\n", pid, uu.nodename, pw->pw_name); fclose(out); if (link(tname, lock_file_name) < 0) { /* check if link count==2 */ if (stat(tname, &sb) < 0) { fprintf(stderr, "Could not stat the lock file\n"); unlink(tname); exit(1); } else { if (sb.st_nlink != 2) { FILE *in; in = fopen(lock_file_name, "r"); if (in) { char line[2048]; fgets(line, sizeof(line), in); line[strlen(line)-1] = 0; /* strip trailing newline */ fprintf(stderr, "Database %s appears to be locked by (pid,node,user)=(%s)\n", path, line); unlink(tname); exit(1); } } else { /* lock succeeded apparently */ } } } else { /* lock succeeded apparently */ } unlink(tname); free(tname); return; } /*}}}*/ #else static volatile void unlock_and_exit(int code)/*{{{*/ { exit(code); } /*}}}*/ #endif /* USE_DOTLOCK */ static char *get_database_path(int traverse_up)/*{{{*/ { char *env_var; env_var = getenv("TDL_DATABASE"); if (env_var) { return env_var; } else { int at_root, orig_size, size, dbname_len, found, stat_result; char *orig_cwd, *cwd, *result, *filename; struct stat statbuf; dbname_len = strlen(DBNAME); size = 16; orig_size = 16; found = 0; at_root = 0; cwd = new_array(char, size); orig_cwd = new_array(char, orig_size); do { result = getcwd(orig_cwd, orig_size); if (!result) { if (errno == ERANGE) { orig_size <<= 1; orig_cwd = grow_array(char, orig_size, orig_cwd); } else { fprintf(stderr, "Unexpected error reading current directory\n"); unlock_and_exit(1); } } } while (!result); filename = new_array(char, size + dbname_len + 2); filename[0] = 0; do { result = getcwd(cwd, size); if (!result && (errno == ERANGE)) { size <<= 1; cwd = grow_array(char, size, cwd); filename = grow_array(char, size + dbname_len + 2, filename); } else { if (!strcmp(cwd, "/")) { at_root = 1; } strcpy(filename, cwd); strcat(filename, "/"); strcat(filename, DBNAME); stat_result = stat(filename, &statbuf); if ((stat_result >= 0) && (statbuf.st_mode & 0600)) { found = 1; break; } if (!traverse_up) break; /* Otherwise, go up a level */ chdir (".."); } } while (!at_root); free(cwd); /* Reason for this : if using create in a subdirectory of a directory * already containing a .tdldb, the cwd after the call here from main would * get left pointing at the directory containing the .tdldb that already * exists, making the call here from process_create() fail. So go back to * the directory where we started. */ chdir(orig_cwd); free(orig_cwd); if (found) { return filename; } else { return default_database_path; } } } /*}}}*/ static void rename_database(char *path)/*{{{*/ { int len; char *pathbak; len = strlen(path); pathbak = new_array(char, len + 5); strcpy(pathbak, path); strcat(pathbak, ".bak"); if (rename(path, pathbak) < 0) { if (is_noisy) { perror("warning, couldn't save backup database:"); } } free(pathbak); return; } /*}}}*/ static char *executable_name(char *argv0)/*{{{*/ { char *p; for (p=argv0; *p; p++) ; for (; p>=argv0; p--) { if (*p == '/') return (p+1); } return argv0; } /*}}}*/ static void load_database(char *path) /*{{{*/ /* Return 1 if successful, 0 if no database was found */ { FILE *in; currently_dirty = 0; #ifdef USE_DOTLOCK lock_database(path); #endif in = fopen(path, "rb"); if (in) { /* Database may not exist, e.g. if the program has never been run before. */ read_database(in, &top); fclose(in); is_loaded = 1; } else { if (is_noisy) { fprintf(stderr, "warning: no database found above this directory\n"); } } } /*}}}*/ void load_database_if_not_loaded(void)/*{{{*/ { if (!is_loaded) { load_database(current_database_path); is_loaded = 1; } } /*}}}*/ static mode_t get_mode(const char *path)/*{{{*/ { mode_t result; const mode_t default_result = 0600; /* access to user only. */ struct stat sb; if (stat(path, &sb) < 0) { result = default_result; } else { if (!S_ISREG(sb.st_mode)) { fprintf(stderr, "Warning : existing database is not a regular file!\n"); result = default_result; } else { result = sb.st_mode & (S_IRWXU | S_IRWXG | S_IRWXO); } } return result; } /*}}}*/ static void save_database(char *path)/*{{{*/ { FILE *out; int out_fd; mode_t database_mode; if (is_loaded && currently_dirty) { database_mode = get_mode(path); /* The next line only used to happen if the command wasn't 'create'. * However, it should quietly fail for create, where the existing database * doesn't exist */ rename_database(path); /* Open database this way so that the permissions from the existing database can be duplicated onto the new one in way free of race conditions. */ out_fd = open(path, O_WRONLY | O_CREAT | O_EXCL, database_mode); if (out_fd < 0) { fprintf(stderr, "Could not open new database %s for writing : %s\n", path, strerror(errno)); unlock_and_exit(1); } else { /* Normal case */ out = fdopen(out_fd, "wb"); } if (!out) { fprintf(stderr, "Cannot open database %s for writing\n", path); unlock_and_exit(1); } write_database(out, &top); fclose(out); } currently_dirty = 0; return; } /*}}}*/ void free_database(struct links *x)/*{{{*/ { struct node *y; struct node *next; for (y=x->next; y != (struct node *) x; y = next) { free_database(&y->kids); free(y->text); next = y->chain.next; free(y); } x->next = x->prev = (struct node *) x; } /*}}}*/ static char *get_version(void)/*{{{*/ { static char buffer[256]; static char cvs_version[] = "$Name: V1_5_2 $"; char *p, *q; for (p=cvs_version; *p; p++) { if (*p == ':') { p++; break; } } while (isspace(*p)) p++; if (*p == '$') { strcpy(buffer, "development version"); } else { for (q=buffer; *p && *p != '$'; p++) { if (!isspace(*p)) { if (*p == '_') *q++ = '.'; else *q++ = *p; } } *q = 0; } return buffer; } /*}}}*/ /* {{{ One line descriptions of the subcommands */ static char desc_above[] = "Move entries above (before) another entry"; static char desc_add[] = "Add a new entry to the database"; static char desc_after[] = "Move entries after (below) another entry"; static char desc_before[] = "Move entries before (above) another entry"; static char desc_below[] = "Move entries below (after) another entry"; static char desc_clone[] = "Make deep copy of one or more entries"; static char desc_copyto[] = "Insert deep copy of one or more entries under another entry"; static char desc_create[] = "Create a new database in the current directory"; static char desc_defer[] = "Put off starting some tasks until a given time"; static char desc_delete[] = "Remove 1 or more entries from the database"; static char desc_done[] = "Mark 1 or more entries as done"; static char desc_edit[] = "Change the text of an entry"; static char desc_exit[] = "Exit program, saving database"; static char desc_export[] = "Export entries to another database"; static char desc_help[] = "Display help information"; static char desc_import[] = "Import entries from another database"; static char desc_ignore[] = "Postpone or partially remove 1 or more entries"; static char desc_into[] = "Move entries to end of new parent"; static char desc_list[] = "List entries in database (default from top node)"; static char desc_log[] = "Add a new entry to the database, mark it done as well"; static char desc_moveto[] = "Move entries to end of new parent"; static char desc_narrow[] = "Restrict actions to part of the database"; static char desc_open[] = "Move one or more entries out of postponed/deferred state"; static char desc_postpone[] = "Make one or more entries postponed indefinitely"; static char desc_priority[] = "Change the priority of 1 or more entries"; static char desc_purge[] = "Remove old done entries in subtrees"; static char desc_quit[] = "Exit program, NOT saving database"; static char desc_remove[] = "Remove 1 or more entries from the database"; static char desc_report[] = "Report completed tasks in interval"; static char desc_revert[] = "Discard changes and reload previous database from disc"; static char desc_save[] = "Save the database back to disc and keep working"; static char desc_undo[] = "Mark 1 or more entries as not done (cancel effect of 'done')"; static char desc_usage[] = "Display help information"; static char desc_version[] = "Display program version"; static char desc_which[] = "Display filename of database being used"; static char desc_widen[] = "Widen the part of the database to which actions apply"; /* }}} */ /* {{{ Synopsis of each subcommand */ static char synop_above[] = " ..."; static char synop_add[] = "[@] [] [] "; static char synop_after[] = " ..."; static char synop_before[] = " ..."; static char synop_below[] = " ..."; static char synop_clone[] = " ..."; static char synop_copyto[] = " ..."; static char synop_create[] = ""; static char synop_defer[] = "[@] {...] ..."; static char synop_delete[] = "[...] ..."; static char synop_done[] = "[@] [...] ..."; static char synop_edit[] = " []"; static char synop_exit[] = ""; static char synop_export[] = " ..."; static char synop_help[] = "[]"; static char synop_ignore[] = "[...] ..."; static char synop_import[] = ""; static char synop_into[] = " ..."; static char synop_list[] = "[-v] [-a] [-p] [-m] [-1..9] [] [|/...]\n" "-v : verbose (show dates, priorities etc)\n" "-a : show all entries, including 'done' ones\n" "-p : show deferred and postponed entries\n" "-m : don't use colours (monochrome)\n" "-1,-2,..,-9 : summarise (and don't show) entries below this depth\n" " : word to match on"; static char synop_log[] = "[@] [] [] "; static char synop_moveto[] = " ..."; static char synop_narrow[] = ""; static char synop_open[] = "[...] ..."; static char synop_postpone[] = "[...] ..."; static char synop_priority[] = " [...] ..."; static char synop_purge[] = " [ ...]"; static char synop_quit[] = ""; static char synop_remove[] = "[...] ..."; static char synop_report[] = " []\n" "(end defaults to now)"; static char synop_revert[] = ""; static char synop_save[] = ""; static char synop_undo[] = "[...] ..."; static char synop_usage[] = "[]"; static char synop_version[] = ""; static char synop_which[] = ""; static char synop_widen[] = "[]"; /* }}} */ static int process_create(char **x)/*{{{*/ { char *dbpath = get_database_path(0); struct stat sb; int result; result = stat(dbpath, &sb); if (result >= 0) { fprintf(stderr, "Can't create database <%s>, it already exists!\n", dbpath); return -1; } else { /* Should have an empty database, and the dirty flag will be set */ current_database_path = dbpath; /* don't emit complaint about not being able to move database to its backup */ is_noisy = 0; /* Force empty database to be written out */ is_loaded = currently_dirty = 1; return 0; } } /*}}}*/ static int process_priority(char **x)/*{{{*/ { int error; enum Priority priority; struct node *n; int do_descendents; priority = parse_priority(*x, &error); if (error < 0) { fprintf(stderr, "usage: priority %s\n", synop_priority); return error; } while (*++x) { do_descendents = include_descendents(*x); /* May modify *x */ n = lookup_node(*x, 0, NULL); if (!n) return -1; n->priority = priority; if (do_descendents) { set_descendent_priority(n, priority); } } return 0; }/*}}}*/ static int process_which(char **argv)/*{{{*/ { printf("%s\n", current_database_path); return 0; } /*}}}*/ static int process_version(char **x)/*{{{*/ { fprintf(stderr, "tdl %s\n", get_version()); return 0; } /*}}}*/ static int process_exit(char **x)/*{{{*/ { save_database(current_database_path); free_database(&top); unlock_and_exit(0); return 0; /* moot */ } /*}}}*/ static int process_quit(char **x)/*{{{*/ { /* Just get out quick, don't write the database back */ char ans[4]; if (currently_dirty) { printf(" WARNING: if you quit, all changes to database will be lost!\n" " Use command 'exit' instead of 'quit' if you wish to save data.\n" " Really quit [y/N]? "); fgets (ans, 4, stdin); if (strcasecmp(ans,"y\n") != 0) { printf(" Quit canceled.\n"); return 0; } } free_database(&top); unlock_and_exit(0); return 0; /* moot */ } /*}}}*/ static int process_save(char **x)/*{{{*/ { /* FIXME: I'm not sure whether the behaviour here should include renaming the * existing disc database to become the backup file. I think the precedent * would be how vi or emacs handle backup files when multiple saves are done * within a session. */ save_database(current_database_path); return 0; } /*}}}*/ static int process_revert(char **x)/*{{{*/ { if (is_loaded) { free_database(&top); } is_loaded = currently_dirty = 0; return 0; } /*}}}*/ /* Forward prototype */ static int usage(char **x); struct command cmds[] = {/*{{{*/ {"--help", NULL, usage, desc_help, NULL, NULL, 0, 0, 3, 0, 1}, {"-h", NULL, usage, desc_help, NULL, NULL, 0, 0, 2, 0, 1}, {"-V", NULL, process_version, desc_version, NULL, NULL, 0, 0, 2, 0, 1}, {"above", NULL, process_above, desc_above, synop_above, NULL, 1, 1, 2, 1, 1}, {"add", "tdla", process_add, desc_add, synop_add, NULL, 1, 1, 2, 1, 1}, {"after", NULL, process_below, desc_after, synop_after, NULL, 1, 1, 2, 1, 1}, {"before", NULL, process_above, desc_before, synop_before, NULL, 1, 1, 3, 1, 1}, {"below", NULL, process_below, desc_below, synop_below, NULL, 1, 1, 3, 1, 1}, {"clone", NULL, process_clone, desc_clone, synop_clone, NULL, 1, 1, 2, 1, 1}, {"copyto", NULL, process_copyto, desc_copyto, synop_copyto, NULL, 1, 1, 2, 1, 1}, {"create", NULL, process_create, desc_create, synop_create, NULL, 1, 0, 2, 0, 1}, {"defer", NULL, process_defer, desc_defer, synop_defer, NULL, 1, 1, 3, 1, 1}, {"delete", NULL, process_remove, desc_delete, synop_delete, NULL, 1, 1, 3, 1, 1}, {"done", "tdld", process_done, desc_done, synop_done, complete_done, 1, 1, 2, 1, 1}, {"edit", NULL, process_edit, desc_edit, synop_edit, NULL, 1, 1, 2, 1, 1}, {"exit", NULL, process_exit, desc_exit, synop_exit, NULL, 0, 0, 3, 1, 0}, {"export", NULL, process_export, desc_export, synop_export, NULL, 0, 1, 3, 1, 1}, {"help", NULL, usage, desc_help, synop_help, complete_help, 0, 0, 1, 1, 1}, {"ignore", NULL, process_ignore, desc_ignore, synop_ignore, complete_done, 1, 1, 2, 1, 1}, {"import", NULL, process_import, desc_import, synop_import, NULL, 1, 1, 2, 1, 1}, {"into", NULL, process_into, desc_into, synop_into, NULL, 1, 1, 2, 1, 1}, {"list", "tdll", process_list, desc_list, synop_list, complete_list, 0, 1, 2, 1, 1}, {"ls", NULL, process_list, desc_list, synop_list, complete_list, 0, 1, 2, 1, 1}, {"log", "tdlg", process_log, desc_log, synop_log, NULL, 1, 1, 2, 1, 1}, {"moveto", NULL, process_into, desc_moveto, synop_moveto, NULL, 1, 1, 1, 1, 1}, {"narrow", NULL, process_narrow, desc_narrow, synop_narrow, NULL, 0, 1, 1, 1, 0}, {"open", NULL, process_open, desc_open, synop_open, complete_open, 1, 1, 1, 1, 1}, {"postpone", NULL, process_postpone, desc_postpone,synop_postpone,complete_postpone, 1, 1, 2, 1, 1}, {"priority", NULL, process_priority, desc_priority,synop_priority,complete_priority, 1, 1, 2, 1, 1}, {"purge", NULL, process_purge, desc_purge, synop_purge, NULL, 1, 1, 2, 1, 1}, {"quit", NULL, process_quit, desc_quit, synop_quit, NULL, 0, 0, 1, 1, 0}, {"remove", NULL, process_remove, desc_remove, synop_remove, NULL, 1, 1, 3, 1, 1}, {"report", NULL, process_report, desc_report, synop_report, NULL, 0, 1, 3, 1, 1}, {"revert", NULL, process_revert, desc_revert, synop_revert, NULL, 0, 0, 3, 1, 0}, {"save", NULL, process_save, desc_save, synop_save, NULL, 0, 1, 1, 1, 0}, {"undo", NULL, process_undo, desc_undo, synop_undo, NULL, 1, 1, 2, 1, 1}, {"usage", NULL, usage, desc_usage, synop_usage, complete_help, 0, 0, 2, 1, 1}, {"version", NULL, process_version, desc_version, synop_version, NULL, 0, 0, 1, 1, 1}, {"which", NULL, process_which, desc_which, synop_which, NULL, 0, 0, 2, 1, 1}, {"widen", NULL, process_widen, desc_widen, synop_widen, NULL, 0, 1, 2, 1, 0} };/*}}}*/ int n_cmds = 0; #define N(x) (sizeof(x) / sizeof(x[0])) static int is_processing = 0; static int was_signalled = 0; static void handle_signal(int a)/*{{{*/ { was_signalled = 1; /* And close stdin, which should cause readline() in inter.c to return * immediately if it was active when the signal arrived. */ close(0); } /*}}}*/ static void guarded_sigaction(int signum, struct sigaction *sa)/*{{{*/ { if (sigaction(signum, sa, NULL) < 0) { perror("sigaction"); unlock_and_exit(1); } } /*}}}*/ static void setup_signals(void)/*{{{*/ { struct sigaction sa; if (sigemptyset(&sa.sa_mask) < 0) { perror("sigemptyset"); unlock_and_exit(1); } sa.sa_handler = handle_signal; sa.sa_flags = 0; guarded_sigaction(SIGHUP, &sa); guarded_sigaction(SIGINT, &sa); guarded_sigaction(SIGQUIT, &sa); guarded_sigaction(SIGTERM, &sa); return; } /*}}}*/ static void print_copyright(void)/*{{{*/ { fprintf(stderr, "tdl %s, Copyright (C) 2001-2004 Richard P. Curnow\n" "tdl comes with ABSOLUTELY NO WARRANTY.\n" "This is free software, and you are welcome to redistribute it\n" "under certain conditions; see the GNU General Public License for details.\n\n", get_version()); } /*}}}*/ void dispatch(char **argv) /* and other args *//*{{{*/ { int i, index=-1; char *executable; int is_tdl; char **p, **pp; if (was_signalled) { save_database(current_database_path); unlock_and_exit(0); } executable = executable_name(argv[0]); is_tdl = (!strcmp(executable, "tdl")); p = argv + 1; if (*p && !strcmp(*p, "-q")) p++; /* Parse command line */ if (is_tdl && !*p) { /* If no arguments, go into interactive mode, but only if we didn't come from there (!) */ if (!is_interactive) { setup_signals(); print_copyright(); is_interactive = 1; interactive(); } return; } if (is_tdl) { for (i=0; i= 0) { int result; is_processing = 1; if (!is_loaded && cmds[index].load_db) { load_database(current_database_path); } pp = is_tdl ? (p + 1) : p; result = (cmds[index].func)(pp); /* Check for failure */ if (result < 0) { if (!is_interactive) { unlock_and_exit(-result); } /* If interactive, the handling function has emitted its error message. * Just 'abort' this command and go back to the prompt */ } else { if (cmds[index].dirty) { currently_dirty = 1; } } is_processing = 0; if (was_signalled) { save_database(current_database_path); unlock_and_exit(0); } } else { fprintf(stderr, "Unknown command <%s>\n", argv[1]); if (!is_interactive) { unlock_and_exit(1); } } } /*}}}*/ static int usage(char **x)/*{{{*/ { int i, index; char *cmd = *x; if (cmd) { /* Detailed help for the one command */ index = -1; for (i=0; i= 0) { fprintf(stdout, "Description\n %s\n\n", cmds[i].descrip); fprintf(stdout, "Synopsis\n"); if (is_interactive) { fprintf(stdout, " %s %s\n", cmds[i].name, cmds[i].synopsis ? cmds[i].synopsis : ""); } else { fprintf(stdout, " tdl [-q] %s %s\n", cmds[i].name, cmds[i].synopsis ? cmds[i].synopsis : ""); if (cmds[i].shortcut) { fprintf(stdout, " %s [-q] %s\n", cmds[i].shortcut, cmds[i].synopsis ? cmds[i].synopsis : ""); } } fprintf(stdout, "\n" "General notes (where they apply to a command):\n" "\n" "<*_index> : 1, 1.1 etc (see output of 'tdl list')\n" " : urgent|high|normal|low|verylow\n" " : [-|+][0-9]+[shdwmy][-hh[mm[ss]]] OR\n" " [-|+](sun|mon|tue|wed|thu|fri|sat)[-hh[mm[ss]]] OR\n" " [[[cc]yy]mm]dd[-hh[mm[ss]]]\n" " : Any text (you'll need to quote it if >1 word)\n" ); } else { fprintf(stderr, "Unrecognized command <%s>, no help available\n", cmd); } } else { print_copyright(); if (!is_interactive) { fprintf(stdout, "tdl [-q] : Enter interactive mode\n"); } for (i=0; i' for more help on a particular command\n"); } else { fprintf(stdout, "\nEnter 'tdl help ' for more help on a particular command\n"); } } fprintf(stdout, "\n"); return 0; } /*}}}*/ /*{{{ int main (int argc, char **argv)*/ int main (int argc, char **argv) { n_cmds = N(cmds); is_interactive = 0; /* Initialise database */ top.prev = (struct node *) ⊤ top.next = (struct node *) ⊤ if ((argc > 1) && (!strcmp(argv[1], "-q"))) { is_noisy = 0; } current_database_path = get_database_path(1); dispatch(argv); save_database(current_database_path); free_database(&top); unlock_and_exit(0); return 0; /* moot */ } /*}}}*/