From d45f03595626c22ba0029fb059df9d19bd7e6158 Mon Sep 17 00:00:00 2001 From: "Richard W.M. Jones" Date: Thu, 10 Nov 2016 15:19:54 +0000 Subject: [PATCH] Initial commit. --- .gitignore | 35 ++++++ COPYING | 339 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ META.in | 6 + Makefile.am | 93 +++++++++++++++ README | 5 + common-rules.mk | 33 ++++++ configure.ac | 99 ++++++++++++++++ m4/ocaml.m4 | 217 ++++++++++++++++++++++++++++++++++ ocaml-link.sh | 44 +++++++ todo-schema.sql | 72 ++++++++++++ todo.ml | 43 +++++++ todo.spec.in | 52 +++++++++ todo_add.ml | 76 ++++++++++++ todo_cmdline.ml | 154 +++++++++++++++++++++++++ todo_config.ml.in | 21 ++++ todo_list.ml | 103 +++++++++++++++++ todo_move.ml | 54 +++++++++ todo_retire.ml | 37 ++++++ todo_tag.ml | 116 +++++++++++++++++++ todo_tag_utils.ml | 20 ++++ todo_types.ml | 26 +++++ todo_utils.ml | 61 ++++++++++ 22 files changed, 1706 insertions(+) create mode 100644 .gitignore create mode 100644 COPYING create mode 100644 META.in create mode 100644 Makefile.am create mode 100644 README create mode 100644 common-rules.mk create mode 100644 configure.ac create mode 100644 m4/ocaml.m4 create mode 100755 ocaml-link.sh create mode 100644 todo-schema.sql create mode 100644 todo.ml create mode 100644 todo.spec.in create mode 100644 todo_add.ml create mode 100644 todo_cmdline.ml create mode 100644 todo_config.ml.in create mode 100644 todo_list.ml create mode 100644 todo_move.ml create mode 100644 todo_retire.ml create mode 100644 todo_tag.ml create mode 100644 todo_tag_utils.ml create mode 100644 todo_types.ml create mode 100644 todo_utils.ml diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6cd3a5f --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +*~ +*.o +*.cmi +*.cmo +*.cmx + +Makefile +Makefile.in + +/.depend +/META +/aclocal.m4 +/autom4te.cache +/compile +/config.guess +/config.h +/config.h.in +/config.log +/config.status +/config.sub +/configure +/dummy.c +/install-sh +/libtool +/ltmain.sh +/m4/libtool.m4 +/m4/ltoptions.m4 +/m4/ltsugar.m4 +/m4/ltversion.m4 +/m4/lt~obsolete.m4 +/missing +/stamp-h1 +/todo +/todo.spec +/todo_config.ml diff --git a/COPYING b/COPYING new file mode 100644 index 0000000..d159169 --- /dev/null +++ b/COPYING @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + 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., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. diff --git a/META.in b/META.in new file mode 100644 index 0000000..552c409 --- /dev/null +++ b/META.in @@ -0,0 +1,6 @@ +# http://projects.camlcity.org/projects/dl/findlib-1.3.3/doc/guide-html/x412.html +name = "todo" +version = "@PACKAGE_VERSION@" +description = "To do list management" + +requires = "unix" diff --git a/Makefile.am b/Makefile.am new file mode 100644 index 0000000..7aaa7b2 --- /dev/null +++ b/Makefile.am @@ -0,0 +1,93 @@ +# todo +# Copyright (C) 2016 Red Hat Inc. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +include common-rules.mk + +ACLOCAL_AMFLAGS = -I m4 + +EXTRA_DIST = \ + COPYING \ + META.in \ + README + +all_sources = $(TODO_SOURCES_ML) + +# You must adjust this to point to the right database host. The +# database itself is hard-coded as 'todo'. Setting the environment +# variable here merely tells PG'OCaml that. +#export PGHOST=todo +export PGDATABASE=todo + +bin_PROGRAMS = todo + +TODO_SOURCES_ML = \ + todo_config.ml \ + todo_types.ml \ + todo_utils.ml \ + todo_tag_utils.ml \ + todo_add.ml \ + todo_list.ml \ + todo_move.ml \ + todo_retire.ml \ + todo_tag.ml \ + todo_cmdline.ml \ + todo.ml +if !HAVE_OCAMLOPT +TODO_OBJECTS = $(TODO_SOURCES_ML:.ml=.cmo) +else +TODO_OBJECTS = $(TODO_SOURCES_ML:.ml=.cmx) +endif + +todo_SOURCES = dummy.c +todo_DEPENDENCIES = $(TODO_OBJECTS) $(top_srcdir)/ocaml-link.sh +todo_LINK = \ + $(top_srcdir)/ocaml-link.sh -- \ + $(OCAMLFIND) $(OCAMLBEST) $(OCAMLFLAGS) $(OCAMLPACKAGES) \ + $(OCAMLLINKFLAGS) $(TODO_OBJECTS) -o $@ + +BUILT_SOURCES = dummy.c +dummy.c: + rm -f $@ + touch $@ + +# Dependencies. +depend: .depend + +.depend: $(all_sources) + rm -f $@ $@-t + $(OCAMLFIND) ocamldep $(OCAMLPACKAGES) -I $(abs_srcdir) $^ | \ + $(SED) 's/ *$$//' | \ + $(SED) -e :a -e '/ *\\$$/N; s/ *\\\n */ /; ta' | \ + $(SED) -e 's,$(abs_srcdir)/,$(builddir)/,g' | \ + sort > $@-t + mv $@-t $@ + +-include .depend + +# RPM package. +rpm: dist + rpmbuild -ta $(PACKAGE_NAME)-$(PACKAGE_VERSION).tar.gz + +if HAVE_POD2MAN + +#man_MANS = + +#goaljobs.1: goaljobs.pod +# $(POD2MAN) -c "Goaljobs" --release $(PACKAGE)-$(VERSION) $< > $@-t +# mv $@-t $@ + +endif diff --git a/README b/README new file mode 100644 index 0000000..61cb9fb --- /dev/null +++ b/README @@ -0,0 +1,5 @@ +This is the software I now use to manage my to-do list. It is +probably not useful or appropriate for anyone else. If it's useful +for you, that's great, but I don't want to be contacted about it. + +- Richard W.M. Jones, 2016-11-10 diff --git a/common-rules.mk b/common-rules.mk new file mode 100644 index 0000000..853efad --- /dev/null +++ b/common-rules.mk @@ -0,0 +1,33 @@ +# todo +# Copyright (C) 2016 Red Hat Inc. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +CLEANFILES = *~ *.cmi *.cmo *.cmx *.cma *.cmxa *.o + +OCAMLFLAGS = -g -warn-error CDEFLMPSUVYZX-3 +OCAMLPACKAGES = \ + -package unix,calendar,pgocaml.syntax \ + -syntax camlp4o \ + -I $(top_builddir) + +%.cmi: %.mli + $(OCAMLFIND) ocamlc $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@ +%.cmo: %.ml + $(OCAMLFIND) ocamlc $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@ +%.cmx: %.ml + $(OCAMLFIND) ocamlopt $(OCAMLFLAGS) $(OCAMLPACKAGES) -c $< -o $@ + +SUFFIXES = .cmo .cmi .cmx .ml .mli .mll .mly diff --git a/configure.ac b/configure.ac new file mode 100644 index 0000000..7a01621 --- /dev/null +++ b/configure.ac @@ -0,0 +1,99 @@ +# todo +# Copyright (C) 2016 Red Hat Inc. +# +# 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., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +AC_INIT([todo],[0.1]) +AM_INIT_AUTOMAKE([foreign]) + +AC_SUBST([RPM_RELEASE],[1]) + +AC_CONFIG_MACRO_DIR([m4]) + +dnl Allow all GNU/Linux functions. +dnl autoconf complains unless this is very early in the file. +AC_USE_SYSTEM_EXTENSIONS + +AC_PROG_LIBTOOL + +dnl Check for basic C environment. +AC_PROG_CC_STDC +AC_PROG_INSTALL +AC_PROG_CPP + +AC_C_PROTOTYPES +test "x$U" != "x" && AC_MSG_ERROR([Compiler not ANSI compliant]) + +AM_PROG_CC_C_O + +AC_ARG_ENABLE([gcc-warnings], + [AS_HELP_STRING([--enable-gcc-warnings], + [turn on lots of GCC warnings (for developers)])], + [case $enableval in + yes|no) ;; + *) AC_MSG_ERROR([bad value $enableval for gcc-warnings option]) ;; + esac + gcc_warnings=$enableval], + [gcc_warnings=no] +) + +if test "$gcc_warnings" = yes; then + # XXX With gnulib we can improve this in future. + WARN_CFLAGS="-Wall" + AC_SUBST([WARN_CFLAGS]) + WERROR_CFLAGS="-Werror" + AC_SUBST([WERROR_CFLAGS]) +fi + +dnl Check support for 64 bit file offsets. +AC_SYS_LARGEFILE + +dnl OCaml compiler. +AC_PROG_OCAML +if test "$OCAMLC" = "no"; then + AC_MSG_ERROR([You must install the OCaml compiler]) +fi + +AM_CONDITIONAL([HAVE_OCAMLOPT], [test "x$OCAMLOPT" != "xno"]) + +dnl OCaml findlib ("ocamlfind") is required. +AC_PROG_FINDLIB +if test "x$OCAMLFIND" = "xno"; then + AC_MSG_ERROR([You must install OCaml findlib (the ocamlfind command)]) +fi + +dnl Check for OCaml package Calendar (required). +AC_CHECK_OCAML_PKG(calendar) +if test "x$OCAML_PKG_calendar" = "xno"; then + AC_MSG_ERROR([You must install the 'ocaml-calendar' library]) +fi + +dnl Check for OCaml package PG'OCaml (required). +AC_CHECK_OCAML_PKG(pgocaml) +if test "x$OCAML_PKG_pgocaml" = "xno"; then + AC_MSG_ERROR([You must install the 'ocaml-pgocaml' library]) +fi + +dnl Check for POD (for manual pages). +AC_CHECK_PROG(POD2MAN,pod2man,pod2man,no) +AM_CONDITIONAL([HAVE_POD2MAN], + [test "x$POD2MAN" != "xno"]) + +AC_CONFIG_HEADERS([config.h]) +AC_CONFIG_FILES([todo_config.ml + todo.spec + Makefile + META]) +AC_OUTPUT diff --git a/m4/ocaml.m4 b/m4/ocaml.m4 new file mode 100644 index 0000000..fddd6a0 --- /dev/null +++ b/m4/ocaml.m4 @@ -0,0 +1,217 @@ +dnl autoconf macros for OCaml +dnl +dnl Copyright © 2009 Richard W.M. Jones +dnl Copyright © 2009 Stefano Zacchiroli +dnl Copyright © 2000-2005 Olivier Andrieu +dnl Copyright © 2000-2005 Jean-Christophe Filliâtre +dnl Copyright © 2000-2005 Georges Mariano +dnl +dnl For documentation, please read the ocaml.m4 man page. + +AC_DEFUN([AC_PROG_OCAML], +[dnl + # checking for ocamlc + AC_CHECK_TOOL([OCAMLC],[ocamlc],[no]) + + if test "$OCAMLC" != "no"; then + OCAMLVERSION=`$OCAMLC -v | sed -n -e 's|.*version* *\(.*\)$|\1|p'` + AC_MSG_RESULT([OCaml version is $OCAMLVERSION]) + OCAMLLIB=`$OCAMLC -where 2>/dev/null || $OCAMLC -v|tail -1|cut -d ' ' -f 4` + AC_MSG_RESULT([OCaml library path is $OCAMLLIB]) + + AC_SUBST([OCAMLVERSION]) + AC_SUBST([OCAMLLIB]) + + # checking for ocamlopt + AC_CHECK_TOOL([OCAMLOPT],[ocamlopt],[no]) + OCAMLBEST=byte + if test "$OCAMLOPT" = "no"; then + AC_MSG_WARN([Cannot find ocamlopt; bytecode compilation only.]) + else + TMPVERSION=`$OCAMLOPT -v | sed -n -e 's|.*version* *\(.*\)$|\1|p' ` + if test "$TMPVERSION" != "$OCAMLVERSION" ; then + AC_MSG_RESULT([versions differs from ocamlc; ocamlopt discarded.]) + OCAMLOPT=no + else + OCAMLBEST=opt + fi + fi + + AC_SUBST([OCAMLBEST]) + + # checking for ocamlc.opt + AC_CHECK_TOOL([OCAMLCDOTOPT],[ocamlc.opt],[no]) + if test "$OCAMLCDOTOPT" != "no"; then + TMPVERSION=`$OCAMLCDOTOPT -v | sed -n -e 's|.*version* *\(.*\)$|\1|p' ` + if test "$TMPVERSION" != "$OCAMLVERSION" ; then + AC_MSG_RESULT([versions differs from ocamlc; ocamlc.opt discarded.]) + else + OCAMLC=$OCAMLCDOTOPT + fi + fi + + # checking for ocamlopt.opt + if test "$OCAMLOPT" != "no" ; then + AC_CHECK_TOOL([OCAMLOPTDOTOPT],[ocamlopt.opt],[no]) + if test "$OCAMLOPTDOTOPT" != "no"; then + TMPVERSION=`$OCAMLOPTDOTOPT -v | sed -n -e 's|.*version* *\(.*\)$|\1|p' ` + if test "$TMPVERSION" != "$OCAMLVERSION" ; then + AC_MSG_RESULT([version differs from ocamlc; ocamlopt.opt discarded.]) + else + OCAMLOPT=$OCAMLOPTDOTOPT + fi + fi + fi + + AC_SUBST([OCAMLOPT]) + fi + + AC_SUBST([OCAMLC]) + + # checking for ocamldep + AC_CHECK_TOOL([OCAMLDEP],[ocamldep],[no]) + + # checking for ocamlmktop + AC_CHECK_TOOL([OCAMLMKTOP],[ocamlmktop],[no]) + + # checking for ocamlmklib + AC_CHECK_TOOL([OCAMLMKLIB],[ocamlmklib],[no]) + + # checking for ocamldoc + AC_CHECK_TOOL([OCAMLDOC],[ocamldoc],[no]) + + # checking for ocamlbuild + AC_CHECK_TOOL([OCAMLBUILD],[ocamlbuild],[no]) +]) + + +AC_DEFUN([AC_PROG_OCAMLLEX], +[dnl + # checking for ocamllex + AC_CHECK_TOOL([OCAMLLEX],[ocamllex],[no]) + if test "$OCAMLLEX" != "no"; then + AC_CHECK_TOOL([OCAMLLEXDOTOPT],[ocamllex.opt],[no]) + if test "$OCAMLLEXDOTOPT" != "no"; then + OCAMLLEX=$OCAMLLEXDOTOPT + fi + fi + AC_SUBST([OCAMLLEX]) +]) + +AC_DEFUN([AC_PROG_OCAMLYACC], +[dnl + AC_CHECK_TOOL([OCAMLYACC],[ocamlyacc],[no]) + AC_SUBST([OCAMLYACC]) +]) + + +AC_DEFUN([AC_PROG_CAMLP4], +[dnl + AC_REQUIRE([AC_PROG_OCAML])dnl + + # checking for camlp4 + AC_CHECK_TOOL([CAMLP4],[camlp4],[no]) + if test "$CAMLP4" != "no"; then + TMPVERSION=`$CAMLP4 -v 2>&1| sed -n -e 's|.*version *\(.*\)$|\1|p'` + if test "$TMPVERSION" != "$OCAMLVERSION" ; then + AC_MSG_RESULT([versions differs from ocamlc]) + CAMLP4=no + fi + fi + AC_SUBST([CAMLP4]) + + # checking for companion tools + AC_CHECK_TOOL([CAMLP4BOOT],[camlp4boot],[no]) + AC_CHECK_TOOL([CAMLP4O],[camlp4o],[no]) + AC_CHECK_TOOL([CAMLP4OF],[camlp4of],[no]) + AC_CHECK_TOOL([CAMLP4OOF],[camlp4oof],[no]) + AC_CHECK_TOOL([CAMLP4ORF],[camlp4orf],[no]) + AC_CHECK_TOOL([CAMLP4PROF],[camlp4prof],[no]) + AC_CHECK_TOOL([CAMLP4R],[camlp4r],[no]) + AC_CHECK_TOOL([CAMLP4RF],[camlp4rf],[no]) + AC_SUBST([CAMLP4BOOT]) + AC_SUBST([CAMLP4O]) + AC_SUBST([CAMLP4OF]) + AC_SUBST([CAMLP4OOF]) + AC_SUBST([CAMLP4ORF]) + AC_SUBST([CAMLP4PROF]) + AC_SUBST([CAMLP4R]) + AC_SUBST([CAMLP4RF]) +]) + + +AC_DEFUN([AC_PROG_FINDLIB], +[dnl + AC_REQUIRE([AC_PROG_OCAML])dnl + + # checking for ocamlfind + AC_CHECK_TOOL([OCAMLFIND],[ocamlfind],[no]) + AC_SUBST([OCAMLFIND]) +]) + + +dnl Thanks to Jim Meyering for working this next bit out for us. +dnl XXX We should define AS_TR_SH if it's not defined already +dnl (eg. for old autoconf). +AC_DEFUN([AC_CHECK_OCAML_PKG], +[dnl + AC_REQUIRE([AC_PROG_FINDLIB])dnl + + AC_MSG_CHECKING([for OCaml findlib package $1]) + + unset found + unset pkg + found=no + for pkg in $1 $2 ; do + if $OCAMLFIND query $pkg >/dev/null 2>/dev/null; then + AC_MSG_RESULT([found]) + AS_TR_SH([OCAML_PKG_$1])=$pkg + found=yes + break + fi + done + if test "$found" = "no" ; then + AC_MSG_RESULT([not found]) + AS_TR_SH([OCAML_PKG_$1])=no + fi + + AC_SUBST(AS_TR_SH([OCAML_PKG_$1])) +]) + + +AC_DEFUN([AC_CHECK_OCAML_MODULE], +[dnl + AC_MSG_CHECKING([for OCaml module $2]) + + cat > conftest.ml <&5 2>&5 ; then + found=yes + break + fi + done + + if test "$found" ; then + AC_MSG_RESULT([$$1]) + else + AC_MSG_RESULT([not found]) + $1=no + fi + AC_SUBST([$1]) +]) + + +dnl XXX Cross-compiling +AC_DEFUN([AC_CHECK_OCAML_WORD_SIZE], +[dnl + AC_MSG_CHECKING([for OCaml compiler word size]) + cat > conftest.ml <&2 ; exit 1 ; fi +eval set -- "$TEMP" + +cclib= + +while true ; do + case "$1" in + -cclib|--cclib) cclib="$2" ; shift 2 ;; + --) shift ; break ;; + *) echo "Internal error!" ; exit 1 ;; + esac +done + +exec "$@" -linkpkg -cclib "${cclib}" diff --git a/todo-schema.sql b/todo-schema.sql new file mode 100644 index 0000000..eb4492b --- /dev/null +++ b/todo-schema.sql @@ -0,0 +1,72 @@ +-- PostgreSQL database schema. + +begin work; + +-- List of all tasks. +-- +-- Note that unsorted tasks (which need to be moved to 'imminent', +-- 'todo', 'ideas', 'retired' lists) are any item which appears in +-- this table but not in one of the other tables. +create table tasks ( + id serial not null primary key, + description text not null, + created timestamp without time zone not null default now(), + + -- task source + rhbz integer, -- if task comes from RHBZ + message_id text -- if task comes from email +); + +-- Tasks which must be completed immediately and should take no more +-- than a few minutes (else they should be moved to another list). +create table imminent ( + taskid integer not null references tasks (id), + added timestamp without time zone not null default now() +); + +-- Regular to-do items with deadlines. +create table todo ( + taskid integer not null references tasks (id), + added timestamp without time zone not null default now(), + deadline timestamp without time zone not null, + estimate interval -- optional +); + +-- Ideas. These have no committed deadline. +create table ideas ( + taskid integer not null references tasks (id), + added timestamp without time zone not null default now() +); + +-- Retired/done items. +create table retired ( + taskid integer not null references tasks (id), + added timestamp without time zone not null default now() +); + +-- Tags (eg 'urgent', 'personal', 'work') which can be added to any +-- task. +create table tags ( + id serial not null primary key, + name text not null, + colour text not null references colours (colour) +); + +create table tags_tasks ( + tagid integer not null references tags (id), + taskid integer not null references tasks (id) +); + +create table colours ( + colour text not null, + unique (colour) +); +insert into colours (colour) values ('black'); +insert into colours (colour) values ('blue'); +insert into colours (colour) values ('green'); +insert into colours (colour) values ('red'); +insert into colours (colour) values ('purple'); +insert into colours (colour) values ('cyan'); +insert into colours (colour) values ('yellow'); + +commit; diff --git a/todo.ml b/todo.ml new file mode 100644 index 0000000..2216fc3 --- /dev/null +++ b/todo.ml @@ -0,0 +1,43 @@ +(* 'todo' command, for adding, listing, deleting etc tasks from the + * command line. + *) + +open CalendarLib + +open Todo_types +open Todo_utils + +open Printf + +(* Parse the command line. *) +let subcommand, anon_params, list_retired, list_all, tag_del, todo_estimate = + Todo_cmdline.parse_command_line () + +(* Connect to the database. *) +let dbh = PGOCaml.connect ~database:"todo" () + +(* Run the subcommand. *) +let () = + match subcommand with + | Idea -> (* todo idea *) + Todo_add.cmd_idea dbh anon_params + | List -> (* todo list *) + Todo_list.cmd_list dbh anon_params list_retired list_all + | Move -> (* todo move *) + Todo_move.cmd_move dbh anon_params + | Retire -> (* todo retire *) + Todo_retire.cmd_retire dbh anon_params + | Tag -> (* todo tag *) + Todo_tag.cmd_tag dbh anon_params tag_del + | Tag_add -> (* todo tag-add *) + Todo_tag.cmd_tag_add dbh anon_params + | Tag_colour -> (* todo tag-colour *) + Todo_tag.cmd_tag_colour dbh anon_params + | Tag_del -> (* todo tag-del *) + Todo_tag.cmd_tag_del dbh anon_params + | Tag_list -> (* todo tag-list *) + Todo_tag.cmd_tag_list dbh anon_params + | Today -> (* todo today *) + Todo_add.cmd_today dbh anon_params + | Todo -> (* todo todo|task *) + Todo_add.cmd_todo dbh anon_params todo_estimate diff --git a/todo.spec.in b/todo.spec.in new file mode 100644 index 0000000..4e4d696 --- /dev/null +++ b/todo.spec.in @@ -0,0 +1,52 @@ +%global opt %(test -x %{_bindir}/ocamlopt && echo 1 || echo 0) + +Name: @PACKAGE_NAME@ +Version: @PACKAGE_VERSION@ +Release: @RPM_RELEASE@%{?dist} +Summary: To do list management +License: GPLv2+ + +#URL: +Source0: %{name}-%{version}.tar.gz + +BuildRequires: ocaml >= 3.12.0 +BuildRequires: ocaml-findlib-devel +BuildRequires: ocaml-sqlite-devel + +# For building manual pages. +BuildRequires: perl-podlators + + +%description + + +%prep +%setup -q + + +%build +%configure +make %{?_smp_mflags} + + +%install +make DESTDIR=$RPM_BUILD_ROOT install + +%if %opt +# Remove bytecode library. +rm $RPM_BUILD_ROOT%{_libdir}/ocaml/todo/todo.cma +%endif + + +%files +%doc COPYING README + + +%changelog +* Thu Nov 10 2016 Richard W.M. Jones - @PACKAGE_VERSION@-@RPM_RELEASE@ +- Initial release. + +# Local variables: +# mode: shell-script +# sh-shell: rpm +# end: diff --git a/todo_add.ml b/todo_add.ml new file mode 100644 index 0000000..ab34092 --- /dev/null +++ b/todo_add.ml @@ -0,0 +1,76 @@ +(* Implement subcommands for adding new tasks. *) + +open CalendarLib + +open Todo_types +open Todo_utils + +open Printf + +let parse_tags tags = nsplit "," tags + +let insert_tags dbh taskid tags = + List.iter ( + fun tag -> + let rows = PGSQL(dbh) "select id from tags where name = $tag" in + let id = + match rows with + | [] -> error "unknown tag: %s" tag + | [id] -> id + | _ -> assert false in + PGSQL(dbh) "insert into tags_tasks (tagid, taskid) + values ($id, $taskid)" + ) tags + +let cmd_todo dbh anon_params estimate = + let deadline, description, tags = + match anon_params with + | [deadline; description] -> + Printer.Date.from_string deadline, description, [] + | [deadline; description; tags] -> + Printer.Date.from_string deadline, description, parse_tags tags + | _ -> error "incorrect number of parameters to 'todo' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "insert into tasks (description) values ($description)"; + let taskid = PGOCaml.serial4 dbh "tasks_id_seq" in + PGSQL(dbh) "insert into todo (taskid, deadline, estimate) + values ($taskid, $deadline :: date, $?estimate)"; + insert_tags dbh taskid tags; + + PGOCaml.commit dbh + +let cmd_today dbh anon_params = + let description, tags = + match anon_params with + | [description] -> description, [] + | [description; tags] -> + description, parse_tags tags + | _ -> error "incorrect number of parameters to 'today' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "insert into tasks (description) values ($description)"; + let taskid = PGOCaml.serial4 dbh "tasks_id_seq" in + PGSQL(dbh) "insert into imminent (taskid) values ($taskid)"; + insert_tags dbh taskid tags; + + PGOCaml.commit dbh + +let cmd_idea dbh anon_params = + let description, tags = + match anon_params with + | [description] -> description, [] + | [description; tags] -> + description, parse_tags tags + | _ -> error "incorrect number of parameters to 'idea' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "insert into tasks (description) values ($description)"; + let taskid = PGOCaml.serial4 dbh "tasks_id_seq" in + PGSQL(dbh) "insert into ideas (taskid) values ($taskid)"; + insert_tags dbh taskid tags; + + PGOCaml.commit dbh diff --git a/todo_cmdline.ml b/todo_cmdline.ml new file mode 100644 index 0000000..30fe225 --- /dev/null +++ b/todo_cmdline.ml @@ -0,0 +1,154 @@ +(* Parse the command line. *) + +open CalendarLib + +open Todo_types +open Todo_utils + +open Printf + +let usage = "\ +todo -- A simple to-do list + + todo [--global-options] [subcommand] [--subcommand-options] + +Subcommands: + todo list [--all] [--retired] + - List all tasks + todo today \"description\" [tag[,tag...]] + - Add a simple task for today, with an optional list of tags + todo idea \"description\" [tag[,tag...]] + - Add an long-term idea + todo [todo|task] deadline [--estimate nr_days] \"description\" [tag[,tag...]] + - Add a to-do task with a deadline and optional time estimate + todo retire ID [ID ...] + - Retire one or more tasks using the task #ID + todo move ID [today | [todo|task] deadline | ideas | retired | unsorted] + - Move a task to another list + todo tag ID tag [--del tag] + - Add or remove tags from a task + todo tag-list + - List all tags and their usage + todo tag-add tag colour + - Create a new tag, with a colour + todo tag-del tag + - Delete a tag (must be unused) + todo tag-colour tag colour + - Change the colour of a tag + +Tag colours may be: black blue green cyan red purple yellow + +Examples: + todo today \"Eat breakfast\" personal,food + todo task 2019-31-12 \"Prepare for next decade\" + todo idea \"Write a to-do manager\" work + todo list + +Options:" + +(* Parse the command line. *) +let parse_command_line () = + let print_version_and_exit () = + printf "%s %s\n" Todo_config.package_name Todo_config.package_version; + exit 0 + in + + let list_all = ref false in + let list_retired = ref false in + let tag_del = ref [] in + let add_tag_del tag = tag_del := tag :: !tag_del in + let todo_estimate = ref None in + let set_todo_estimate days = + todo_estimate := Some (Calendar.Period.day days) + in + + (* Per-subcommand specs. *) + let idea_spec = + Arg.align [ + ] in + let list_spec = + Arg.align [ + "--all", Arg.Set list_all, " List all tasks including retired"; + "--retired", Arg.Set list_retired, " List only retired tasks"; + ] in + let move_spec = + Arg.align [ + ] in + let retire_spec = + Arg.align [ + ] in + let tag_spec = + Arg.align [ + "--del", Arg.String add_tag_del, "tag Remove tag"; + "--delete", Arg.String add_tag_del, "tag Remove tag"; + "--remove", Arg.String add_tag_del, "tag Remove tag"; + "--rm", Arg.String add_tag_del, "tag Remove tag"; + ] in + let tag_add_spec = + Arg.align [ + ] in + let tag_colour_spec = + Arg.align [ + ] in + let tag_del_spec = + Arg.align [ + ] in + let tag_list_spec = + Arg.align [ + ] in + let today_spec = + Arg.align [ + ] in + let todo_spec = + Arg.align [ + "--estimate", Arg.Int set_todo_estimate, "days Estimated time taken"; + ] in + + (* This parses the global parameters before the subcommand. *) + let global_spec = + Arg.align [ + "--version", Arg.Unit print_version_and_exit, " Display version and exit"; + ] in + let spec = ref global_spec in + let subcommand = ref None in + let anon_params = ref [] in + let anon s = + if !subcommand = None then ( + (* It's the subcommand, so switch the spec. *) + let sc = + try subcommand_of_string s + with Invalid_argument _ -> + raise (Arg.Bad "unknown subcommand") in + subcommand := Some sc; + match sc with + | Idea -> spec := idea_spec + | List -> spec := list_spec + | Move -> spec := move_spec + | Retire -> spec := retire_spec + | Tag -> spec := tag_spec + | Tag_add -> spec := tag_add_spec + | Tag_colour -> spec := tag_colour_spec + | Tag_del -> spec := tag_del_spec + | Tag_list -> spec := tag_list_spec + | Today -> spec := today_spec + | Todo -> spec := todo_spec + ) + else + (* We've seen the subcommand already, just add it to anon_params. *) + anon_params := s :: !anon_params + in + Arg.parse_dynamic spec anon usage; + + (* Return the parsed command line. *) + let subcommand = + match !subcommand with + | None -> error "no subcommand given" + | Some c -> c in + let anon_params = List.rev !anon_params in + + let list_retired = !list_retired in + let list_all = !list_all in + let tag_del = List.rev !tag_del in + let todo_estimate = !todo_estimate in + + subcommand, anon_params, list_retired, list_all, tag_del, todo_estimate diff --git a/todo_config.ml.in b/todo_config.ml.in new file mode 100644 index 0000000..0d8a61f --- /dev/null +++ b/todo_config.ml.in @@ -0,0 +1,21 @@ +(* todo + * Copyright (C) 2016 Red Hat Inc. + * @configure_input@ + * + * 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., + * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + *) + +let package_name = "@PACKAGE_NAME@" +let package_version = "@PACKAGE_VERSION@" diff --git a/todo_list.ml b/todo_list.ml new file mode 100644 index 0000000..0ba1a1a --- /dev/null +++ b/todo_list.ml @@ -0,0 +1,103 @@ +(* Implement 'list' subcommand. *) + +open CalendarLib + +open Todo_types +open Todo_utils +open Todo_tag_utils + +open Printf + +let tags_of_task dbh id = + let rows = PGSQL(dbh) "select tags.name, tags.colour + from tags_tasks, tags + where tags_tasks.taskid = $id + and tags_tasks.tagid = tags.id + order by tags.name" in + if rows = [] then "" + else + " " ^ + String.concat " " + (List.map ( + fun (name, colour) -> + string_of_tag name colour + ) rows) + +let cmd_list dbh anon_params list_retired list_all = + if anon_params <> [] then + error "extra parameters to 'list' subcommand"; + + let show_unretired, show_retired = + match list_retired, list_all with + | _, true -> true, true + | false, false -> true, false + | true, false -> false, true in + + if show_unretired then ( + let rows = PGSQL(dbh) "select imminent.taskid, tasks.description + from imminent, tasks + where imminent.taskid = tasks.id + order by tasks.description" in + if rows <> [] then printf "Today:\n"; + List.iter ( + fun (id, desc) -> + printf " %s #%ld%s\n" desc id (tags_of_task dbh id) + ) rows; + + let rows = PGSQL(dbh) "select todo.taskid, todo.deadline, todo.estimate, + tasks.description + from todo, tasks + where todo.taskid = tasks.id + order by todo.deadline" in + if rows <> [] then printf "To-do list:\n"; + List.iter ( + fun (id, deadline, estimate, desc) -> + let deadline = + if Time.equal (Time.midnight ()) (Calendar.to_time deadline) then + Printer.Calendar.sprint "%F" deadline + else + Printer.Calendar.to_string deadline in + let estimate = + match estimate with + | None -> "" + | Some period -> string_of_estimate period ^ " " in + printf " %s %s%s #%ld%s\n" + deadline estimate desc id (tags_of_task dbh id) + ) rows; + + let rows = PGSQL(dbh) " + select tasks.id, tasks.description + from tasks + where not exists (select 1 from imminent where taskid = tasks.id) + and not exists (select 1 from todo where taskid = tasks.id) + and not exists (select 1 from ideas where taskid = tasks.id) + and not exists (select 1 from retired where taskid = tasks.id) + order by tasks.description" in + if rows <> [] then printf "Unsorted (use 'todo move' to move these):\n"; + List.iter ( + fun (id, desc) -> + printf " %s #%ld%s\n" desc id (tags_of_task dbh id) + ) rows; + + let rows = PGSQL(dbh) "select ideas.taskid, tasks.description + from ideas, tasks + where ideas.taskid = tasks.id + order by tasks.description" in + if rows <> [] then printf "Ideas:\n"; + List.iter ( + fun (id, desc) -> + printf " %s #%ld%s\n" desc id (tags_of_task dbh id) + ) rows + ); (* unretired *) + + if show_retired then ( + let rows = PGSQL(dbh) "select retired.taskid, tasks.description + from retired, tasks + where retired.taskid = tasks.id + order by tasks.description" in + if rows <> [] then printf "Retired:\n"; + List.iter ( + fun (id, desc) -> + printf " %s #%ld%s\n" desc id (tags_of_task dbh id) + ) rows + ) diff --git a/todo_move.ml b/todo_move.ml new file mode 100644 index 0000000..5007ad3 --- /dev/null +++ b/todo_move.ml @@ -0,0 +1,54 @@ +(* Implement 'move' subcommand. *) + +open CalendarLib + +open Todo_types +open Todo_utils + +open Printf + +(* The form of the command is one of: + * move ID today + * move ID [todo|task] deadline + * move ID ideas + * move ID retired + * move ID unsorted + *) +let cmd_move dbh anon_params = + let id, anon_params = + match anon_params with + | id :: rest -> Int32.of_string id, rest + | _ -> error "incorrect parameters to 'move' subcommand" in + let dest = + match anon_params with + | ["today"|"imminent"] -> `Today + | ["todo"|"task"; deadline] -> + `Todo (Printer.Calendar.from_string deadline) + | ["idea"|"ideas"] -> `Ideas + | ["retire"|"retired"] -> `Retired + | ["none"|"unsorted"] -> `Unsorted + | _ -> error "incorrect destination for 'move' subcommand" in + + PGOCaml.begin_work dbh; + + (* Remove it from any other table. *) + PGSQL(dbh) "delete from imminent where taskid = $id"; + PGSQL(dbh) "delete from todo where taskid = $id"; + PGSQL(dbh) "delete from ideas where taskid = $id"; + PGSQL(dbh) "delete from retired where taskid = $id"; + + (* Add it to the destination table. *) + (match dest with + | `Today -> + PGSQL(dbh) "insert into imminent (taskid) values ($id)" + | `Todo deadline -> + PGSQL(dbh) "insert into todo (taskid, deadline) values ($id, $deadline)" + | `Ideas -> + PGSQL(dbh) "insert into ideas (taskid) values ($id)" + | `Retired -> + PGSQL(dbh) "insert into retired (taskid) values ($id)" + | `Unsorted -> + () (* Nothing - unsorted items are not present in any table. *) + ); + + PGOCaml.commit dbh diff --git a/todo_retire.ml b/todo_retire.ml new file mode 100644 index 0000000..d6ec055 --- /dev/null +++ b/todo_retire.ml @@ -0,0 +1,37 @@ +(* Implement 'retire' subcommand. *) + +open CalendarLib + +open Todo_types +open Todo_utils + +open Printf + +let cmd_retire dbh anon_params = + let ids = + match anon_params with + | [] -> error "give a list of IDs (#ID) to retire" + | ids -> List.map Int32.of_string ids in + + PGOCaml.begin_work dbh; + + (* It's an error to retire a task which is already retired. *) + List.iter ( + fun taskid -> + let rows = PGSQL(dbh) "select 1 from retired where taskid = $taskid" in + if rows = [Some 1_l] then + error "task %ld is already retired" taskid; + ) ids; + + (* Drop the tasks from every other table. *) + PGSQL(dbh) "delete from imminent where taskid in $@ids"; + PGSQL(dbh) "delete from todo where taskid in $@ids"; + PGSQL(dbh) "delete from ideas where taskid in $@ids"; + + (* Add them to the retired table. *) + List.iter ( + fun taskid -> + PGSQL(dbh) "insert into retired (taskid) values ($taskid)" + ) ids; + + PGOCaml.commit dbh diff --git a/todo_tag.ml b/todo_tag.ml new file mode 100644 index 0000000..a4ab5a5 --- /dev/null +++ b/todo_tag.ml @@ -0,0 +1,116 @@ +(* Implement subcommands for listing and adding tags. *) + +open CalendarLib + +open Todo_types +open Todo_utils +open Todo_tag_utils + +open Printf + +let cmd_tag_list dbh anon_params = + if anon_params <> [] then + error "extra parameters to 'tag-list' subcommand"; + + (* Used tags. *) + let rows = PGSQL(dbh) " + select tags.name, tags.colour, tasks.id + from tags_tasks, tags, tasks + where tags_tasks.tagid = tags.id + and tags_tasks.taskid = tasks.id + order by tags.name, tasks.id" in + if rows <> [] then printf "Tags used:\n"; + let prev = ref None in + List.iter ( + fun (name, colour, taskid) -> + let cur = name, colour in + (match !prev with + | None -> printf " %s used by" (string_of_tag name colour) + | Some prev when prev <> cur -> + printf "\n %s used by" (string_of_tag name colour) + | Some _ -> () + ); + prev := Some cur; + printf " #%ld" taskid + ) rows; + if !prev <> None then printf "\n"; + + (* Unused tags. *) + let rows = PGSQL(dbh) " + select tags.name, tags.colour + from tags + where not exists (select 1 from tags_tasks where tagid = tags.id) + order by tags.name" in + if rows <> [] then printf "Tags not used (delete with 'todo tag-del'):\n"; + List.iter ( + fun (name, colour) -> + printf " %s\n" (string_of_tag name colour) + ) rows + +let cmd_tag dbh anon_params tag_del = + let id, tag_add = + match anon_params with + | id :: rest -> Int32.of_string id, rest + | _ -> error "incorrect parameters to 'tag' subcommand" in + if anon_params = [] && tag_del = [] then + error "no tags to add or delete from this task"; + + (* Map the tag_add and tag_del names to tag IDs. *) + let tag_add = + if tag_add <> [] then + PGSQL(dbh) "select id from tags where name in $@tag_add" + else [] in + let tag_del = + if tag_del <> [] then + PGSQL(dbh) "select id from tags where name in $@tag_del" + else [] in + + PGOCaml.begin_work dbh; + + if tag_del <> [] then + PGSQL(dbh) "delete from tags_tasks + where taskid = $id and tagid in $@tag_del"; + + List.iter ( + fun tag -> + PGSQL(dbh) "insert into tags_tasks (taskid, tagid) + values ($id, $tag)" + ) tag_add; + + PGOCaml.commit dbh + +let cmd_tag_add dbh anon_params = + let name, colour = + match anon_params with + | [name; colour] -> name, colour + | _ -> error "incorrect parameters to 'tag-add' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "insert into tags (name, colour) values ($name, $colour)"; + + PGOCaml.commit dbh + +let cmd_tag_del dbh anon_params = + let name = + match anon_params with + | [name] -> name + | _ -> error "incorrect parameters to 'tag-del' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "delete from tags where name = $name"; + + PGOCaml.commit dbh + +let cmd_tag_colour dbh anon_params = + let name, colour = + match anon_params with + | [name; colour] -> name, colour + | _ -> error "incorrect parameters to 'tag-colour' subcommand" in + + PGOCaml.begin_work dbh; + + PGSQL(dbh) "update tags set colour = $colour where name = $name"; + + PGOCaml.commit dbh diff --git a/todo_tag_utils.ml b/todo_tag_utils.ml new file mode 100644 index 0000000..29a760b --- /dev/null +++ b/todo_tag_utils.ml @@ -0,0 +1,20 @@ +(* Utility functions related to tags. *) + +open Printf + +let string_of_tag name colour = + let bg, fg = + match colour with + (* white fg *) + | "black" -> 40, 37 + | "blue" -> 44, 37 + | "green" -> 42, 37 + | "red" -> 41, 37 + | "purple" -> 45, 37 + + (* black fg *) + | "cyan" -> 46, 30 + | "yellow" -> 43, 30 + | _ -> assert false in + + sprintf "\x1b[%d;%dm\x1b[%d;%dm %s \x1b[0m" 1 fg 1 bg name diff --git a/todo_types.ml b/todo_types.ml new file mode 100644 index 0000000..3c6f1c5 --- /dev/null +++ b/todo_types.ml @@ -0,0 +1,26 @@ +type subcommand = + | Idea + | List + | Move + | Retire + | Tag + | Tag_add + | Tag_colour + | Tag_del + | Tag_list + | Today + | Todo + +let subcommand_of_string = function + | "idea" -> Idea + | "list" -> List + | "move" -> Move + | "retire" -> Retire + | "tag" -> Tag + | "tag-add" -> Tag_add + | "tag-color" | "tag-colour" -> Tag_colour + | "tag-del" | "tag-delete" | "tag-rm" -> Tag_del + | "tag-list" -> Tag_list + | "task" | "todo" -> Todo + | "today" -> Today + | _ -> invalid_arg "subcommand_of_string" diff --git a/todo_utils.ml b/todo_utils.ml new file mode 100644 index 0000000..27f13dd --- /dev/null +++ b/todo_utils.ml @@ -0,0 +1,61 @@ +(* Utility functions. *) + +open CalendarLib + +open Printf + +let rec find s sub = + let len = String.length s in + let sublen = String.length sub in + let rec loop i = + if i <= len-sublen then ( + let rec loop2 j = + if j < sublen then ( + if s.[i+j] = sub.[j] then loop2 (j+1) + else -1 + ) else + i (* found *) + in + let r = loop2 0 in + if r = -1 then loop (i+1) else r + ) else + -1 (* not found *) + in + loop 0 + +let rec nsplit sep str = + let len = String.length str in + let seplen = String.length sep in + let i = find str sep in + if i = -1 then [str] + else ( + let s' = String.sub str 0 i in + let s'' = String.sub str (i+seplen) (len-i-seplen) in + s' :: nsplit sep s'' + ) + +let error fs = + let display str = + prerr_string "todo: error: "; + prerr_endline str; + exit 1 + in + ksprintf display fs + +let string_of_estimate period = + (* We ignore the seconds, and assume every estimate is >= 1 day. *) + let (years, months, days, _) = Calendar.Period.ymds period in + let buf = Buffer.create 13 in + let append str = + if Buffer.length buf > 0 then + Buffer.add_string buf ", "; + Buffer.add_string buf str + in + if years >= 2 then append (sprintf "%d years" years) + else if years = 1 then append "1 year"; + if months >= 2 then append (sprintf "%d months" months) + else if months = 1 then append "1 month"; + if days >= 2 then append (sprintf "%d days" days) + else if days = 1 then append "1 day"; + let str = Buffer.contents buf in + if str <> "" then str else "-" -- 1.8.3.1