diff --git a/configure.ac b/configure.ac index b04bee072..ba9b1ac3c 100644 --- a/configure.ac +++ b/configure.ac @@ -51,6 +51,35 @@ else AC_DEFINE([UNKNOWN], [], [Compiling on Unknown]) fi +# Check for Lua. +AC_ARG_ENABLE([lua], AS_HELP_STRING([--enable-lua], [Disable feature lua])) +AC_ARG_WITH(lua-inc, [--with-lua-inc=DIR, Lua include files are in DIR],lua_inc=$withval,lua_inc='') +AC_ARG_WITH(lua-lib, [--with-lua-lib=DIR, Lua library files are in DIR],lua_lib=$withval,lua_lib='') + +if test "x$enable_lua" = "xyes" ; then + ac_save_CPPFLAGS="$CPPFLAGS" + ac_save_CFLAGS="$CFLAGS" + ac_save_LDFLAGS="$LDFLAGS" + + LUA_CFLAGS="" + LUA_LFLAGS="" + + if test -n "$lua_inc"; then + CFLAGS="$CFLAGS -I$lua_inc" + CPPFLAGS="$CPPFLAGS -I$lua_inc" + fi + if test -n "$lua_lib"; then + LDFLAGS="$LDFLAGS -L$lua_lib" + LUA_LFLAGS="-llua" + fi +fi + +if test "x$enable_lua" = "xyes" ; then + AC_SUBST(LUA_CFLAGS) + AC_SUBST(LUA_LFLAGS) + AC_DEFINE([HAVE_LIBLUA], [1], [Building with Lua support]) +fi + AM_INIT_AUTOMAKE AC_CONFIG_SRCDIR([src/main.cpp]) AC_CONFIG_HEADER([auto.h]) @@ -64,6 +93,7 @@ AC_SUBST(CFLAGS) # Checks for libraries. AC_CHECK_LIB(ncurses,initscr) +AC_CHECK_LIB(lua,lua_open) # Checks for header files. AC_HEADER_STDC diff --git a/src/API.cpp b/src/API.cpp new file mode 100644 index 000000000..a991e413c --- /dev/null +++ b/src/API.cpp @@ -0,0 +1,617 @@ +//////////////////////////////////////////////////////////////////////////////// +// Task Lua API +// +// Copyright 2006 - 2010, Paul Beckingham. +// 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 +// +// ------------- +// +// Copyright © 1994–2008 Lua.org, PUC-Rio. +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. +// +//////////////////////////////////////////////////////////////////////////////// + +#include // TODO Remove +#include +#include "Context.h" +#include "API.h" + +extern Context context; + +#ifdef HAVE_LIBLUA + +//////////////////////////////////////////////////////////////////////////////// +// Returns a string representing the task version number, such as '1.9.0'. +static int api_task_version (lua_State* L) +{ + lua_pushstring (L, PACKAGE_VERSION); + return 1; +} + +//////////////////////////////////////////////////////////////////////////////// +// Returns a string representing the Lua version number, such as '5.1.4'. +// Lua 5.2.0 has a 'lua_version' call, but 5.1.4 is the target. +static int api_task_lua_version (lua_State* L) +{ + // Convert "Lua 5.1.4" -> "5.1.4" + std::string ver = LUA_RELEASE; + lua_pushstring (L, ver.substr (4, std::string::npos).c_str ()); + return 1; +} + +//////////////////////////////////////////////////////////////////////////////// +// Returns the type of OS that task is running on. +static int api_task_os (lua_State* L) +{ +#if defined (DARWIN) + lua_pushstring (L, "darwin"); +#elif defined (SOLARIS) + lua_pushstring (L, "solaris"); +#elif defined (CYGWIN) + lua_pushstring (L, "cygwin"); +#elif defined (OPENBSD) + lua_pushstring (L, "openbsd"); +#elif defined (HAIKU) + lua_pushstring (L, "haiku"); +#elif defined (FREEBSD) + lua_pushstring (L, "freebsd"); +#elif defined (LINUX) + lua_pushstring (L, "linux"); +#else + lua_pushstring (L, "unknown"); +#endif + + return 1; +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_feature (lua_State* L) +{ + std::string name = luaL_checkstring (L, 1); + bool value = false; + + if (name == "readline") + { +#ifdef HAVE_READLINE + value = true; +#endif + } + + else if (name == "ncurses") + { +#ifdef HAVE_NCURSES + value = true; +#endif + } + + lua_pushnumber (L, value ? 1 : 0); + return 1; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +static int api_task_aliases () +{ + return {}; +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns values from .taskrc, by name. +static int api_task_get_config (name) +{ + return "foo" +} + +//////////////////////////////////////////////////////////////////////////////// +-- Temporarily sets .taskrc values, by name. +static int api_task_set_config (name, value) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns an internationalized string, by string ID, from the appropriate +-- locale-based strings file. +static int api_task_i18n_string (id) +{ + return "le foo" +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns a list of tips, from the appropriate locale-based tips file. +static int api_task_i18n_tips () +{ + return {} +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns the name of the current command. +static int api_task_get_command () +{ + return "list" +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns a list of string messages generated so far. +static int api_task_get_header_messages () +{ + return {} +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_footnote_messages () +{ + return {} +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_debug_messages () +{ + return {} +} + +//////////////////////////////////////////////////////////////////////////////// +-- Records additional messages, for subsequent display. +static int api_task_header_message (text) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_footnote_message (text) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_debug_message (text) +{ +} +*/ + +//////////////////////////////////////////////////////////////////////////////// +// Causes the shell or interactive mode task to exit. Ordinarily this does not +// occur. +static int api_task_exit (lua_State* L) +{ + // TODO Is this the correct exception? How does the shell handle this? + throw std::string ("Exiting."); + return 0; +} + +/* +//////////////////////////////////////////////////////////////////////////////// +-- Shuts off the hook system for any subsequent hook calls for this command. +static int api_task_inhibit_further_hooks () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +-- Returns a table that contains a complete copy of the task. +static int api_task_get (id) +{ + return task +} + +//////////////////////////////////////////////////////////////////////////////// +-- Creates a new task from the data specified in the table t. +static int api_task_add (t) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +-- Modifies the task described in the table t. +static int api_task_modify (t) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +-- 'id' is the task id passed to the hook function. Date attributes are +-- returned as a numeric epoch offset. Tags and annotations are returned +-- as tables. A nil value indicates a missing value. +static int api_task_get_uuid (id) +{ + return task.uuid +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_description (id) +{ + return task.description +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_annotations (id) +{ + return task.annotations +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_project (id) +{ + return task.project +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_priority (id) +{ + return task.priority +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_tags (id) +{ + return task.tags +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_status (id) +{ + return task.status +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_due (id) +{ + return task.due_date +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_entry (id) +{ + return task.entry_date +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_start (id) +{ + return task.start_date +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_end (id) +{ + return task.end_date +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_recur (id) +{ + return task.recur +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_until (id) +{ + return task.until_date +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_get_wait (id) +{ + return task.wait_date +} + +//////////////////////////////////////////////////////////////////////////////// +-- 'id' is the task id passed to the hook function. Date attributes are +-- expected as numeric epoch offsets. Tags and annotations are expected +-- as tables. A nil value indicates a missing value. +static int api_task_set_description (id, value) +{ + task.description = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_annotations (id, value) +{ + task.annotations = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_project (id, value) +{ + task.project = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_priority (id, value) +{ + task.priority = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_tags (id, value) +{ + task.tags = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_status (id, value) +{ + task.status = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_due (id, value) +{ + task.due_date = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_start (id, value) +{ + task.start_date = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_recur (id, value) +{ + task.recur = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_until (id, value) +{ + task.until_date = value +} + +//////////////////////////////////////////////////////////////////////////////// +static int api_task_set_wait (id, value) +{ + task.wait_date = value +} +*/ + +//////////////////////////////////////////////////////////////////////////////// +API::API () +: L (NULL) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +API::~API () +{ + if (L) + { + lua_close (L); + L = NULL; + } +} + +//////////////////////////////////////////////////////////////////////////////// +void API::initialize () +{ + // Initialize Lua. + L = lua_open (); + luaL_openlibs (L); // TODO Error handling + + // Register all the API functions in Lua global space. + lua_pushcfunction (L, api_task_version); lua_setglobal (L, "task_version"); + lua_pushcfunction (L, api_task_lua_version); lua_setglobal (L, "task_lua_version"); + lua_pushcfunction (L, api_task_os); lua_setglobal (L, "task_os"); + lua_pushcfunction (L, api_task_feature); lua_setglobal (L, "task_feature"); +/* + lua_pushcfunction (L, api_task_aliases); lua_setglobal (L, "api_task_aliases"); + lua_pushcfunction (L, api_task_get_config); lua_setglobal (L, "api_task_get_config"); + lua_pushcfunction (L, api_task_set_config); lua_setglobal (L, "api_task_set_config"); + lua_pushcfunction (L, api_task_i18n_string); lua_setglobal (L, "api_task_i18n_string"); + lua_pushcfunction (L, api_task_i18n_tips); lua_setglobal (L, "api_task_i18n_tips"); + lua_pushcfunction (L, api_task_get_command); lua_setglobal (L, "api_task_get_command"); + lua_pushcfunction (L, api_task_get_header_messages); lua_setglobal (L, "api_task_get_header_messages"); + lua_pushcfunction (L, api_task_get_footnote_messages); lua_setglobal (L, "api_task_get_footnote_messages"); + lua_pushcfunction (L, api_task_get_debug_messages); lua_setglobal (L, "api_task_get_debug_messages"); + lua_pushcfunction (L, api_task_header_message); lua_setglobal (L, "api_task_header_message"); + lua_pushcfunction (L, api_task_footnote_message); lua_setglobal (L, "api_task_footnote_message"); + lua_pushcfunction (L, api_task_debug_message); lua_setglobal (L, "api_task_debug_message"); +*/ + lua_pushcfunction (L, api_task_exit); lua_setglobal (L, "task_exit"); +/* + lua_pushcfunction (L, api_task_inhibit_further_hooks); lua_setglobal (L, "api_task_inhibit_further_hooks"); + lua_pushcfunction (L, api_task_get); lua_setglobal (L, "api_task_get"); + lua_pushcfunction (L, api_task_add); lua_setglobal (L, "api_task_add"); + lua_pushcfunction (L, api_task_modify); lua_setglobal (L, "api_task_modify"); + lua_pushcfunction (L, api_task_get_uuid); lua_setglobal (L, "api_task_get_uuid"); + lua_pushcfunction (L, api_task_get_description); lua_setglobal (L, "api_task_get_description"); + lua_pushcfunction (L, api_task_get_annotations); lua_setglobal (L, "api_task_get_annotations"); + lua_pushcfunction (L, api_task_get_project); lua_setglobal (L, "api_task_get_project"); + lua_pushcfunction (L, api_task_get_priority); lua_setglobal (L, "api_task_get_priority"); + lua_pushcfunction (L, api_task_get_tags); lua_setglobal (L, "api_task_get_tags"); + lua_pushcfunction (L, api_task_get_status); lua_setglobal (L, "api_task_get_status"); + lua_pushcfunction (L, api_task_get_due); lua_setglobal (L, "api_task_get_due"); + lua_pushcfunction (L, api_task_get_entry); lua_setglobal (L, "api_task_get_entry"); + lua_pushcfunction (L, api_task_get_start); lua_setglobal (L, "api_task_get_start"); + lua_pushcfunction (L, api_task_get_end); lua_setglobal (L, "api_task_get_end"); + lua_pushcfunction (L, api_task_get_recur); lua_setglobal (L, "api_task_get_recur"); + lua_pushcfunction (L, api_task_get_until); lua_setglobal (L, "api_task_get_until"); + lua_pushcfunction (L, api_task_get_wait); lua_setglobal (L, "api_task_get_wait"); + lua_pushcfunction (L, api_task_set_description); lua_setglobal (L, "api_task_set_description"); + lua_pushcfunction (L, api_task_set_annotations); lua_setglobal (L, "api_task_set_annotations"); + lua_pushcfunction (L, api_task_set_project); lua_setglobal (L, "api_task_set_project"); + lua_pushcfunction (L, api_task_set_priority); lua_setglobal (L, "api_task_set_priority"); + lua_pushcfunction (L, api_task_set_tags); lua_setglobal (L, "api_task_set_tags"); + lua_pushcfunction (L, api_task_set_status); lua_setglobal (L, "api_task_set_status"); + lua_pushcfunction (L, api_task_set_due); lua_setglobal (L, "api_task_set_due"); + lua_pushcfunction (L, api_task_set_start); lua_setglobal (L, "api_task_set_start"); + lua_pushcfunction (L, api_task_set_recur); lua_setglobal (L, "api_task_set_recur"); + lua_pushcfunction (L, api_task_set_until); lua_setglobal (L, "api_task_set_until"); + lua_pushcfunction (L, api_task_set_wait); lua_setglobal (L, "api_task_set_wait"); +*/ +} + +//////////////////////////////////////////////////////////////////////////////// +bool API::callProgramHook ( + const std::string& file, + const std::string& function) +{ + loadFile (file); + + // Get function. + lua_getglobal (L, function.c_str ()); + if (!lua_isfunction (L, -1)) + { + lua_pop (L, 1); + throw std::string ("The Lua function '") + function + "' was not found."; + } + + // Make call. + if (lua_pcall (L, 0, 2, 0) != 0) + throw std::string ("Error calling '") + function + "' - " + lua_tostring (L, -1); + + // Call successful - get return values. + if (!lua_isnumber (L, -2)) + throw std::string ("Error: '") + function + "' did not return a success indicator"; + + if (!lua_isstring (L, -1) && !lua_isnil (L, -1)) + throw std::string ("Error: '") + function + "' did not return a message or nil"; + + int rc = lua_tointeger (L, -2); + const char* message = lua_tostring (L, -1); + + if (rc == 0) + { + if (message) + context.footnote (std::string ("Warning: ") + message); + } + else + { + if (message) + throw std::string (message); + } + + lua_pop (L, 1); + return rc == 0 ? true : false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool API::callListHook ( + const std::string& file, + const std::string& function/*, + iterator i*/) +{ + loadFile (file); + + // TODO Get function. + // TODO Prepare args. + // TODO Make call. + // TODO Get exit status. + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool API::callTaskHook ( + const std::string& file, + const std::string& function, + int id) +{ + loadFile (file); + + // Get function. + lua_getglobal (L, function.c_str ()); + if (!lua_isfunction (L, -1)) + { + lua_pop (L, 1); + throw std::string ("The Lua function '") + function + "' was not found."; + } + + // Prepare args. + lua_pushnumber (L, id); + + // Make call. + if (lua_pcall (L, 1, 2, 0) != 0) + throw std::string ("Error calling '") + function + "' - " + lua_tostring (L, -1); + + // Call successful - get return values. + if (!lua_isnumber (L, -2)) + throw std::string ("Error: '") + function + "' did not return a success indicator"; + + if (!lua_isstring (L, -1) && !lua_isnil (L, -1)) + throw std::string ("Error: '") + function + "' did not return a message or nil"; + + int rc = lua_tointeger (L, -2); + const char* message = lua_tostring (L, -1); + + if (rc == 0) + { + if (message) + context.footnote (std::string ("Warning: ") + message); + } + else + { + if (message) + throw std::string (message); + } + + lua_pop (L, 1); + return rc == 0 ? true : false; +} + +//////////////////////////////////////////////////////////////////////////////// +bool API::callFieldHook ( + const std::string& file, + const std::string& function, + const std::string& field, + const std::string& value) +{ + loadFile (file); + + // TODO Get function. + // TODO Prepare args. + // TODO Make call. + // TODO Get exit status. + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +void API::loadFile (const std::string& file) +{ + // If the file is not loaded. + if (std::find (loaded.begin (), loaded.end (), file) == loaded.end ()) + { + // Load the file, if possible. + if (luaL_loadfile (L, file.c_str ()) || lua_pcall (L, 0, 0, 0)) + throw std::string ("Error: ") + std::string (lua_tostring (L, -1)); + + // Mark this as loaded, so as to not bother again. + loaded.push_back (file); + } +} + +//////////////////////////////////////////////////////////////////////////////// + +#endif + diff --git a/src/API.h b/src/API.h new file mode 100644 index 000000000..ec808085d --- /dev/null +++ b/src/API.h @@ -0,0 +1,66 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2010, Paul Beckingham. +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_API +#define INCLUDED_API + +#include "auto.h" +#ifdef HAVE_LIBLUA + +#include +#include +extern "C" +{ + #include "lua.h" + #include "lualib.h" + #include "lauxlib.h" +} + +class API +{ +public: + API (); + API (const API&); + API& operator= (const API&); + ~API (); + + void initialize (); + bool callProgramHook (const std::string&, const std::string&); + bool callListHook (const std::string&, const std::string& /*, iterator */); + bool callTaskHook (const std::string&, const std::string&, int); + bool callFieldHook (const std::string&, const std::string&, const std::string&, const std::string&); + +private: + void loadFile (const std::string&); + +public: + lua_State* L; + std::vector loaded; +}; + +#endif +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/Context.cpp b/src/Context.cpp index 2907e8635..4617fd3bf 100644 --- a/src/Context.cpp +++ b/src/Context.cpp @@ -83,6 +83,11 @@ void Context::initialize (int argc, char** argv) } initialize (); + + // Hook system init, plus post-start event occurring at the first possible + // moment after hook initialization. + hooks.initialize (); + hooks.trigger ("post-start"); } //////////////////////////////////////////////////////////////////////////////// @@ -134,8 +139,6 @@ void Context::initialize () int Context::run () { int rc; - Timer t ("Context::run"); - std::string output; try { @@ -156,30 +159,39 @@ int Context::run () } // Dump all debug messages. + hooks.trigger ("pre-debug"); if (config.getBoolean ("debug")) foreach (d, debugMessages) if (config.getBoolean ("color") || config.getBoolean ("_forcecolor")) std::cout << colorizeDebug (*d) << std::endl; else std::cout << *d << std::endl; + hooks.trigger ("post-debug"); // Dump all headers. + hooks.trigger ("pre-header"); foreach (h, headers) if (config.getBoolean ("color") || config.getBoolean ("_forcecolor")) std::cout << colorizeHeader (*h) << std::endl; else std::cout << *h << std::endl; + hooks.trigger ("post-header"); // Dump the report output. + hooks.trigger ("pre-output"); std::cout << output; + hooks.trigger ("post-output"); // Dump all footnotes. + hooks.trigger ("pre-footnote"); foreach (f, footnotes) if (config.getBoolean ("color") || config.getBoolean ("_forcecolor")) std::cout << colorizeFootnote (*f) << std::endl; else std::cout << *f << std::endl; + hooks.trigger ("post-footnote"); + hooks.trigger ("pre-exit"); return rc; } @@ -187,8 +199,11 @@ int Context::run () int Context::dispatch (std::string &out) { int rc = 0; + Timer t ("Context::dispatch"); + hooks.trigger ("pre-dispatch"); + // TODO Just look at this thing. It cries out for a dispatch table. if (cmd.command == "projects") { rc = handleProjects (out); } else if (cmd.command == "tags") { rc = handleTags (out); } @@ -239,6 +254,7 @@ int Context::dispatch (std::string &out) if (cmd.isWriteCommand () && !inShadow) shadow (); + hooks.trigger ("post-dispatch"); return rc; } @@ -357,8 +373,6 @@ void Context::loadCorrectConfigFile () "Could not read home directory from the passwd file.")); std::string home = pw->pw_dir; -// std::string rc = home + "/.taskrc"; -// std::string data = home + "/.task"; File rc (home + "/.taskrc"); Directory data (home + "./task"); diff --git a/src/Context.h b/src/Context.h index 8ab7ee523..1382619cf 100644 --- a/src/Context.h +++ b/src/Context.h @@ -36,6 +36,7 @@ #include "Task.h" #include "TDB.h" #include "StringTable.h" +#include "Hooks.h" class Context { @@ -90,6 +91,7 @@ public: std::map aliases; std::vector tagAdditions; std::vector tagRemovals; + Hooks hooks; private: std::vector headers; diff --git a/src/Hooks.cpp b/src/Hooks.cpp new file mode 100644 index 000000000..7b2138622 --- /dev/null +++ b/src/Hooks.cpp @@ -0,0 +1,218 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2010, Paul Beckingham. +// 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 "Context.h" +#include "Hooks.h" + +extern Context context; + +//////////////////////////////////////////////////////////////////////////////// +Hook::Hook () +: event ("") +, file ("") +, function ("") +{ +} + +//////////////////////////////////////////////////////////////////////////////// +Hook::Hook (const std::string& e, const std::string& f, const std::string& fn) +: event (e) +, file (f) +, function (fn) +{ +} + +//////////////////////////////////////////////////////////////////////////////// +Hook::Hook (const Hook& other) +{ + event = other.event; + file = other.file; + function = other.function; +} + +//////////////////////////////////////////////////////////////////////////////// +Hook& Hook::operator= (const Hook& other) +{ + if (this != &other) + { + event = other.event; + file = other.file; + function = other.function; + } + + return *this; +} + +//////////////////////////////////////////////////////////////////////////////// +Hooks::Hooks () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +Hooks::~Hooks () +{ +} + +//////////////////////////////////////////////////////////////////////////////// +void Hooks::initialize () +{ +#ifdef HAVE_LIBLUA + api.initialize (); +#endif + + // TODO Enumerate all hooks, and tell API about the script files it must load + // in order to call them. Note that API will perform a deferred read, + // which means that if it isn't called, a script will not be loaded. + + std::vector vars; + context.config.all (vars); + + std::vector ::iterator it; + for (it = vars.begin (); it != vars.end (); ++it) + { + std::string type; + std::string name; + std::string value; + + // "." + Nibbler n (*it); + if (n.getUntil ('.', type) && + type == "hook" && + n.skip ('.') && + n.getUntilEOS (name)) + { + std::string value = context.config.get (*it); + Nibbler n (value); + + // : [, ...] + while (!n.depleted ()) + { + std::string file; + std::string function; + if (n.getUntil (':', file) && + n.skip (':') && + n.getUntil (',', function)) + { + context.debug (std::string ("Event '") + name + "' hooked by " + file + ", function " + function); + Hook h (name, Path::expand (file), function); + all.push_back (h); + + (void) n.skip (','); + } + else + throw std::string ("Malformed hook definition '") + *it + "'"; + } + } + } +} + +//////////////////////////////////////////////////////////////////////////////// +void Hooks::setTaskId (int id) +{ +#ifdef HAVE_LIBLUA + task_id = id; +#endif +} + +//////////////////////////////////////////////////////////////////////////////// +bool Hooks::trigger (const std::string& event) +{ +#ifdef HAVE_LIBLUA + std::vector ::iterator it; + for (it = all.begin (); it != all.end (); ++it) + { + if (it->event == event) + { + bool rc = true; + std::string type; + if (eventType (event, type)) + { + context.debug (std::string ("Event ") + event + " triggered"); + + // Figure out where to get the calling-context info from. + if (type == "program") rc = api.callProgramHook (it->file, it->function); + else if (type == "list") rc = api.callListHook (it->file, it->function/*, tasks*/); + else if (type == "task") rc = api.callTaskHook (it->file, it->function, task_id); + else if (type == "field") rc = api.callFieldHook (it->file, it->function, "field", "value"); + } + else + throw std::string ("Unrecognized hook event '") + event + "'"; + + // If any hook returns false, stop. + if (!rc) + return false; + } + } +#endif + + return true; +} + +//////////////////////////////////////////////////////////////////////////////// +bool Hooks::eventType (const std::string& event, std::string& type) +{ + if (event == "post-start" || + event == "pre-exit" || + event == "pre-debug" || event == "post-debug" || + event == "pre-header" || event == "post-header" || + event == "pre-footnote" || event == "post-footnote" || + event == "pre-output" || event == "post-output" || + event == "pre-dispatch" || event == "post-dispatch" || + event == "pre-gc" || event == "post-gc" || + event == "pre-undo" || event == "post-undo" || + event == "pre-file-lock" || event == "post-file-lock" || + event == "pre-add-command" || event == "post-add-command" || + event == "pre-delete-command" || event == "post-delete-command" || + event == "pre-info-command" || event == "post-info-command") + { + type = "program"; + return true; + } + else if (event == "?") + { + type = "list"; + return true; + } + else if (event == "pre-tag" || event == "post-tag" || + event == "pre-detag" || event == "post-detag" || + event == "pre-delete" || event == "post-delete" || + event == "pre-completed" || event == "post-completed") + { + type = "task"; + return true; + } + else if (event == "?") + { + type = "field"; + return true; + } + + return false; +} + +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/Hooks.h b/src/Hooks.h new file mode 100644 index 000000000..43c809244 --- /dev/null +++ b/src/Hooks.h @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////////////////// +// task - a command line task list manager. +// +// Copyright 2006 - 2010, Paul Beckingham. +// 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 +// +//////////////////////////////////////////////////////////////////////////////// +#ifndef INCLUDED_HOOKS +#define INCLUDED_HOOKS + +#include +#include +#include "API.h" +#include "auto.h" + +// Hook class representing a single hook, which is just a three-way map. +class Hook +{ +public: + Hook (); + Hook (const std::string&, const std::string&, const std::string&); + Hook (const Hook&); + Hook& operator= (const Hook&); + +public: + std::string event; + std::string file; + std::string function; +}; + +// Hooks class for managing the loading and calling of hook functions. +class Hooks +{ +public: + Hooks (); // Default constructor + ~Hooks (); // Destructor + Hooks (const Hooks&); // Deliberately unimplemented + Hooks& operator= (const Hooks&); // Deliberately unimplemented + + void initialize (); + + void setTaskId (int); +// void setField (const std::string&, const std::string&); +// void setTaskList (const std::vector &); + bool trigger (const std::string&); + +private: + bool eventType (const std::string&, std::string&); + +private: +#ifdef HAVE_LIBLUA + API api; +#endif + std::vector all; // All current hooks. +#ifdef HAVE_LIBLUA + int task_id; +#endif +}; + +#endif +//////////////////////////////////////////////////////////////////////////////// diff --git a/src/Makefile.am b/src/Makefile.am index 4bc6c9a73..eae558277 100644 --- a/src/Makefile.am +++ b/src/Makefile.am @@ -1,14 +1,16 @@ bin_PROGRAMS = task -task_SOURCES = Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp Date.cpp \ - Directory.cpp Duration.cpp File.cpp Filter.cpp Grid.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 \ - Att.h Cmd.h Color.h Config.h Context.h Date.h Directory.h \ - Duration.h File.h Filter.h Grid.h Keymap.h Location.h \ - Nibbler.h Path.h Permission.h Record.h Sequence.h \ +task_SHORTNAME = t +task_SOURCES = API.cpp Att.cpp Cmd.cpp Color.cpp Config.cpp Context.cpp \ + Date.cpp Directory.cpp Duration.cpp File.cpp Filter.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 - +task_CPPFLAGS=$(LUA_CFLAGS) +task_LDFLAGS=$(LUA_LFLAGS) diff --git a/src/TDB.cpp b/src/TDB.cpp index f2c8d4a4a..4506e0062 100644 --- a/src/TDB.cpp +++ b/src/TDB.cpp @@ -121,48 +121,49 @@ void TDB::location (const std::string& path) //////////////////////////////////////////////////////////////////////////////// void TDB::lock (bool lockFile /* = true */) { - mLock = lockFile; - - mPending.clear (); - mNew.clear (); - mPending.clear (); - - foreach (location, mLocations) + if (context.hooks.trigger ("pre-file-lock")) { - location->pending = openAndLock (location->path + "/pending.data"); - location->completed = openAndLock (location->path + "/completed.data"); - location->undo = openAndLock (location->path + "/undo.data"); - } + mLock = lockFile; - mAllOpenAndLocked = true; + mPending.clear (); + mNew.clear (); + mPending.clear (); + + foreach (location, mLocations) + { + location->pending = openAndLock (location->path + "/pending.data"); + location->completed = openAndLock (location->path + "/completed.data"); + location->undo = openAndLock (location->path + "/undo.data"); + } + + mAllOpenAndLocked = true; + context.hooks.trigger ("post-file-lock"); + } } //////////////////////////////////////////////////////////////////////////////// void TDB::unlock () { - if (mAllOpenAndLocked) + mPending.clear (); + mNew.clear (); + mModified.clear (); + + foreach (location, mLocations) { - mPending.clear (); - mNew.clear (); - mModified.clear (); + fflush (location->pending); + fclose (location->pending); + location->pending = NULL; - foreach (location, mLocations) - { - fflush (location->pending); - fclose (location->pending); - location->pending = NULL; + fflush (location->completed); + fclose (location->completed); + location->completed = NULL; - fflush (location->completed); - fclose (location->completed); - location->completed = NULL; - - fflush (location->undo); - fclose (location->undo); - location->completed = NULL; - } - - mAllOpenAndLocked = false; + fflush (location->undo); + fclose (location->undo); + location->completed = NULL; } + + mAllOpenAndLocked = false; } //////////////////////////////////////////////////////////////////////////////// @@ -343,6 +344,7 @@ void TDB::update (const Task& task) int TDB::commit () { Timer t ("TDB::commit"); + context.hooks.trigger ("pre-gc"); int quantity = mNew.size () + mModified.size (); @@ -362,6 +364,7 @@ int TDB::commit () writeUndo (*task, mLocations[0].undo); mNew.clear (); + context.hooks.trigger ("post-gc"); return quantity; } @@ -408,6 +411,7 @@ int TDB::commit () mNew.clear (); } + context.hooks.trigger ("post-gc"); return quantity; } @@ -506,6 +510,7 @@ int TDB::nextId () //////////////////////////////////////////////////////////////////////////////// void TDB::undo () { + context.hooks.trigger ("pre-undo"); Directory location (context.config.get ("data.location")); std::string undoFile = location.data + "/undo.data"; @@ -670,6 +675,7 @@ void TDB::undo () // Rewrite files. File::write (pendingFile, p); File::write (undoFile, u); + context.hooks.trigger ("post-undo"); return; } } @@ -708,6 +714,7 @@ void TDB::undo () } std::cout << "Undo complete." << std::endl; + context.hooks.trigger ("post-undo"); return; } } diff --git a/src/Task.cpp b/src/Task.cpp index 27f6258fd..15d51a864 100644 --- a/src/Task.cpp +++ b/src/Task.cpp @@ -27,6 +27,7 @@ #include #include +#include "Context.h" #include "Nibbler.h" #include "Date.h" #include "Duration.h" @@ -34,6 +35,8 @@ #include "text.h" #include "util.h" +extern Context context; + //////////////////////////////////////////////////////////////////////////////// Task::Task () : id (0) diff --git a/src/command.cpp b/src/command.cpp index 8e58f3074..6103baea1 100644 --- a/src/command.cpp +++ b/src/command.cpp @@ -52,66 +52,71 @@ extern Context context; //////////////////////////////////////////////////////////////////////////////// int handleAdd (std::string &outs) { - std::stringstream out; - - context.task.set ("uuid", uuid ()); - context.task.setEntry (); - - // Recurring tasks get a special status. - if (context.task.has ("due") && - context.task.has ("recur")) + if (context.hooks.trigger ("pre-add-command")) { - context.task.setStatus (Task::recurring); - context.task.set ("mask", ""); - } - else if (context.task.has ("wait")) - context.task.setStatus (Task::waiting); - else - context.task.setStatus (Task::pending); + std::stringstream out; - // Override with default.project, if not specified. - if (context.task.get ("project") == "") - context.task.set ("project", context.config.get ("default.project")); + context.task.set ("uuid", uuid ()); + context.task.setEntry (); - // Override with default.priority, if not specified. - if (context.task.get ("priority") == "") - { - std::string defaultPriority = context.config.get ("default.priority"); - if (Att::validNameValue ("priority", "", defaultPriority)) - context.task.set ("priority", defaultPriority); - } + // Recurring tasks get a special status. + if (context.task.has ("due") && + context.task.has ("recur")) + { + context.task.setStatus (Task::recurring); + context.task.set ("mask", ""); + } + else if (context.task.has ("wait")) + context.task.setStatus (Task::waiting); + else + context.task.setStatus (Task::pending); - // Include tags. - foreach (tag, context.tagAdditions) - context.task.addTag (*tag); + // Override with default.project, if not specified. + if (context.task.get ("project") == "") + context.task.set ("project", context.config.get ("default.project")); - // Perform some logical consistency checks. - if (context.task.has ("recur") && - !context.task.has ("due")) - throw std::string ("You cannot specify a recurring task without a due date."); + // Override with default.priority, if not specified. + if (context.task.get ("priority") == "") + { + std::string defaultPriority = context.config.get ("default.priority"); + if (Att::validNameValue ("priority", "", defaultPriority)) + context.task.set ("priority", defaultPriority); + } - if (context.task.has ("until") && - !context.task.has ("recur")) - throw std::string ("You cannot specify an until date for a non-recurring task."); + // Include tags. + foreach (tag, context.tagAdditions) + context.task.addTag (*tag); - // Only valid tasks can be added. - context.task.validate (); + // Perform some logical consistency checks. + if (context.task.has ("recur") && + !context.task.has ("due")) + throw std::string ("You cannot specify a recurring task without a due date."); - context.tdb.lock (context.config.getBoolean ("locking")); - context.tdb.add (context.task); + if (context.task.has ("until") && + !context.task.has ("recur")) + throw std::string ("You cannot specify an until date for a non-recurring task."); + + // Only valid tasks can be added. + context.task.validate (); + + context.tdb.lock (context.config.getBoolean ("locking")); + context.tdb.add (context.task); #ifdef FEATURE_NEW_ID - // All this, just for an id number. - std::vector all; - Filter none; - context.tdb.loadPending (all, none); - out << "Created task " << context.tdb.nextId () << std::endl; + // All this, just for an id number. + std::vector all; + Filter none; + context.tdb.loadPending (all, none); + out << "Created task " << context.tdb.nextId () << std::endl; #endif - context.tdb.commit (); - context.tdb.unlock (); + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-add-command"); + } - outs = out.str (); return 0; } @@ -490,8 +495,12 @@ int handleVersion (std::string &outs) #endif << std::endl - << "Copyright (C) 2006 - 2010, P. Beckingham, F. Hernandez." + << "Copyright (C) 2006 - 2010 P. Beckingham, F. Hernandez." << std::endl +#ifdef HAVE_LIBLUA + << "Portions of this software Copyright (C) 1994 – 2008 Lua.org, PUC-Rio." + << std::endl +#endif << disclaimer.render () << link.render () << std::endl; @@ -711,6 +720,7 @@ int handleConfig (std::string &outs) out << context.config.checkForDeprecatedColor (); // TODO Check for referenced but missing theme files. // TODO Check for referenced but missing string files. + // TODO Check for referenced but missing hook scripts. // Check for bad values in rc.annotations. std::string annotations = context.config.get ("annotations"); @@ -784,104 +794,116 @@ int handleConfig (std::string &outs) int handleDelete (std::string &outs) { int rc = 0; - std::stringstream out; - context.disallowModification (); - - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - Filter filter; - context.tdb.loadPending (tasks, filter); - - // Filter sequence. - std::vector all = tasks; - context.filter.applySequence (tasks, context.sequence); - - // Determine the end date. - char endTime[16]; - sprintf (endTime, "%u", (unsigned int) time (NULL)); - - foreach (task, tasks) + if (context.hooks.trigger ("pre-delete-command")) { - std::stringstream question; - question << "Permanently delete task " - << task->id - << " '" - << task->get ("description") - << "'?"; + std::stringstream out; - if (!context.config.getBoolean ("confirmation") || confirm (question.str ())) + context.disallowModification (); + + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + Filter filter; + context.tdb.loadPending (tasks, filter); + + // Filter sequence. + std::vector all = tasks; + context.filter.applySequence (tasks, context.sequence); + + // Determine the end date. + char endTime[16]; + sprintf (endTime, "%u", (unsigned int) time (NULL)); + + foreach (task, tasks) { - // Check for the more complex case of a recurring task. If this is a - // recurring task, get confirmation to delete them all. - std::string parent = task->get ("parent"); - if (parent != "") + context.hooks.setTaskId (task->id); + if (context.hooks.trigger ("pre-delete")) { - if (confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) - { - // Scan all pending tasks for siblings of this task, and the parent - // itself, and delete them. - foreach (sibling, all) - { - if (sibling->get ("parent") == parent || - sibling->get ("uuid") == parent) - { - sibling->setStatus (Task::deleted); - sibling->set ("end", endTime); - context.tdb.update (*sibling); + std::stringstream question; + question << "Permanently delete task " + << task->id + << " '" + << task->get ("description") + << "'?"; - if (context.config.getBoolean ("echo.command")) - out << "Deleting recurring task " - << sibling->id - << " '" - << sibling->get ("description") - << "'" - << std::endl; + if (!context.config.getBoolean ("confirmation") || confirm (question.str ())) + { + // Check for the more complex case of a recurring task. If this is a + // recurring task, get confirmation to delete them all. + std::string parent = task->get ("parent"); + if (parent != "") + { + if (confirm ("This is a recurring task. Do you want to delete all pending recurrences of this same task?")) + { + // Scan all pending tasks for siblings of this task, and the parent + // itself, and delete them. + foreach (sibling, all) + { + if (sibling->get ("parent") == parent || + sibling->get ("uuid") == parent) + { + sibling->setStatus (Task::deleted); + sibling->set ("end", endTime); + context.tdb.update (*sibling); + + if (context.config.getBoolean ("echo.command")) + out << "Deleting recurring task " + << sibling->id + << " '" + << sibling->get ("description") + << "'" + << std::endl; + } + } + } + else + { + // Update mask in parent. + task->setStatus (Task::deleted); + updateRecurrenceMask (all, *task); + + task->set ("end", endTime); + context.tdb.update (*task); + + out << "Deleting recurring task " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; } } + else + { + task->setStatus (Task::deleted); + task->set ("end", endTime); + context.tdb.update (*task); + + if (context.config.getBoolean ("echo.command")) + out << "Deleting task " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; + } } - else - { - // Update mask in parent. - task->setStatus (Task::deleted); - updateRecurrenceMask (all, *task); - - task->set ("end", endTime); - context.tdb.update (*task); - - out << "Deleting recurring task " - << task->id - << " '" - << task->get ("description") - << "'" - << std::endl; + else { + out << "Task not deleted." << std::endl; + rc = 1; } - } - else - { - task->setStatus (Task::deleted); - task->set ("end", endTime); - context.tdb.update (*task); - if (context.config.getBoolean ("echo.command")) - out << "Deleting task " - << task->id - << " '" - << task->get ("description") - << "'" - << std::endl; + context.hooks.trigger ("post-delete"); } } - else { - out << "Task not deleted." << std::endl; - rc = 1; - } + + context.tdb.commit (); + context.tdb.unlock (); + + outs = out.str (); + context.hooks.trigger ("post-delete-command"); } - context.tdb.commit (); - context.tdb.unlock (); - - outs = out.str (); return rc; } @@ -1036,20 +1058,28 @@ int handleDone (std::string &outs) if (taskDiff (before, *task)) { - if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + context.hooks.setTaskId (task->id); + if (context.hooks.trigger ("pre-completed")) { - context.tdb.update (*task); + if (permission.confirmed (before, taskDifferences (before, *task) + "Proceed with change?")) + { + context.tdb.update (*task); - if (context.config.getBoolean ("echo.command")) - out << "Completed " - << task->id - << " '" - << task->get ("description") - << "'" - << std::endl; + if (context.config.getBoolean ("echo.command")) + out << "Completed " + << task->id + << " '" + << task->get ("description") + << "'" + << std::endl; - ++count; + ++count; + } + + context.hooks.trigger ("post-completed"); } + else + continue; } updateRecurrenceMask (all, *task); @@ -1066,7 +1096,9 @@ int handleDone (std::string &outs) rc = 1; } - context.tdb.commit (); + if (count) + context.tdb.commit (); + context.tdb.unlock (); if (context.config.getBoolean ("echo.command")) @@ -1744,20 +1776,29 @@ int deltaDescription (Task& task) int deltaTags (Task& task) { int changes = 0; + context.hooks.setTaskId (task.id); // Apply or remove tags, if any. std::vector tags; context.task.getTags (tags); foreach (tag, tags) { - task.addTag (*tag); - ++changes; + if (context.hooks.trigger ("pre-tag")) + { + task.addTag (*tag); + ++changes; + context.hooks.trigger ("post-tag"); + } } foreach (tag, context.tagRemovals) { - task.removeTag (*tag); - ++changes; + if (context.hooks.trigger ("pre-detag")) + { + task.removeTag (*tag); + ++changes; + context.hooks.trigger ("post-detag"); + } } return changes; diff --git a/src/custom.cpp b/src/custom.cpp index 5303ef088..7614c0c93 100644 --- a/src/custom.cpp +++ b/src/custom.cpp @@ -586,39 +586,13 @@ int runCustomReport ( } // Now auto colorize all rows. - std::string due; - Color color_due (context.config.get ("color.due")); - Color color_overdue (context.config.get ("color.overdue")); - - bool imminent; - bool overdue; - for (unsigned int row = 0; row < tasks.size (); ++row) + if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) { - imminent = false; - overdue = false; - due = tasks[row].get ("due"); - if (due.length ()) - { - switch (getDueState (due)) - { - case 2: overdue = true; break; - case 1: imminent = true; break; - case 0: - default: break; - } - } - - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + for (unsigned int row = 0; row < tasks.size (); ++row) { Color c (tasks[row].get ("fg") + " " + tasks[row].get ("bg")); autoColorize (tasks[row], c); table.setRowColor (row, c); - - if (dueColumn != -1) - { - c.blend (overdue ? color_overdue : color_due); - table.setCellColor (row, columnCount, c); - } } } diff --git a/src/report.cpp b/src/report.cpp index b47fc4e43..938dde6d1 100644 --- a/src/report.cpp +++ b/src/report.cpp @@ -300,231 +300,219 @@ int longUsage (std::string &outs) int handleInfo (std::string &outs) { int rc = 0; - // Get all the tasks. - std::vector tasks; - context.tdb.lock (context.config.getBoolean ("locking")); - handleRecurrence (); - context.tdb.loadPending (tasks, context.filter); - context.tdb.commit (); - context.tdb.unlock (); - // Filter sequence. - context.filter.applySequence (tasks, context.sequence); - - // Find the task. - std::stringstream out; - foreach (task, tasks) + if (context.hooks.trigger ("pre-info-command")) { - Table table; - table.setTableWidth (context.getWidth ()); - table.setDateFormat (context.config.get ("dateformat")); + // Get all the tasks. + std::vector tasks; + context.tdb.lock (context.config.getBoolean ("locking")); + handleRecurrence (); + context.tdb.loadPending (tasks, context.filter); + context.tdb.commit (); + context.tdb.unlock (); - table.addColumn ("Name"); - table.addColumn ("Value"); + // Filter sequence. + context.filter.applySequence (tasks, context.sequence); - if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && - context.config.getBoolean ("fontunderline")) + // Find the task. + std::stringstream out; + foreach (task, tasks) { - table.setColumnUnderline (0); - table.setColumnUnderline (1); - } - else - table.setTableDashedUnderline (); + Table table; + table.setTableWidth (context.getWidth ()); + table.setDateFormat (context.config.get ("dateformat")); - table.setColumnWidth (0, Table::minimum); - table.setColumnWidth (1, Table::flexible); + table.addColumn ("Name"); + table.addColumn ("Value"); - table.setColumnJustification (0, Table::left); - table.setColumnJustification (1, Table::left); - Date now; + if ((context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) && + context.config.getBoolean ("fontunderline")) + { + table.setColumnUnderline (0); + table.setColumnUnderline (1); + } + else + table.setTableDashedUnderline (); - int row = table.addRow (); - table.addCell (row, 0, "ID"); - table.addCell (row, 1, task->id); + table.setColumnWidth (0, Table::minimum); + table.setColumnWidth (1, Table::flexible); - std::string status = ucFirst (Task::statusToText (task->getStatus ())); + table.setColumnJustification (0, Table::left); + table.setColumnJustification (1, Table::left); + Date now; - if (task->has ("parent")) - status += " (Recurring)"; + int row = table.addRow (); + table.addCell (row, 0, "ID"); + table.addCell (row, 1, task->id); - row = table.addRow (); - table.addCell (row, 0, "Status"); - table.addCell (row, 1, status); + std::string status = ucFirst (Task::statusToText (task->getStatus ())); - row = table.addRow (); - table.addCell (row, 0, "Description"); - table.addCell (row, 1, getFullDescription (*task, "info")); - - if (task->has ("project")) - { row = table.addRow (); - table.addCell (row, 0, "Project"); - table.addCell (row, 1, task->get ("project")); - } + table.addCell (row, 0, "Description"); + table.addCell (row, 1, getFullDescription (*task, "info")); - if (task->has ("priority")) - { row = table.addRow (); - table.addCell (row, 0, "Priority"); - table.addCell (row, 1, task->get ("priority")); - } + table.addCell (row, 0, "Status"); + table.addCell (row, 1, status); - if (task->getStatus () == Task::recurring || - task->has ("parent")) - { - if (task->has ("recur")) + if (task->has ("project")) { row = table.addRow (); - table.addCell (row, 0, "Recurrence"); - table.addCell (row, 1, task->get ("recur")); + table.addCell (row, 0, "Project"); + table.addCell (row, 1, task->get ("project")); } - if (task->has ("until")) + if (task->has ("priority")) { row = table.addRow (); - table.addCell (row, 0, "Recur until"); - table.addCell (row, 1, task->get ("until")); + table.addCell (row, 0, "Priority"); + table.addCell (row, 1, task->get ("priority")); } - if (task->has ("mask")) + if (task->getStatus () == Task::recurring || + task->has ("parent")) + { + if (task->has ("recur")) + { + row = table.addRow (); + table.addCell (row, 0, "Recurrence"); + table.addCell (row, 1, task->get ("recur")); + } + + if (task->has ("until")) + { + row = table.addRow (); + table.addCell (row, 0, "Recur until"); + table.addCell (row, 1, task->get ("until")); + } + + if (task->has ("mask")) + { + row = table.addRow (); + table.addCell (row, 0, "Mask"); + table.addCell (row, 1, task->get ("mask")); + } + + if (task->has ("parent")) + { + row = table.addRow (); + table.addCell (row, 0, "Parent task"); + table.addCell (row, 1, task->get ("parent")); + } + + row = table.addRow (); + table.addCell (row, 0, "Mask Index"); + table.addCell (row, 1, task->get ("imask")); + } + + // due (colored) + if (task->has ("due")) { row = table.addRow (); - table.addCell (row, 0, "Mask"); - table.addCell (row, 1, task->get ("mask")); + table.addCell (row, 0, "Due"); + + Date dt (atoi (task->get ("due").c_str ())); + std::string format = context.config.get ("reportdateformat"); + if (format == "") + format = context.config.get ("dateformat"); + + std::string due = getDueDate (*task, format); + table.addCell (row, 1, due); } - if (task->has ("parent")) + // wait + if (task->has ("wait")) { row = table.addRow (); - table.addCell (row, 0, "Parent task"); - table.addCell (row, 1, task->get ("parent")); + table.addCell (row, 0, "Waiting until"); + Date dt (atoi (task->get ("wait").c_str ())); + table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); } - row = table.addRow (); - table.addCell (row, 0, "Mask Index"); - table.addCell (row, 1, task->get ("imask")); - } - - // due (colored) - bool imminent = false; - bool overdue = false; - if (task->has ("due")) - { - row = table.addRow (); - table.addCell (row, 0, "Due"); - - Date dt (atoi (task->get ("due").c_str ())); - std::string format = context.config.get ("dateformat.report"); - if (format == "") - format = context.config.get ("dateformat"); - - std::string due = getDueDate (*task, format); - table.addCell (row, 1, due); - - overdue = (dt < now) ? true : false; - int imminentperiod = context.config.getInteger ("due"); - Date imminentDay = now + imminentperiod * 86400; - imminent = dt < imminentDay ? true : false; - - if (context.config.getBoolean ("color") || context.config.getBoolean ("_forcecolor")) + // start + if (task->has ("start")) { - if (overdue) - table.setCellColor (row, 1, Color (context.config.get ("color.overdue"))); - else if (imminent) - table.setCellColor (row, 1, Color (context.config.get ("color.due"))); + row = table.addRow (); + table.addCell (row, 0, "Start"); + Date dt (atoi (task->get ("start").c_str ())); + table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); } - } - // wait - if (task->has ("wait")) - { + // end + if (task->has ("end")) + { + row = table.addRow (); + table.addCell (row, 0, "End"); + Date dt (atoi (task->get ("end").c_str ())); + table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); + } + + // tags ... + std::vector tags; + task->getTags (tags); + if (tags.size ()) + { + std::string allTags; + join (allTags, " ", tags); + + row = table.addRow (); + table.addCell (row, 0, "Tags"); + table.addCell (row, 1, allTags); + } + + // uuid row = table.addRow (); - table.addCell (row, 0, "Waiting until"); - Date dt (atoi (task->get ("wait").c_str ())); - table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); - } + table.addCell (row, 0, "UUID"); + table.addCell (row, 1, task->get ("uuid")); - // start - if (task->has ("start")) - { + // entry row = table.addRow (); - table.addCell (row, 0, "Start"); - Date dt (atoi (task->get ("start").c_str ())); - table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); + table.addCell (row, 0, "Entered"); + Date dt (atoi (task->get ("entry").c_str ())); + std::string entry = dt.toString (context.config.get ("dateformat")); + + std::string age; + std::string created = task->get ("entry"); + if (created.length ()) + { + Date dt (atoi (created.c_str ())); + age = formatSeconds ((time_t) (now - dt)); + } + + table.addCell (row, 1, entry + " (" + age + ")"); + + // fg + std::string color = task->get ("fg"); + if (color != "") + { + row = table.addRow (); + table.addCell (row, 0, "Foreground color"); + table.addCell (row, 1, color); + } + + // bg + color = task->get ("bg"); + if (color != "") + { + row = table.addRow (); + table.addCell (row, 0, "Background color"); + table.addCell (row, 1, color); + } + + out << optionalBlankLine () + << table.render () + << std::endl; } - // end - if (task->has ("end")) - { - row = table.addRow (); - table.addCell (row, 0, "End"); - Date dt (atoi (task->get ("end").c_str ())); - table.addCell (row, 1, dt.toString (context.config.get ("dateformat"))); + if (! tasks.size ()) { + out << "No matches." << std::endl; + rc = 1; } - // tags ... - std::vector tags; - task->getTags (tags); - if (tags.size ()) - { - std::string allTags; - join (allTags, " ", tags); - - row = table.addRow (); - table.addCell (row, 0, "Tags"); - table.addCell (row, 1, allTags); - } - - // uuid - row = table.addRow (); - table.addCell (row, 0, "UUID"); - table.addCell (row, 1, task->get ("uuid")); - - // entry - row = table.addRow (); - table.addCell (row, 0, "Entered"); - Date dt (atoi (task->get ("entry").c_str ())); - std::string entry = dt.toString (context.config.get ("dateformat")); - - std::string age; - std::string created = task->get ("entry"); - if (created.length ()) - { - Date dt (atoi (created.c_str ())); - age = formatSeconds ((time_t) (now - dt)); - } - - table.addCell (row, 1, entry + " (" + age + ")"); - - // fg - std::string color = task->get ("fg"); - if (color != "") - { - row = table.addRow (); - table.addCell (row, 0, "Foreground color"); - table.addCell (row, 1, color); - } - - // bg - color = task->get ("bg"); - if (color != "") - { - row = table.addRow (); - table.addCell (row, 0, "Background color"); - table.addCell (row, 1, color); - } - - out << optionalBlankLine () - << table.render () - << std::endl; + outs = out.str (); + context.hooks.trigger ("post-info-command"); } - if (! tasks.size ()) { - out << "No matches." << std::endl; - rc = 1; - } - - outs = out.str (); return rc; } diff --git a/src/tests/Makefile b/src/tests/Makefile index 2461b7113..edf1ba103 100644 --- a/src/tests/Makefile +++ b/src/tests/Makefile @@ -1,15 +1,16 @@ PROJECT = t.t tdb.t date.t duration.t t.benchmark.t text.t autocomplete.t \ config.t seq.t att.t stringtable.t record.t nibbler.t subst.t filt.t \ cmd.t util.t color.t list.t path.t file.t directory.t -CFLAGS = -I. -I.. -Wall -pedantic -ggdb3 -fno-rtti +CFLAGS = -I. -I.. -I../.. -Wall -pedantic -ggdb3 -fno-rtti LFLAGS = -L/usr/local/lib -lncurses -OBJECTS = ../TDB.o ../Task.o ../text.o ../Date.o ../Table.o ../Duration.o \ - ../util.o ../Config.o ../Sequence.o ../Att.o ../Cmd.o ../Record.o \ - ../StringTable.o ../Subst.o ../Nibbler.o ../Location.o ../Filter.o \ - ../Context.o ../Keymap.o ../command.o ../interactive.o ../report.o \ - ../Grid.o ../Color.o ../rules.o ../recur.o ../custom.o ../import.o \ - ../edit.o ../Timer.o ../Permission.o ../Path.o ../File.o \ - ../Directory.o +OBJECTS = ../t-TDB.o ../t-Task.o ../t-text.o ../t-Date.o ../t-Table.o \ + ../t-Duration.o ../t-util.o ../t-Config.o ../t-Sequence.o ../t-Att.o \ + ../t-Cmd.o ../t-Record.o ../t-StringTable.o ../t-Subst.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 all: $(PROJECT) diff --git a/src/tests/hook.post-start.t b/src/tests/hook.post-start.t new file mode 100755 index 000000000..a9e83f0ee --- /dev/null +++ b/src/tests/hook.post-start.t @@ -0,0 +1,67 @@ +#! /usr/bin/perl +################################################################################ +## task - a command line task list manager. +## +## Copyright 2006 - 2010, Paul Beckingham. +## 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 +## +################################################################################ + +use strict; +use warnings; +use Test::More tests => 7; + +# Create the rc file. +if (open my $fh, '>', 'hook.rc') +{ + print $fh "data.location=.\n", + "hook.post-start=" . $ENV{'PWD'} . "/hook:test\n"; + close $fh; + ok (-r 'hook.rc', 'Created hook.rc'); +} + +if (open my $fh, '>', 'hook') +{ + print $fh "function test () print ('marker') return 0, nil end\n"; + close $fh; + ok (-r 'hook', 'Created hook'); +} + +# Test the hook. +my $output = qx{../task rc:hook.rc _version}; +like ($output, qr/^marker.+\n\d\.\d+\.\d+\n$/ms, 'Found marker before output'); + +# Cleanup. +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +unlink 'undo.data'; +ok (!-r 'undo.data', 'Removed undo.data'); + +unlink 'hook'; +ok (!-r 'hook', 'Removed hook'); + +unlink 'hook.rc'; +ok (!-r 'hook.rc', 'Removed hook.rc'); + +exit 0; + diff --git a/src/tests/hook.pre-exit.t b/src/tests/hook.pre-exit.t new file mode 100755 index 000000000..9093a9719 --- /dev/null +++ b/src/tests/hook.pre-exit.t @@ -0,0 +1,67 @@ +#! /usr/bin/perl +################################################################################ +## task - a command line task list manager. +## +## Copyright 2006 - 2010, Paul Beckingham. +## 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 +## +################################################################################ + +use strict; +use warnings; +use Test::More tests => 7; + +# Create the rc file. +if (open my $fh, '>', 'hook.rc') +{ + print $fh "data.location=.\n", + "hook.pre-exit=" . $ENV{'PWD'} . "/hook:test\n"; + close $fh; + ok (-r 'hook.rc', 'Created hook.rc'); +} + +if (open my $fh, '>', 'hook') +{ + print $fh "function test () print ('marker') return 0, nil end\n"; + close $fh; + ok (-r 'hook', 'Created hook'); +} + +# Test the hook. +my $output = qx{../task rc:hook.rc _version}; +like ($output, qr/\n\d\.\d+\.\d+\nmarker\n$/ms, 'Found marker after output'); + +# Cleanup. +unlink 'pending.data'; +ok (!-r 'pending.data', 'Removed pending.data'); + +unlink 'undo.data'; +ok (!-r 'undo.data', 'Removed undo.data'); + +unlink 'hook'; +ok (!-r 'hook', 'Removed hook'); + +unlink 'hook.rc'; +ok (!-r 'hook.rc', 'Removed hook.rc'); + +exit 0; + diff --git a/with_lua b/with_lua new file mode 100755 index 000000000..5b24f5e97 --- /dev/null +++ b/with_lua @@ -0,0 +1,6 @@ +# This is currently what is required to build with Lua. + +#./configure --enable-lua=yes --with-lua --with-lua-inc=/usr/local/include --with-lua-lib=/usr/local/lib +#./configure --enable-lua --with-lua --with-lua-inc=/usr/local/include --with-lua-lib=/usr/local/lib + ./configure --enable-lua --with-lua-inc=/usr/local/include --with-lua-lib=/usr/local/lib +