diff --git a/ChangeLog b/ChangeLog index aad6296dd..9d905a4a1 100644 --- a/ChangeLog +++ b/ChangeLog @@ -8,6 +8,8 @@ by using the 'log' command in place of 'add' (thanks to Cory Donnelly). + Added features #36 and #37, providing annual versions of the 'history' and 'ghistory' command as 'history.annual' and 'ghistory.annual'. + + Added feature #363 supporting iCalendar export via the 'export.ical' + command. + Fixed bug #406 so that task now includes command aliases in the _commands helper command used by shell completion scripts. diff --git a/NEWS b/NEWS index 9322a0643..d03df28b6 100644 --- a/NEWS +++ b/NEWS @@ -18,6 +18,7 @@ New Features in task 1.9 - New 'log' command to add tasks that are already completed. - New annual history and ghistory command variations. - Alias support in shell completion scripts. + - New iCalendar export format. - Task is now part of Debian Please refer to the ChangeLog file for full details. There are too many to diff --git a/doc/man/task.1 b/doc/man/task.1 index 84aa09eef..49ec59fe4 100644 --- a/doc/man/task.1 +++ b/doc/man/task.1 @@ -133,8 +133,14 @@ Shows task database statistics. Imports tasks from a variety of formats. .TP -.B export \fIfile -Exports all tasks as a CSV file. +.B export +Exports all tasks in CSV format. This command is an alias to the export.csv command. +Redirect the output to a file, if you wish to save it, or pipe it to another command. + +.TP +.B export.ical +Exports all tasks in iCalendar format. +Redirect the output to a file, if you wish to save it, or pipe it to another command. .TP .B color [sample] diff --git a/doc/man/taskrc.5 b/doc/man/taskrc.5 index 9a8faaf71..6b5e225fc 100644 --- a/doc/man/taskrc.5 +++ b/doc/man/taskrc.5 @@ -273,7 +273,8 @@ way. .B alias.rm=delete Task supports command aliases. This alias provides an alternate name (rm) for the delete command. You can use aliases to provide alternate names for any of -task's commands. +task's commands. Several commands you may use are actually aliases - 'history', +for example, or 'export'. .SS DATES diff --git a/i18n/strings.en-US b/i18n/strings.en-US index 5c50550d2..68f3a537b 100644 --- a/i18n/strings.en-US +++ b/i18n/strings.en-US @@ -40,10 +40,9 @@ 208 done 209 duplicate 210 edit -211 export + 212 help -213 history.monthly -214 ghistory.monthly + 215 import 216 info 217 prepend @@ -60,8 +59,6 @@ 228 version 229 shell 230 config -231 history.annual -232 ghistory.annual # 3xx Attributes - must be sequential 300 project diff --git a/src/Cmd.cpp b/src/Cmd.cpp index 4798f1676..c3c240926 100644 --- a/src/Cmd.cpp +++ b/src/Cmd.cpp @@ -105,12 +105,21 @@ void Cmd::load () { if (commands.size () == 0) { + // Commands whose names are not localized. commands.push_back ("_projects"); commands.push_back ("_tags"); commands.push_back ("_commands"); commands.push_back ("_ids"); commands.push_back ("_config"); commands.push_back ("_version"); + commands.push_back ("export.csv"); + commands.push_back ("export.ical"); + commands.push_back ("history.monthly"); + commands.push_back ("history.annual"); + commands.push_back ("ghistory.monthly"); + commands.push_back ("ghistory.annual"); + + // Commands whose names are localized. commands.push_back (context.stringtable.get (CMD_ADD, "add")); commands.push_back (context.stringtable.get (CMD_APPEND, "append")); commands.push_back (context.stringtable.get (CMD_ANNOTATE, "annotate")); @@ -121,12 +130,7 @@ void Cmd::load () commands.push_back (context.stringtable.get (CMD_DONE, "done")); commands.push_back (context.stringtable.get (CMD_DUPLICATE, "duplicate")); commands.push_back (context.stringtable.get (CMD_EDIT, "edit")); - commands.push_back (context.stringtable.get (CMD_EXPORT, "export")); commands.push_back (context.stringtable.get (CMD_HELP, "help")); - commands.push_back (context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly")); - commands.push_back (context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual")); - commands.push_back (context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly")); - commands.push_back (context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual")); commands.push_back (context.stringtable.get (CMD_IMPORT, "import")); commands.push_back (context.stringtable.get (CMD_INFO, "info")); commands.push_back (context.stringtable.get (CMD_LOG, "log")); @@ -198,15 +202,16 @@ bool Cmd::isReadOnlyCommand () command == "_ids" || command == "_config" || command == "_version" || + command == "export.csv" || + command == "export.ical" || + command == "history.monthly" || + command == "history.annual" || + command == "ghistory.monthly" || + command == "ghistory.annual" || command == context.stringtable.get (CMD_CALENDAR, "calendar") || command == context.stringtable.get (CMD_COLORS, "colors") || command == context.stringtable.get (CMD_CONFIG, "config") || - command == context.stringtable.get (CMD_EXPORT, "export") || command == context.stringtable.get (CMD_HELP, "help") || - command == context.stringtable.get (CMD_HISTORY_MONTHLY, "history.monthly") || - command == context.stringtable.get (CMD_HISTORY_ANNUAL, "history.annual") || - command == context.stringtable.get (CMD_GHISTORY_MONTHLY, "ghistory.monthly") || - command == context.stringtable.get (CMD_GHISTORY_ANNUAL, "ghistory.annual") || command == context.stringtable.get (CMD_INFO, "info") || command == context.stringtable.get (CMD_PROJECTS, "projects") || command == context.stringtable.get (CMD_SHELL, "shell") || diff --git a/src/Config.cpp b/src/Config.cpp index 75e23460a..1c014e543 100644 --- a/src/Config.cpp +++ b/src/Config.cpp @@ -159,10 +159,14 @@ std::string Config::defaults = "#import.synonym.tags=?\n" "#import.synonym.uuid=?\n" "\n" + "# Export Controls\n" + "export.ical.class=PRIVATE # Could be PUBLIC, PRIVATE or CONFIDENTIAL\n" + "\n" "# Aliases - alternate names for commands\n" "alias.rm=delete # Alias for the delete command\n" - "alias.history=history.monthly # Prefer monthly history reports\n" - "alias.ghistory=ghistory.monthly # Prefer monthly graphical history reports\n" + "alias.history=history.monthly # Prefer monthly over annual history reports\n" + "alias.ghistory=ghistory.monthly # Prefer monthly graphical over annual history reports\n" + "alias.export=export.csv # Prefer CSV over iCal export\n" "\n" "# Fields: id,uuid,project,priority,priority_long,entry,entry_time,\n" // TODO "# start,entry_time,due,recur,recurrence_indicator,age,\n" // TODO diff --git a/src/Context.cpp b/src/Context.cpp index 844913354..aa26d4030 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -229,7 +229,8 @@ int Context::dispatch (std::string &out) else if (cmd.command == "delete") { rc = handleDelete (out); } else if (cmd.command == "start") { rc = handleStart (out); } else if (cmd.command == "stop") { rc = handleStop (out); } - else if (cmd.command == "export") { rc = handleExport (out); } + else if (cmd.command == "export.csv") { rc = handleExportCSV (out); } + else if (cmd.command == "export.ical") { rc = handleExportiCal (out); } else if (cmd.command == "import") { rc = handleImport (out); } else if (cmd.command == "duplicate") { rc = handleDuplicate (out); } else if (cmd.command == "edit") { rc = handleEdit (out); } diff --git a/src/Makefile.am b/src/Makefile.am index eae558277..d014105ab 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -5,12 +5,12 @@ task_SOURCES = API.cpp Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp \ Grid.cpp Hooks.cpp Keymap.cpp Location.cpp Nibbler.cpp \ Path.cpp Permission.cpp Record.cpp Sequence.cpp \ StringTable.cpp Subst.cpp TDB.cpp Table.cpp Task.cpp Timer.cpp \ - command.cpp custom.cpp edit.cpp import.cpp interactive.cpp \ - main.cpp recur.cpp report.cpp rules.cpp text.cpp util.cpp \ - API.h Att.h Cmd.h Color.h Config.h Context.h Date.h \ - Directory.h Duration.h File.h Filter.h Grid.h Hooks.h Keymap.h \ - Location.h Nibbler.h Path.h Permission.h Record.h Sequence.h \ - StringTable.h Subst.h TDB.h Table.h Task.h Timer.h i18n.h \ - main.h text.h util.h + command.cpp custom.cpp edit.cpp export.cpp import.cpp \ + interactive.cpp main.cpp recur.cpp report.cpp rules.cpp \ + text.cpp util.cpp API.h Att.h Cmd.h Color.h Config.h Context.h \ + Date.h Directory.h Duration.h File.h Filter.h Grid.h Hooks.h \ + Keymap.h Location.h Nibbler.h Path.h Permission.h Record.h \ + Sequence.h StringTable.h Subst.h TDB.h Table.h Task.h Timer.h \ + i18n.h main.h text.h util.h task_CPPFLAGS=$(LUA_CFLAGS) task_LDFLAGS=$(LUA_LFLAGS) diff --git a/src/command.cpp b/src/command.cpp index ea9bf7274..64bd717d3 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -1306,59 +1306,6 @@ int handleDone (std::string &outs) return rc; } -//////////////////////////////////////////////////////////////////////////////// -int handleExport (std::string &outs) -{ - int rc = 0; - - if (context.hooks.trigger ("pre-export-command")) - { - std::stringstream out; - - // Deliberately no 'id'. - out << "'uuid'," - << "'status'," - << "'tags'," - << "'entry'," - << "'start'," - << "'due'," - << "'recur'," - << "'end'," - << "'project'," - << "'priority'," - << "'fg'," - << "'bg'," - << "'description'" - << "\n"; - - int count = 0; - - // Get all the tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.load (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - - foreach (task, tasks) - { - context.hooks.trigger ("pre-display", *task); - - if (task->getStatus () != Task::recurring) - { - out << task->composeCSV ().c_str (); - ++count; - } - } - - outs = out.str (); - context.hooks.trigger ("post-export-command"); - } - - return rc; -} - //////////////////////////////////////////////////////////////////////////////// int handleModify (std::string &outs) { diff --git a/src/export.cpp b/src/export.cpp new file mode 100644 index 000000000..3e49724ee --- /dev/null +++ b/src/export.cpp @@ -0,0 +1,229 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2010, Paul Beckingham, Federico Hernandez. +// All rights reserved. +// +// 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 +#include + +#include "Att.h" +#include "text.h" +#include "util.h" +#include "main.h" +#include "../auto.h" + +extern Context context; + +//////////////////////////////////////////////////////////////////////////////// +int handleExportCSV (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-export-command")) + { + std::stringstream out; + + // Deliberately no 'id'. + out << "'uuid'," + << "'status'," + << "'tags'," + << "'entry'," + << "'start'," + << "'due'," + << "'recur'," + << "'end'," + << "'project'," + << "'priority'," + << "'fg'," + << "'bg'," + << "'description'" + << "\n"; + + int count = 0; + + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + context.hooks.trigger ("pre-display", *task); + + if (task->getStatus () != Task::recurring) + { + out << task->composeCSV ().c_str (); + ++count; + } + } + + outs = out.str (); + context.hooks.trigger ("post-export-command"); + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// +// http://tools.ietf.org/html/rfc5545 +// +// Note: Recurring tasks could be included in more detail. +int handleExportiCal (std::string &outs) +{ + int rc = 0; + + if (context.hooks.trigger ("pre-export-command")) + { + std::stringstream out; + + out << "BEGIN:VCALENDAR\n" + << "VERSION:2.0\n" + << "PRODID:-//GBF//" << PACKAGE_STRING << "//EN\n"; + + int count = 0; + + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.load (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); + + foreach (task, tasks) + { + context.hooks.trigger ("pre-display", *task); + + if (task->getStatus () != Task::recurring) + { + out << "BEGIN:VTODO\n"; + + // Required UID:20070313T123432Z-456553@example.com + out << "UID:" << task->get ("uuid") << "\n"; + + // Required DTSTAMP:20070313T123432Z + Date entry (atoi (task->get ("entry").c_str ())); + out << "DTSTAMP:" << entry.toISO () << "\n"; + + // Optional DTSTART:20070514T110000Z + if (task->has ("start")) + { + Date start (atoi (task->get ("start").c_str ())); + out << "DTSTART:" << start.toISO () << "\n"; + } + + // Optional DUE:20070709T130000Z + if (task->has ("due")) + { + Date due (atoi (task->get ("due").c_str ())); + out << "DUE:" << due.toISO () << "\n"; + } + + // Optional COMPLETED:20070707T100000Z + if (task->has ("end") && task->getStatus () == Task::completed) + { + Date end (atoi (task->get ("end").c_str ())); + out << "COMPLETED:" << end.toISO () << "\n"; + } + + out << "SUMMARY:" << task->get ("description") << "\n"; + + // Optional CLASS:PUBLIC/PRIVATE/CONFIDENTIAL + std::string classification = context.config.get ("export.ical.class"); + if (classification == "") + classification = "PRIVATE"; + out << "CLASS:" << classification << "\n"; + + // Optional multiple CATEGORIES:FAMILY,FINANCE + if (task->getTagCount () > 0) + { + std::vector tags; + task->getTags (tags); + std::string all; + join (all, ",", tags); + out << "CATEGORIES:" << all << "\n"; + } + + // Optional PRIORITY: + // 1-4 H + // 5 M + // 6-9 L + if (task->has ("priority")) + { + out << "PRIORITY:"; + std::string priority = task->get ("priority"); + + if (priority == "H") out << "1"; + else if (priority == "M") out << "5"; + else out << "9"; + + out << "\n"; + } + + // Optional STATUS:NEEDS-ACTION/IN-PROCESS/COMPLETED/CANCELLED + out << "STATUS:"; + Task::status stat = task->getStatus (); + if (stat == Task::pending || stat == Task::waiting) + { + if (task->has ("start")) + out << "IN-PROCESS"; + else + out << "NEEDS-ACTION"; + } + else if (stat == Task::completed) + { + out << "COMPLETED"; + } + else if (stat == Task::deleted) + { + out << "CANCELLED"; + } + out << "\n"; + + // Optional COMMENT:annotation1 + // Optional COMMENT:annotation2 + std::vector annotations; + task->getAnnotations (annotations); + foreach (anno, annotations) + out << "COMMENT:" << anno->value () << "\n"; + + out << "END:VTODO\n"; + ++count; + } + } + + out << "END:VCALENDAR\n"; + + outs = out.str (); + context.hooks.trigger ("post-export-command"); + } + + return rc; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/i18n.h b/src/i18n.h index 8a4e00984..ac5127da1 100644 --- a/src/i18n.h +++ b/src/i18n.h @@ -77,10 +77,9 @@ #define CMD_DONE 208 #define CMD_DUPLICATE 209 #define CMD_EDIT 210 -#define CMD_EXPORT 211 + #define CMD_HELP 212 -#define CMD_HISTORY_MONTHLY 213 -#define CMD_GHISTORY_MONTHLY 214 + #define CMD_IMPORT 215 #define CMD_INFO 216 #define CMD_PREPEND 217 @@ -97,8 +96,6 @@ #define CMD_VERSION 228 #define CMD_SHELL 229 #define CMD_CONFIG 230 -#define CMD_HISTORY_ANNUAL 231 -#define CMD_GHISTORY_ANNUAL 232 // 3xx Attributes #define ATT_PROJECT 300 diff --git a/src/main.h b/src/main.h index 60c1839c0..66831cc48 100644 --- a/src/main.h +++ b/src/main.h @@ -59,7 +59,6 @@ int handleAdd (std::string &); int handleLog (std::string &); int handleAppend (std::string &); int handlePrepend (std::string &); -int handleExport (std::string &); int handleDone (std::string &); int handleModify (std::string &); int handleProjects (std::string &); @@ -127,6 +126,10 @@ std::string colorizeDebug (const std::string&); // import.cpp int handleImport (std::string&); +// export.cpp +int handleExportCSV (std::string &); +int handleExportiCal (std::string &); + // list template /////////////////////////////////////////////////////////////////////////////// template bool listDiff (const T& left, const T& right) diff --git a/src/report.cpp b/src/report.cpp index 52d507278..2d402eb91 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -192,7 +192,11 @@ int shortUsage (std::string &outs) row = table.addRow (); table.addCell (row, 1, "task export"); - table.addCell (row, 2, "Lists all tasks in CSV format."); + table.addCell (row, 2, "Lists all tasks in CSV format. Alias to export.csv"); + + row = table.addRow (); + table.addCell (row, 1, "task export.ical"); + table.addCell (row, 2, "Lists all tasks in iCalendar format."); row = table.addRow (); table.addCell (row, 1, "task color [sample]"); diff --git a/src/tests/Makefile b/src/tests/Makefile index 0d42bc000..f93191ec4 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -9,8 +9,9 @@ OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \ ../t-Nibbler.o ../t-Location.o ../t-Filter.o ../t-Context.o \ ../t-Keymap.o ../t-command.o ../t-interactive.o ../t-report.o \ ../t-Grid.o ../t-Color.o ../t-rules.o ../t-recur.o ../t-custom.o \ - ../t-import.o ../t-edit.o ../t-Timer.o ../t-Permission.o ../t-Path.o \ - ../t-File.o ../t-Directory.o ../t-Hooks.o ../t-API.o + ../t-export.o ../t-import.o ../t-edit.o ../t-Timer.o \ + ../t-Permission.o ../t-Path.o ../t-File.o ../t-Directory.o \ + ../t-Hooks.o ../t-API.o all: $(PROJECT)