taskwarrior/src/Context.cpp
2018-05-13 23:00:00 -04:00

1294 lines
47 KiB
C++
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

////////////////////////////////////////////////////////////////////////////////
//
// Copyright 2006 - 2017, Paul Beckingham, Federico Hernandez.
//
// 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.
//
// http://www.opensource.org/licenses/mit-license.php
//
////////////////////////////////////////////////////////////////////////////////
#include <cmake.h>
#include <Context.h>
#include <iostream>
#include <fstream>
#include <sstream>
#include <iomanip>
#include <algorithm>
#include <assert.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <FS.h>
#include <Eval.h>
#include <Variant.h>
#include <Datetime.h>
#include <shared.h>
#include <format.h>
#include <main.h>
#include <i18n.h>
#ifdef HAVE_COMMIT
#include <commit.h>
#endif
#include <stdio.h>
#include <sys/ioctl.h>
#ifdef SOLARIS
#include <sys/termios.h>
#endif
////////////////////////////////////////////////////////////////////////////////
// This string is parsed and used as default values for configuration.
std::string configurationDefaults =
"# Taskwarrior program configuration file.\n"
"# For more documentation, see http://taskwarrior.org or try 'man task', 'man task-color',\n"
"# 'man task-sync' or 'man taskrc'\n"
"\n"
"# Here is an example of entries that use the default, override and blank values\n"
"# variable=foo -- By specifying a value, this overrides the default\n"
"# variable= -- By specifying no value, this means no default\n"
"# #variable=foo -- By commenting out the line, or deleting it, this uses the default\n"
"\n"
"# Use the command 'task show' to see all defaults and overrides\n"
"\n"
"# Files\n"
"data.location=~/.task\n"
"locking=1 # Use file-level locking\n"
"gc=1 # Garbage-collect data files - DO NOT CHANGE unless you are sure\n"
"exit.on.missing.db=0 # Whether to exit if ~/.task is not found\n"
"hooks=1 # Master control switch for hooks\n"
"\n"
"# Terminal\n"
"detection=1 # Detects terminal width\n"
"defaultwidth=80 # Without detection, assumed width\n"
"defaultheight=24 # Without detection, assumed height\n"
"avoidlastcolumn=0 # Fixes Cygwin width problem\n"
"hyphenate=1 # Hyphenates lines wrapped on non-word-breaks\n"
"#editor=vi # Preferred text editor\n"
"reserved.lines=1 # Assume a 1-line prompt\n"
"\n"
"# Miscellaneous\n"
"# # Comma-separated list. May contain any subset of:\n"
"verbose=blank,header,footnote,label,new-id,new-uuid,affected,edit,special,project,sync,unwait,recur\n"
"confirmation=1 # Confirmation on delete, big changes\n"
"recurrence=1 # Enable recurrence\n"
"recurrence.confirmation=prompt # Confirmation for propagating changes among recurring tasks (yes/no/prompt)\n"
"allow.empty.filter=1 # An empty filter gets a warning and requires confirmation\n"
"indent.annotation=2 # Indent spaces for annotations\n"
"indent.report=0 # Indent spaces for whole report\n"
"row.padding=0 # Left and right padding for each row of report\n"
"column.padding=1 # Spaces between each column in a report\n"
"bulk=3 # 3 or more tasks considered a bulk change and is confirmed\n"
"nag=You have more urgent tasks. # Nag message to keep you honest\n" // TODO
"search.case.sensitive=1 # Setting to no allows case insensitive searches\n"
"active.indicator=* # What to show as an active task indicator\n"
"tag.indicator=+ # What to show as a tag indicator\n"
"dependency.indicator=D # What to show as a dependency indicator\n"
"recurrence.indicator=R # What to show as a task recurrence indicator\n"
"recurrence.limit=1 # Number of future recurring pending tasks\n"
"undo.style=side # Undo style - can be 'side', or 'diff'\n"
"regex=1 # Assume all search/filter strings are regexes\n"
"xterm.title=0 # Sets xterm title for some commands\n"
"expressions=infix # Prefer infix over postfix expressions\n"
"json.array=1 # Enclose JSON output in [ ]\n"
"json.depends.array=0 # Encode dependencies as a JSON array\n"
"abbreviation.minimum=2 # Shortest allowed abbreviation\n"
"\n"
"# Dates\n"
"dateformat=Y-M-D # Preferred input and display date format\n"
"dateformat.holiday=YMD # Preferred input date format for holidays\n"
"dateformat.edit=Y-M-D H:N:S # Preferred display date format when editing\n"
"dateformat.info=Y-M-D H:N:S # Preferred display date format for information\n"
"dateformat.report= # Preferred display date format for reports\n"
"dateformat.annotation= # Preferred display date format for annotations\n"
"date.iso=1 # Enable ISO date support\n"
"weekstart="
STRING_DATE_SUNDAY
" # Sunday or Monday only\n"
"displayweeknumber=1 # Show week numbers on calendar\n"
"due=7 # Task is considered due in 7 days\n"
"\n"
"# Calendar controls\n"
"calendar.legend=1 # Display the legend on calendar\n"
"calendar.details=sparse # Calendar shows information for tasks w/due dates: full, sparse or none\n"
"calendar.details.report=list # Report to use when showing task information in cal\n"
"calendar.offset=0 # Apply an offset value to control the first month of the calendar\n"
"calendar.offset.value=-1 # The number of months the first month of the calendar is moved\n"
"calendar.holidays=none # Show public holidays on calendar:full, sparse or none\n"
"#monthsperline=3 # Number of calendar months on a line\n"
"\n"
"# Journal controls\n"
"journal.time=0 # Record start/stop commands as annotation\n"
"journal.time.start.annotation=Started task # Annotation description for the start journal entry\n"
"journal.time.stop.annotation=Stopped task # Annotation description for the stop journal entry\n"
"journal.info=1 # Display task journal with info command\n"
"\n"
"# Dependency controls\n"
"dependency.reminder=1 # Nags on dependency chain violations\n"
"dependency.confirmation=1 # Should dependency chain repair be confirmed?\n"
"\n"
"# Urgency Coefficients\n"
"urgency.user.tag.next.coefficient=15.0 # Urgency coefficient for 'next' special tag\n"
"urgency.due.coefficient=12.0 # Urgency coefficient for due dates\n"
"urgency.blocking.coefficient=8.0 # Urgency coefficient for blocking tasks\n"
"urgency.active.coefficient=4.0 # Urgency coefficient for active tasks\n"
"urgency.scheduled.coefficient=5.0 # Urgency coefficient for scheduled tasks\n"
"urgency.age.coefficient=2.0 # Urgency coefficient for age\n"
"urgency.annotations.coefficient=1.0 # Urgency coefficient for annotations\n"
"urgency.tags.coefficient=1.0 # Urgency coefficient for tags\n"
"urgency.project.coefficient=1.0 # Urgency coefficient for projects\n"
"urgency.blocked.coefficient=-5.0 # Urgency coefficient for blocked tasks\n"
"urgency.waiting.coefficient=-3.0 # Urgency coefficient for waiting status\n"
"urgency.inherit=0 # Recursively inherit highest urgency value from blocked tasks\n"
"urgency.age.max=365 # Maximum age in days\n"
"\n"
"#urgency.user.project.foo.coefficient=5.0 # Urgency coefficients for 'foo' project\n"
"#urgency.user.tag.foo.coefficient=5.0 # Urgency coefficients for 'foo' tag\n"
"#urgency.uda.foo.coefficient=5.0 # Urgency coefficients for UDA 'foo'\n"
"\n"
"# Color controls.\n"
"color=1 # Enable color\n"
"\n"
"rule.precedence.color=deleted,completed,active,keyword.,tag.,project.,overdue,scheduled,due.today,due,blocked,blocking,recurring,tagged,uda.\n"
"\n"
"# General decoration\n"
"rule.color.merge=1\n"
"color.label=\n"
"color.label.sort=\n"
"color.alternate=on gray2\n"
"color.header=color3\n"
"color.footnote=color3\n"
"color.warning=bold red\n"
"color.error=white on red\n"
"color.debug=color4\n"
"\n"
"# Task state\n"
"color.completed=\n"
"color.deleted=\n"
"color.active=rgb555 on rgb410\n"
"color.recurring=rgb013\n"
"color.scheduled=on rgb001\n"
"color.until=\n"
"color.blocked=white on color8\n"
"color.blocking=black on color15\n"
"\n"
"# Project\n"
"color.project.none=\n"
"\n"
"# Priority UDA\n"
"color.uda.priority.H=color255\n"
"color.uda.priority.L=color245\n"
"color.uda.priority.M=color250\n"
"\n"
"# Tags\n"
"color.tag.next=rgb440\n"
"color.tag.none=\n"
"color.tagged=rgb031\n"
"\n"
"# Due\n"
"color.due.today=rgb400\n"
"color.due=color1\n"
"color.overdue=color9\n"
"\n"
"# Report: burndown\n"
"color.burndown.done=on rgb010\n"
"color.burndown.pending=on color9\n"
"color.burndown.started=on color11\n"
"\n"
"# Report: history\n"
"color.history.add=color0 on rgb500\n"
"color.history.delete=color0 on rgb550\n"
"color.history.done=color0 on rgb050\n"
"\n"
"# Report: summary\n"
"color.summary.background=white on color0\n"
"color.summary.bar=black on rgb141\n"
"\n"
"# Command: calendar\n"
"color.calendar.due.today=color15 on color1\n"
"color.calendar.due=color0 on color1\n"
"color.calendar.holiday=color0 on color11\n"
"color.calendar.overdue=color0 on color9\n"
"color.calendar.today=color15 on rgb013\n"
"color.calendar.weekend=on color235\n"
"color.calendar.weeknumber=rgb013\n"
"\n"
"# Command: sync\n"
"color.sync.added=rgb010\n"
"color.sync.changed=color11\n"
"color.sync.rejected=color9\n"
"\n"
"# Command: undo\n"
"color.undo.after=color2\n"
"color.undo.before=color1\n"
"\n"
"# UDA priority\n"
"uda.priority.type=string # UDA priority is a string type\n"
"uda.priority.label=Priority # UDA priority has a display label'\n"
"uda.priority.values=H,M,L, # UDA priority values are 'H', 'M', 'L' or ''\n"
" # UDA priority sorting is 'H' > 'M' > 'L' > '' (highest to lowest)\n"
"#uda.priority.default=M # UDA priority default value of 'M'\n"
"urgency.uda.priority.H.coefficient=6.0 # UDA priority coefficient for value 'H'\n"
"urgency.uda.priority.M.coefficient=3.9 # UDA priority coefficient for value 'M'\n"
"urgency.uda.priority.L.coefficient=1.8 # UDA priority coefficient for value 'L'\n"
"\n"
"# Here is the rule precedence order, highest to lowest.\n"
"# Note that these are just the color rule names, without the leading 'color.'\n"
"# and any trailing '.value'.\n"
"rule.precedence.color=deleted,completed,active,keyword.,tag.,project.,overdue,scheduled,due.today,due,blocked,blocking,recurring,tagged,uda.\n"
"\n"
"#default.project=foo # Default project for 'add' command\n"
"#default.due=eom # Default due date for 'add' command\n"
"#default.scheduled=eom # Default scheduled date for 'add' command\n"
"default.command=next # When no arguments are specified\n"
"default.timesheet.filter=( +PENDING and start.after:now-4wks ) or ( +COMPLETED and end.after:now-4wks )\n"
"\n"
"_forcecolor=0 # Forces color to be on, even for non TTY output\n"
"complete.all.tags=0 # Include old tag names in '_ags' command\n"
"list.all.projects=0 # Include old project names in 'projects' command\n"
"summary.all.projects=0 # Include old project names in 'summary' command\n"
"list.all.tags=0 # Include old tag names in 'tags' command\n"
"print.empty.columns=0 # Print columns which have no data for any task\n"
"debug=0 # Display diagnostics\n"
"debug.tls=0 # Sync diagnostics\n"
"sugar=1 # Syntactic sugar\n"
"obfuscate=0 # Obfuscate data for error reporting\n"
"fontunderline=1 # Uses underlines rather than -------\n"
"\n"
"# WARNING: Please read the documentation (man task-sync) before setting up\n"
"# Taskwarrior for Taskserver synchronization.\n"
"#taskd.ca=<certificate file>\n"
"#taskd.certificate=<certificate file>\n"
"#taskd.credentials=<organization>/<name>/<password>\n"
"#taskd.server=<server>:<port>\n"
"taskd.trust=strict\n"
"#taskd.trust=ignore hostname\n"
"#taskd.trust=allow all\n"
"taskd.ciphers=NORMAL\n"
"\n"
"# Aliases - alternate names for commands\n"
"alias.rm=delete # Alias for the delete command\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.burndown=burndown.weekly # Prefer the weekly burndown chart\n"
"alias.shell=exec tasksh # Alias old shell command to new shell\n"
"\n"
"# Reports\n"
"\n"
"report.long.description=All details of tasks\n"
"report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,Description\n"
"report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,wait.remaining,scheduled,due,until,description\n"
"report.long.filter=status:pending\n"
"report.long.sort=modified-\n"
"\n"
"report.list.description=Most details of tasks\n"
"report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n"
"report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n"
"report.list.filter=status:pending\n"
"report.list.sort=start-,due+,project+,urgency-\n"
"\n"
"report.ls.description=Few details of tasks\n"
"report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n"
"report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n"
"report.ls.filter=status:pending\n"
"report.ls.sort=start-,description+\n"
"\n"
"report.minimal.description=Minimal details of tasks\n"
"report.minimal.labels=ID,Project,Tags,Description\n"
"report.minimal.columns=id,project,tags.count,description.count\n"
"report.minimal.filter=status:pending or status:waiting\n"
"report.minimal.sort=project+/,description+\n"
"\n"
"report.newest.description=Newest tasks\n"
"report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.newest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.newest.filter=status:pending or status:waiting\n"
"report.newest.sort=entry-\n"
"\n"
"report.oldest.description=Oldest tasks\n"
"report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.oldest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
"report.oldest.filter=status:pending or status:waiting\n"
"report.oldest.sort=entry+\n"
"\n"
"report.overdue.description=Overdue tasks\n"
"report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n"
"report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,scheduled.countdown,due,until,description,urgency\n"
"report.overdue.filter=(status:pending or status:waiting) and +OVERDUE\n"
"report.overdue.sort=urgency-,due+\n"
"\n"
"report.active.description=Active tasks\n"
"report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,Description\n"
"report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,recur,wait,scheduled.remaining,due,until,description\n"
"report.active.filter=status:pending and +ACTIVE\n"
"report.active.sort=project+,start+\n"
"\n"
"report.completed.description=Completed tasks\n"
"report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n"
"report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,recur.indicator,due,description\n"
"report.completed.filter=status:completed\n"
"report.completed.sort=end+\n"
"\n"
"report.recurring.description=Recurring Tasks\n"
"report.recurring.labels=ID,Active,Age,D,P,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n"
"report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
"report.recurring.filter=(status:pending or status:waiting) and (+PARENT or +CHILD)\n"
"report.recurring.sort=due+,urgency-,entry+\n"
"\n"
"report.waiting.description=Waiting (hidden) tasks\n"
"report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n"
"report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,recur.indicator,wait,wait.remaining,scheduled,due,until,description\n"
"report.waiting.filter=+WAITING\n"
"report.waiting.sort=due+,wait+,entry+\n"
"\n"
"report.all.description=All tasks\n"
"report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
"report.all.columns=id,status.short,uuid.short,start.active,entry.age,end.age,depends.indicator,priority,project.parent,tags.count,recur.indicator,wait.remaining,scheduled.remaining,due,until.remaining,description\n"
"report.all.sort=entry-\n"
"\n"
"report.next.description=Most urgent tasks\n"
"report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n"
"report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency\n"
"report.next.filter=status:pending limit:page\n"
"report.next.sort=urgency-\n"
"\n"
"report.ready.description=Most urgent actionable tasks\n"
"report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n"
"report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n"
"report.ready.filter=+READY\n"
"report.ready.sort=start-,urgency-\n"
"\n"
"report.blocked.description=Blocked tasks\n"
"report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.blocked.sort=due+,priority-,start-,project+\n"
"report.blocked.filter=status:pending +BLOCKED\n"
"\n"
"report.unblocked.description=Unblocked tasks\n"
"report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
"report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
"report.unblocked.sort=due+,priority-,start-,project+\n"
"report.unblocked.filter=status:pending -BLOCKED\n"
"\n"
"report.blocking.description=Blocking tasks\n"
"report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n"
"report.blocking.columns=id,uuid.short,start.active,depends,project,tags,recur,wait,scheduled.remaining,due.relative,until.remaining,description.count,urgency\n"
"report.blocking.sort=urgency-,due+,entry+\n"
"report.blocking.filter=status:pending +BLOCKING\n"
"\n"
"report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n"
"\n";
// Supported modifiers, synonyms on the same line.
static const char* modifierNames[] =
{
"before", "under", "below",
"after", "over", "above",
"none",
"any",
"is", "equals",
"isnt", "not",
"has", "contains",
"hasnt",
"startswith", "left",
"endswith", "right",
"word",
"noword"
};
#define NUM_MODIFIER_NAMES (sizeof (modifierNames) / sizeof (modifierNames[0]))
////////////////////////////////////////////////////////////////////////////////
Context::~Context ()
{
for (auto& com : commands)
delete com.second;
for (auto& col : columns)
delete col.second;
}
////////////////////////////////////////////////////////////////////////////////
int Context::initialize (int argc, const char** argv)
{
timer_total.start ();
int rc = 0;
try
{
////////////////////////////////////////////////////////////////////////////
//
// [1] Load the correct config file.
// - Default to ~/.taskrc (ctor).
// - Allow command line override rc:<file>
// - Allow $TASKRC override.
// - Load resultant file.
// - Apply command line overrides to the config.
//
////////////////////////////////////////////////////////////////////////////
CLI2::getOverride (argc, argv, home_dir, rc_file);
char* override = getenv ("TASKRC");
if (override)
{
rc_file = File (override);
header (format (STRING_CONTEXT_RC_OVERRIDE, rc_file._data));
}
// Artificial scope for timing purposes.
{
Timer timer;
config.parse (configurationDefaults);
config.load (rc_file._data);
debugTiming (format ("Config::load ({1})", rc_file._data), timer);
}
CLI2::applyOverrides (argc, argv);
////////////////////////////////////////////////////////////////////////////
//
// [2] Locate the data directory.
// - Default to ~/.task (ctor).
// - Allow command line override rc.data.location:<dir>
// - Allow $TASKDATA override.
// - Inform TDB2 where to find data.
// - Create the rc_file and data_dir, if necessary.
//
////////////////////////////////////////////////////////////////////////////
CLI2::getDataLocation (argc, argv, data_dir);
override = getenv ("TASKDATA");
if (override)
{
data_dir = Directory (override);
config.set ("data.location", data_dir._data);
header (format (STRING_CONTEXT_DATA_OVERRIDE, data_dir._data));
}
tdb2.set_location (data_dir);
createDefaultConfig ();
////////////////////////////////////////////////////////////////////////////
//
// [3] Instantiate Command objects and capture command entities.
//
////////////////////////////////////////////////////////////////////////////
Command::factory (commands);
for (auto& cmd : commands)
cli2.entity ("cmd", cmd.first);
////////////////////////////////////////////////////////////////////////////
//
// [4] Instantiate Column objects and capture column entities.
//
////////////////////////////////////////////////////////////////////////////
Column::factory (columns);
for (auto& col : columns)
cli2.entity ("attribute", col.first);
cli2.entity ("pseudo", "limit");
////////////////////////////////////////////////////////////////////////////
//
// [5] Capture modifier and operator entities.
//
////////////////////////////////////////////////////////////////////////////
for (unsigned int i = 0; i < NUM_MODIFIER_NAMES; ++i)
cli2.entity ("modifier", modifierNames[i]);
for (auto& op : Eval::getOperators ())
cli2.entity ("operator", op);
for (auto& op : Eval::getBinaryOperators ())
cli2.entity ("binary_operator", op);
////////////////////////////////////////////////////////////////////////////
//
// [6] Complete the Context initialization.
//
////////////////////////////////////////////////////////////////////////////
initializeColorRules ();
staticInitialization ();
propagateDebug ();
loadAliases ();
////////////////////////////////////////////////////////////////////////////
//
// [7] Parse the command line.
//
////////////////////////////////////////////////////////////////////////////
for (int i = 0; i < argc; i++)
cli2.add (argv[i]);
cli2.analyze ();
// Extract a recomposed command line.
auto foundDefault = false;
auto foundAssumed = false;
std::string combined;
for (auto& a : cli2._args)
{
if (combined.length ())
combined += ' ';
combined += a.attribute ("raw");
if (a.hasTag ("DEFAULT"))
foundDefault = true;
if (a.hasTag ("ASSUMED"))
foundAssumed = true;
}
if (foundDefault)
header ("[" + combined + "]");
if (foundAssumed)
header (STRING_ASSUME_INFO);
////////////////////////////////////////////////////////////////////////////
//
// [8] Initialize hooks.
//
////////////////////////////////////////////////////////////////////////////
hooks.initialize ();
}
catch (const std::string& message)
{
error (message);
rc = 2;
}
catch (int)
{
// Hooks can terminate processing by throwing integers.
rc = 4;
}
catch (...)
{
error (STRING_UNKNOWN_ERROR);
rc = 3;
}
// On initialization failure...
if (rc)
{
// Dump all debug messages, controlled by rc.debug.
if (config.getBoolean ("debug"))
{
for (auto& d : debugMessages)
if (color ())
std::cerr << colorizeDebug (d) << '\n';
else
std::cerr << d << '\n';
}
// Dump all headers, controlled by 'header' verbosity token.
if (verbose ("header"))
{
for (auto& h : headers)
if (color ())
std::cerr << colorizeHeader (h) << '\n';
else
std::cerr << h << '\n';
}
// Dump all footnotes, controlled by 'footnote' verbosity token.
if (verbose ("footnote"))
{
for (auto& f : footnotes)
if (color ())
std::cerr << colorizeFootnote (f) << '\n';
else
std::cerr << f << '\n';
}
// Dump all errors, non-maskable.
// Colorized as footnotes.
for (auto& e : errors)
if (color ())
std::cerr << colorizeFootnote (e) << '\n';
else
std::cerr << e << '\n';
}
time_init_us += timer_total.total_us ();
return rc;
}
////////////////////////////////////////////////////////////////////////////////
int Context::run ()
{
int rc;
std::string output;
try
{
hooks.onLaunch ();
rc = dispatch (output);
tdb2.commit (); // Harmless if called when nothing changed.
hooks.onExit (); // No chance to update data.
timer_total.stop ();
time_total_us += timer_total.total_us ();
std::stringstream s;
s << "Perf "
<< PACKAGE_STRING
<< ' '
#ifdef HAVE_COMMIT
<< COMMIT
#else
<< '-'
#endif
<< ' '
<< Datetime ().toISO ()
<< " init:" << time_init_us
<< " load:" << time_load_us
<< " gc:" << (time_gc_us > 0 ? time_gc_us - time_load_us : time_gc_us)
<< " filter:" << time_filter_us
<< " commit:" << time_commit_us
<< " sort:" << time_sort_us
<< " render:" << time_render_us
<< " hooks:" << time_hooks_us
<< " other:" << time_total_us -
time_init_us -
time_gc_us -
time_filter_us -
time_commit_us -
time_sort_us -
time_render_us -
time_hooks_us
<< " total:" << time_total_us
<< '\n';
debug (s.str ());
}
catch (const std::string& message)
{
error (message);
rc = 2;
}
catch (int)
{
// Hooks can terminate processing by throwing integers.
rc = 4;
}
catch (...)
{
error (STRING_UNKNOWN_ERROR);
rc = 3;
}
// Dump all debug messages, controlled by rc.debug.
if (config.getBoolean ("debug"))
{
for (auto& d : debugMessages)
if (color ())
std::cerr << colorizeDebug (d) << '\n';
else
std::cerr << d << '\n';
}
// Dump all headers, controlled by 'header' verbosity token.
if (verbose ("header"))
{
for (auto& h : headers)
if (color ())
std::cerr << colorizeHeader (h) << '\n';
else
std::cerr << h << '\n';
}
// Dump the report output.
std::cout << output;
// Dump all footnotes, controlled by 'footnote' verbosity token.
if (verbose ("footnote"))
{
for (auto& f : footnotes)
if (color ())
std::cerr << colorizeFootnote (f) << '\n';
else
std::cerr << f << '\n';
}
// Dump all errors, non-maskable.
// Colorized as footnotes.
for (auto& e : errors)
if (color ())
std::cerr << colorizeError (e) << '\n';
else
std::cerr << e << '\n';
return rc;
}
////////////////////////////////////////////////////////////////////////////////
// Dispatch to the command found by the CLI parser.
int Context::dispatch (std::string &out)
{
// Autocomplete args against keywords.
std::string command = cli2.getCommand ();
if (command != "")
{
updateXtermTitle ();
updateVerbosity ();
Command* c = commands[command];
assert (c);
// The command know whether they need a GC.
if (c->needs_gc () &&
! tdb2.read_only ())
{
run_gc = config.getBoolean ("gc");
tdb2.gc ();
}
else
{
run_gc = false;
}
/*
// Only read-only commands can be run when TDB2 is read-only.
// TODO Implement TDB2::read_only
if (tdb2.read_only () && !c->read_only ())
throw std::string ("");
*/
// This is something that is only needed for write commands with no other
// filter processing.
if (c->accepts_modifications () &&
! c->accepts_filter ())
{
cli2.prepareFilter ();
}
// With rc.debug.parser == 2, there are more tree dumps than you might want,
// but we need the rc.debug.parser == 1 case covered also, with the final
// tree.
if (config.getBoolean ("debug") &&
config.getInteger ("debug.parser") == 1)
debug (cli2.dump ("Parse Tree (before command-specifіc processing)"));
return c->execute (out);
}
assert (commands["help"]);
return commands["help"]->execute (out);
}
////////////////////////////////////////////////////////////////////////////////
int Context::getWidth ()
{
// Determine window size.
auto width = config.getInteger ("defaultwidth");
// A zero width value means 'infinity', which is approximated here by 2^16.
if (width == 0)
return 65536;
if (config.getBoolean ("detection"))
{
if (terminal_width == 0 &&
terminal_height == 0)
{
unsigned short buff[4];
if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &buff) != -1)
{
terminal_height = buff[0];
terminal_width = buff[1];
}
}
width = terminal_width;
// Ncurses does this, and perhaps we need to as well, to avoid a problem on
// Cygwin where the display goes right up to the terminal width, and causes
// an odd color wrapping problem.
if (config.getBoolean ("avoidlastcolumn"))
--width;
}
return width;
}
////////////////////////////////////////////////////////////////////////////////
int Context::getHeight ()
{
// Determine window size.
auto height = config.getInteger ("defaultheight");
// A zero height value means 'infinity', which is approximated here by 2^16.
if (height == 0)
return 65536;
if (config.getBoolean ("detection"))
{
if (terminal_width == 0 &&
terminal_height == 0)
{
unsigned short buff[4];
if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &buff) != -1)
{
terminal_height = buff[0];
terminal_width = buff[1];
}
}
height = terminal_height;
}
return height;
}
////////////////////////////////////////////////////////////////////////////////
bool Context::color ()
{
if (determine_color_use)
{
// What the config says.
use_color = config.getBoolean ("color");
// Only tty's support color.
if (! isatty (STDOUT_FILENO))
{
// No ioctl.
config.set ("detection", "off");
config.set ("color", "off");
// Files don't get color.
use_color = false;
}
// Override.
if (config.getBoolean ("_forcecolor"))
{
config.set ("color", "on");
use_color = true;
}
// No need to go through this again.
determine_color_use = false;
}
// Cached result.
return use_color;
}
////////////////////////////////////////////////////////////////////////////////
// Support verbosity levels:
//
// rc.verbose=1 Show all feedback.
// rc.verbose=0 Show regular feedback.
// rc.verbose=nothing Show the absolute minimum.
// rc.verbose=one,two Show verbosity for 'one' and 'two' only.
//
// TODO This mechanism is clunky, and should slowly evolve into something more
// logical and consistent. This should probably mean that 'nothing' should
// take the place of '0'.
bool Context::verbose (const std::string& token)
{
if (verbosity.empty ())
{
verbosity_legacy = config.getBoolean ("verbose");
for (auto& token : split (config.get ("verbose"), ','))
verbosity.insert (token);
// Regular feedback means almost everything.
// This odd test is to see if a Boolean-false value is a real one, which
// means it is not 1/true/T/yes/on, but also should not be one of the
// valid tokens either.
if (! verbosity_legacy && ! verbosity.empty ())
{
std::string v = *(verbosity.begin ());
if (v != "nothing" &&
v != "blank" && // This list must be complete.
v != "header" && //
v != "footnote" && //
v != "label" && //
v != "new-id" && //
v != "new-uuid" && //
v != "affected" && //
v != "edit" && //
v != "special" && //
v != "project" && //
v != "sync" && //
v != "filter" && //
v != "unwait" && //
v != "recur") //
{
// This list emulates rc.verbose=off in version 1.9.4.
verbosity = {"blank", "label", "new-id", "edit"};
}
}
// Some flags imply "footnote" verbosity being active. Make it so.
if (! verbosity.count ("footnote"))
{
// TODO: Some of these may not use footnotes yet. They should.
for (auto flag : {"affected", "new-id", "new-uuid", "project", "unwait", "recur"})
{
if (verbosity.count (flag))
{
verbosity.insert ("footnote");
break;
}
}
}
}
// rc.verbose=true|y|yes|1|on overrides all.
if (verbosity_legacy)
return true;
// rc.verbose=nothing overrides all.
if (verbosity.size () == 1 &&
*(verbosity.begin ()) == "nothing")
return false;
// Specific token match.
if (verbosity.count (token))
return true;
return false;
}
////////////////////////////////////////////////////////////////////////////////
const std::vector <std::string> Context::getColumns () const
{
std::vector <std::string> output;
for (auto& col : columns)
output.push_back (col.first);
return output;
}
////////////////////////////////////////////////////////////////////////////////
// A value of zero mean unlimited.
// A value of 'page' means however many screen lines there are.
// A value of a positive integer is a row/task limit.
void Context::getLimits (int& rows, int& lines)
{
rows = 0;
lines = 0;
// This is an integer specified as a filter (limit:10).
auto limit = config.get ("limit");
if (limit != "")
{
if (limit == "page")
{
rows = 0;
lines = getHeight ();
}
else
{
rows = (int) strtol (limit.c_str (), NULL, 10);
lines = 0;
}
}
}
////////////////////////////////////////////////////////////////////////////////
// The 'Task' object, among others, is shared between projects. To make this
// easier, it has been decoupled from Context.
void Context::staticInitialization ()
{
CLI2::minimumMatchLength = config.getInteger ("abbreviation.minimum");
Lexer::minimumMatchLength = config.getInteger ("abbreviation.minimum");
Task::defaultProject = config.get ("default.project");
Task::defaultDue = config.get ("default.due");
Task::defaultScheduled = config.get ("default.scheduled");
Task::searchCaseSensitive = Variant::searchCaseSensitive = config.getBoolean ("search.case.sensitive");
Task::regex = Variant::searchUsingRegex = config.getBoolean ("regex");
Lexer::dateFormat = Variant::dateFormat = config.get ("dateformat");
Datetime::isoEnabled = config.getBoolean ("date.iso");
TDB2::debug_mode = config.getBoolean ("debug");
for (auto& rc : config)
{
if (rc.first.substr (0, 4) == "uda." &&
rc.first.substr (rc.first.length () - 7, 7) == ".values")
{
std::string name = rc.first.substr (4, rc.first.length () - 7 - 4);
auto values = split (rc.second, ',');
for (auto r = values.rbegin(); r != values.rend (); ++r)
Task::customOrder[name].push_back (*r);
}
}
for (auto& col : columns)
{
Task::attributes[col.first] = col.second->type ();
Lexer::attributes[col.first] = col.second->type ();
}
Task::urgencyProjectCoefficient = config.getReal ("urgency.project.coefficient");
Task::urgencyActiveCoefficient = config.getReal ("urgency.active.coefficient");
Task::urgencyScheduledCoefficient = config.getReal ("urgency.scheduled.coefficient");
Task::urgencyWaitingCoefficient = config.getReal ("urgency.waiting.coefficient");
Task::urgencyBlockedCoefficient = config.getReal ("urgency.blocked.coefficient");
Task::urgencyAnnotationsCoefficient = config.getReal ("urgency.annotations.coefficient");
Task::urgencyTagsCoefficient = config.getReal ("urgency.tags.coefficient");
Task::urgencyDueCoefficient = config.getReal ("urgency.due.coefficient");
Task::urgencyBlockingCoefficient = config.getReal ("urgency.blocking.coefficient");
Task::urgencyAgeCoefficient = config.getReal ("urgency.age.coefficient");
Task::urgencyAgeMax = config.getReal ("urgency.age.max");
// Tag- and project-specific coefficients.
for (auto& var : config.all ())
if (var.substr (0, 13) == "urgency.user." ||
var.substr (0, 12) == "urgency.uda.")
Task::coefficients[var] = config.getReal (var);
}
////////////////////////////////////////////////////////////////////////////////
void Context::createDefaultConfig ()
{
// Do we need to create a default rc?
if (rc_file._data != "" && ! rc_file.exists ())
{
if (config.getBoolean ("confirmation") &&
! confirm (format (STRING_CONTEXT_CREATE_RC, home_dir, rc_file._data)))
throw std::string (STRING_CONTEXT_NEED_RC);
// Override data.location in the defaults.
auto loc = configurationDefaults.find ("data.location=~/.task");
// loc+0^ +14^ +21^
Datetime now;
std::stringstream contents;
contents << "# [Created by "
<< PACKAGE_STRING
<< ' '
<< now.toString ("m/d/Y H:N:S")
<< "]\n"
<< configurationDefaults.substr (0, loc + 14)
<< data_dir._original
<< "\n\n# Color theme (uncomment one to use)\n"
<< "#include " << TASK_RCDIR << "/light-16.theme\n"
<< "#include " << TASK_RCDIR << "/light-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-16.theme\n"
<< "#include " << TASK_RCDIR << "/dark-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-red-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-green-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-blue-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-violets-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-yellow-green.theme\n"
<< "#include " << TASK_RCDIR << "/dark-gray-256.theme\n"
<< "#include " << TASK_RCDIR << "/dark-gray-blue-256.theme\n"
<< "#include " << TASK_RCDIR << "/solarized-dark-256.theme\n"
<< "#include " << TASK_RCDIR << "/solarized-light-256.theme\n"
<< "#include " << TASK_RCDIR << "/no-color.theme\n"
<< '\n';
// Write out the new file.
if (! File::write (rc_file._data, contents.str ()))
throw format (STRING_CONFIG_BAD_WRITE, rc_file._data);
}
// Create data location, if necessary.
Directory d (data_dir);
if (! d.exists ())
{
if (config.getBoolean ("exit.on.missing.db"))
throw std::string ("Error: rc.data.location does not exist - exiting according to rc.exit.on.missing.db setting.");
d.create ();
if (config.has ("hooks.location"))
d = Directory (config.get ("hooks.location"));
else
d += "hooks";
d.create ();
}
}
////////////////////////////////////////////////////////////////////////////////
void Context::decomposeSortField (
const std::string& field,
std::string& key,
bool& ascending,
bool& breakIndicator)
{
int length = field.length ();
int decoration = 1;
breakIndicator = false;
if (field[length - decoration] == '/')
{
breakIndicator = true;
++decoration;
}
if (field[length - decoration] == '+')
{
ascending = true;
key = field.substr (0, length - decoration);
}
else if (field[length - decoration] == '-')
{
ascending = false;
key = field.substr (0, length - decoration);
}
else
{
ascending = true;
key = field;
}
}
////////////////////////////////////////////////////////////////////////////////
void Context::debugTiming (const std::string& details, const Timer& timer)
{
std::stringstream out;
out << "Timer "
<< details
<< ' '
<< std::setprecision (6)
<< std::fixed
<< timer.total_us () / 1.0e6
<< " sec";
debug (out.str ());
}
////////////////////////////////////////////////////////////////////////////////
// This capability is to answer the question of 'what did I just do to generate
// this output?'.
void Context::updateXtermTitle ()
{
if (config.getBoolean ("xterm.title") && isatty (STDOUT_FILENO))
{
auto command = cli2.getCommand ();
std::string title;
for (auto a = cli2._args.begin (); a != cli2._args.end (); ++a)
{
if (a != cli2._args.begin ())
title += ' ';
title += a->attribute ("raw");
}
std::cout << "]0;task " << command << ' ' << title << "";
}
}
////////////////////////////////////////////////////////////////////////////////
// This function allows a clean output if the command is a helper subcommand.
void Context::updateVerbosity ()
{
auto command = cli2.getCommand ();
if (command != "" &&
command[0] == '_')
{
verbosity = {"nothing"};
}
}
////////////////////////////////////////////////////////////////////////////////
void Context::loadAliases ()
{
for (auto& i : config)
if (i.first.substr (0, 6) == "alias.")
cli2.alias (i.first.substr (6), i.second);
}
////////////////////////////////////////////////////////////////////////////////
// Using the general rc.debug setting automaticalls sets debug.tls, debug.hooks
// and debug.parser, unless they already have values, which by default they do
// not.
void Context::propagateDebug ()
{
if (config.getBoolean ("debug"))
{
if (! config.has ("debug.tls"))
config.set ("debug.tls", 2);
if (! config.has ("debug.hooks"))
config.set ("debug.hooks", 1);
if (! config.has ("debug.parser"))
config.set ("debug.parser", 1);
}
else
{
if ((config.has ("debug.hooks") && config.getInteger ("debug.hooks")) ||
(config.has ("debug.parser") && config.getInteger ("debug.parser")) )
config.set ("debug", true);
}
}
////////////////////////////////////////////////////////////////////////////////
// No duplicates.
void Context::header (const std::string& input)
{
if (input.length () &&
std::find (headers.begin (), headers.end (), input) == headers.end ())
headers.push_back (input);
}
////////////////////////////////////////////////////////////////////////////////
// No duplicates.
void Context::footnote (const std::string& input)
{
if (input.length () &&
std::find (footnotes.begin (), footnotes.end (), input) == footnotes.end ())
footnotes.push_back (input);
}
////////////////////////////////////////////////////////////////////////////////
// No duplicates.
void Context::error (const std::string& input)
{
if (input.length () &&
std::find (errors.begin (), errors.end (), input) == errors.end ())
errors.push_back (input);
}
////////////////////////////////////////////////////////////////////////////////
void Context::debug (const std::string& input)
{
if (input.length ())
debugMessages.push_back (input);
}
////////////////////////////////////////////////////////////////////////////////