1 ////////////////////////////////////////////////////////////////////////////////
2 //
3 // Copyright 2006 - 2021, Tomas Babej, Paul Beckingham, Federico Hernandez.
4 //
5 // Permission is hereby granted, free of charge, to any person obtaining a copy
6 // of this software and associated documentation files (the "Software"), to deal
7 // in the Software without restriction, including without limitation the rights
8 // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 // copies of the Software, and to permit persons to whom the Software is
10 // furnished to do so, subject to the following conditions:
11 //
12 // The above copyright notice and this permission notice shall be included
13 // in all copies or substantial portions of the Software.
14 //
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
16 // OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
18 // THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 // SOFTWARE.
22 //
23 // https://www.opensource.org/licenses/mit-license.php
24 //
25 ////////////////////////////////////////////////////////////////////////////////
26 
27 #include <cmake.h>
28 #include <Context.h>
29 #include <iostream>
30 #include <fstream>
31 #include <sstream>
32 #include <iomanip>
33 #include <algorithm>
34 #include <assert.h>
35 #include <stdlib.h>
36 #include <string.h>
37 #include <unistd.h>
38 #include <FS.h>
39 #include <Eval.h>
40 #include <Variant.h>
41 #include <Datetime.h>
42 #include <Duration.h>
43 #include <shared.h>
44 #include <format.h>
45 #include <main.h>
46 
47 #ifdef HAVE_COMMIT
48 #include <commit.h>
49 #endif
50 
51 #include <stdio.h>
52 #include <sys/ioctl.h>
53 
54 #ifdef SOLARIS
55 #include <sys/termios.h>
56 #endif
57 
58 ////////////////////////////////////////////////////////////////////////////////
59 // This string is parsed and used as default values for configuration.
60 std::string configurationDefaults =
61   "# Taskwarrior program configuration file.\n"
62   "# For more documentation, see https://taskwarrior.org or try 'man task', 'man task-color',\n"
63   "# 'man task-sync' or 'man taskrc'\n"
64   "\n"
65   "# Here is an example of entries that use the default, override and blank values\n"
66   "#   variable=foo   -- By specifying a value, this overrides the default\n"
67   "#   variable=      -- By specifying no value, this means no default\n"
68   "#   #variable=foo  -- By commenting out the line, or deleting it, this uses the default\n"
69   "\n"
70   "# You can also refence environment variables:\n"
71   "#   variable=$HOME/task\n"
72   "#   variable=$VALUE\n"
73   "\n"
74   "# Use the command 'task show' to see all defaults and overrides\n"
75   "\n"
76   "# Files\n"
77   "data.location=~/.task\n"
78   "locking=1                                      # Use file-level locking\n"
79   "gc=1                                           # Garbage-collect data files - DO NOT CHANGE unless you are sure\n"
80   "exit.on.missing.db=0                           # Whether to exit if ~/.task is not found\n"
81   "hooks=1                                        # Master control switch for hooks\n"
82   "\n"
83   "# Terminal\n"
84   "detection=1                                    # Detects terminal width\n"
85   "defaultwidth=80                                # Without detection, assumed width\n"
86   "defaultheight=24                               # Without detection, assumed height\n"
87   "avoidlastcolumn=0                              # Fixes Cygwin width problem\n"
88   "hyphenate=1                                    # Hyphenates lines wrapped on non-word-breaks\n"
89   "#editor=vi                                     # Preferred text editor\n"
90   "reserved.lines=1                               # Assume a 1-line prompt\n"
91   "\n"
92   "# Miscellaneous\n"
93   "# verbose=                                     # Comma-separated list.  May contain any subset of:\n"
94   "# affected,blank,context,default,edit,filter,footnote,header,label,new-id,new-uuid,override,project,recur,special,sync\n"
95   "verbose=affected,blank,context,edit,header,footnote,label,new-id,project,special,sync,override,recur\n"
96   "confirmation=1                                 # Confirmation on delete, big changes\n"
97   "recurrence=1                                   # Enable recurrence\n"
98   "recurrence.confirmation=prompt                 # Confirmation for propagating changes among recurring tasks (yes/no/prompt)\n"
99   "allow.empty.filter=1                           # An empty filter gets a warning and requires confirmation\n"
100   "indent.annotation=2                            # Indent spaces for annotations\n"
101   "indent.report=0                                # Indent spaces for whole report\n"
102   "row.padding=0                                  # Left and right padding for each row of report\n"
103   "column.padding=1                               # Spaces between each column in a report\n"
104   "bulk=3                                         # 3 or more tasks considered a bulk change and is confirmed\n"
105   "nag=You have more urgent tasks.                # Nag message to keep you honest\n"                      // TODO
106   "search.case.sensitive=1                        # Setting to no allows case insensitive searches\n"
107   "active.indicator=*                             # What to show as an active task indicator\n"
108   "tag.indicator=+                                # What to show as a tag indicator\n"
109   "dependency.indicator=D                         # What to show as a dependency indicator\n"
110   "recurrence.indicator=R                         # What to show as a task recurrence indicator\n"
111   "recurrence.limit=1                             # Number of future recurring pending tasks\n"
112   "undo.style=side                                # Undo style - can be 'side', or 'diff'\n"
113   "regex=1                                        # Assume all search/filter strings are regexes\n"
114   "xterm.title=0                                  # Sets xterm title for some commands\n"
115   "expressions=infix                              # Prefer infix over postfix expressions\n"
116   "json.array=1                                   # Enclose JSON output in [ ]\n"
117   "abbreviation.minimum=2                         # Shortest allowed abbreviation\n"
118   "news.version=                                  # Latest version higlights read by the user\n"
119   "\n"
120   "# Dates\n"
121   "dateformat=Y-M-D                               # Preferred input and display date format\n"
122   "dateformat.holiday=YMD                         # Preferred input date format for holidays\n"
123   "dateformat.edit=Y-M-D H:N:S                    # Preferred display date format when editing\n"
124   "dateformat.info=Y-M-D H:N:S                    # Preferred display date format for information\n"
125   "dateformat.report=                             # Preferred display date format for reports\n"
126   "dateformat.annotation=                         # Preferred display date format for annotations\n"
127   "date.iso=1                                     # Enable ISO date support\n"
128   "weekstart=sunday                               # Sunday or Monday only\n"
129   "displayweeknumber=1                            # Show week numbers on calendar\n"
130   "due=7                                          # Task is considered due in 7 days\n"
131   "\n"
132   "# Calendar controls\n"
133   "calendar.legend=1                              # Display the legend on calendar\n"
134   "calendar.details=sparse                        # Calendar shows information for tasks w/due dates: full, sparse or none\n"
135   "calendar.details.report=list                   # Report to use when showing task information in cal\n"
136   "calendar.offset=0                              # Apply an offset value to control the first month of the calendar\n"
137   "calendar.offset.value=-1                       # The number of months the first month of the calendar is moved\n"
138   "calendar.holidays=none                         # Show public holidays on calendar:full, sparse or none\n"
139   "#calendar.monthsperline=3                      # Number of calendar months on a line\n"
140   "\n"
141   "# Journal controls\n"
142   "journal.time=0                                 # Record start/stop commands as annotation\n"
143   "journal.time.start.annotation=Started task     # Annotation description for the start journal entry\n"
144   "journal.time.stop.annotation=Stopped task      # Annotation description for the stop  journal entry\n"
145   "journal.info=1                                 # Display task journal with info command\n"
146   "\n"
147   "# Dependency controls\n"
148   "dependency.reminder=1                          # Nags on dependency chain violations\n"
149   "dependency.confirmation=1                      # Should dependency chain repair be confirmed?\n"
150   "\n"
151   "# Urgency Coefficients\n"
152   "urgency.user.tag.next.coefficient=15.0         # Urgency coefficient for 'next' special tag\n"
153   "urgency.due.coefficient=12.0                   # Urgency coefficient for due dates\n"
154   "urgency.blocking.coefficient=8.0               # Urgency coefficient for blocking tasks\n"
155   "urgency.active.coefficient=4.0                 # Urgency coefficient for active tasks\n"
156   "urgency.scheduled.coefficient=5.0              # Urgency coefficient for scheduled tasks\n"
157   "urgency.age.coefficient=2.0                    # Urgency coefficient for age\n"
158   "urgency.annotations.coefficient=1.0            # Urgency coefficient for annotations\n"
159   "urgency.tags.coefficient=1.0                   # Urgency coefficient for tags\n"
160   "urgency.project.coefficient=1.0                # Urgency coefficient for projects\n"
161   "urgency.blocked.coefficient=-5.0               # Urgency coefficient for blocked tasks\n"
162   "urgency.waiting.coefficient=-3.0               # Urgency coefficient for waiting status\n"
163   "urgency.inherit=0                              # Recursively inherit highest urgency value from blocked tasks\n"
164   "urgency.age.max=365                            # Maximum age in days\n"
165   "\n"
166   "#urgency.user.project.foo.coefficient=5.0      # Urgency coefficients for 'foo' project\n"
167   "#urgency.user.tag.foo.coefficient=5.0          # Urgency coefficients for 'foo' tag\n"
168   "#urgency.uda.foo.coefficient=5.0               # Urgency coefficients for UDA 'foo'\n"
169   "\n"
170   "# Color controls.\n"
171   "color=1                                        # Enable color\n"
172   "\n"
173   "# Here is the rule precedence order, highest to lowest.\n"
174   "# Note that these are just the color rule names, without the leading 'color.'\n"
175   "#      and any trailing '.value'.\n"
176   "rule.precedence.color=deleted,completed,active,keyword.,tag.,project.,overdue,scheduled,due.today,due,blocked,blocking,recurring,tagged,uda.\n"
177   "\n"
178   "# General decoration\n"
179   "rule.color.merge=1\n"
180   "color.label=\n"
181   "color.label.sort=\n"
182   "color.alternate=on gray2\n"
183   "color.header=color3\n"
184   "color.footnote=color3\n"
185   "color.warning=bold red\n"
186   "color.error=white on red\n"
187   "color.debug=color4\n"
188   "\n"
189   "# Task state\n"
190   "color.completed=\n"
191   "color.deleted=\n"
192   "color.active=rgb555 on rgb410\n"
193   "color.recurring=rgb013\n"
194   "color.scheduled=on rgb001\n"
195   "color.until=\n"
196   "color.blocked=white on color8\n"
197   "color.blocking=black on color15\n"
198   "\n"
199   "# Project\n"
200   "color.project.none=\n"
201   "\n"
202   "# Priority UDA\n"
203   "color.uda.priority.H=color255\n"
204   "color.uda.priority.L=color245\n"
205   "color.uda.priority.M=color250\n"
206   "\n"
207   "# Tags\n"
208   "color.tag.next=rgb440\n"
209   "color.tag.none=\n"
210   "color.tagged=rgb031\n"
211   "\n"
212   "# Due\n"
213   "color.due.today=rgb400\n"
214   "color.due=color1\n"
215   "color.overdue=color9\n"
216   "\n"
217   "# Report: burndown\n"
218   "color.burndown.done=on rgb010\n"
219   "color.burndown.pending=on color9\n"
220   "color.burndown.started=on color11\n"
221   "\n"
222   "# Report: history\n"
223   "color.history.add=color0 on rgb500\n"
224   "color.history.delete=color0 on rgb550\n"
225   "color.history.done=color0 on rgb050\n"
226   "\n"
227   "# Report: summary\n"
228   "color.summary.background=white on color0\n"
229   "color.summary.bar=black on rgb141\n"
230   "\n"
231   "# Command: calendar\n"
232   "color.calendar.due.today=color15 on color1\n"
233   "color.calendar.due=color0 on color1\n"
234   "color.calendar.holiday=color0 on color11\n"
235   "color.calendar.scheduled=rgb013 on color15\n"
236   "color.calendar.overdue=color0 on color9\n"
237   "color.calendar.today=color15 on rgb013\n"
238   "color.calendar.weekend=on color235\n"
239   "color.calendar.weeknumber=rgb013\n"
240   "\n"
241   "# Command: sync\n"
242   "color.sync.added=rgb010\n"
243   "color.sync.changed=color11\n"
244   "color.sync.rejected=color9\n"
245   "\n"
246   "# Command: undo\n"
247   "color.undo.after=color2\n"
248   "color.undo.before=color1\n"
249   "\n"
250   "# UDA priority\n"
251   "uda.priority.type=string                       # UDA priority is a string type\n"
252   "uda.priority.label=Priority                    # UDA priority has a display label'\n"
253   "uda.priority.values=H,M,L,                     # UDA priority values are 'H', 'M', 'L' or ''\n"
254   "                                               # UDA priority sorting is 'H' > 'M' > 'L' > '' (highest to lowest)\n"
255   "#uda.priority.default=M                        # UDA priority default value of 'M'\n"
256   "urgency.uda.priority.H.coefficient=6.0         # UDA priority coefficient for value 'H'\n"
257   "urgency.uda.priority.M.coefficient=3.9         # UDA priority coefficient for value 'M'\n"
258   "urgency.uda.priority.L.coefficient=1.8         # UDA priority coefficient for value 'L'\n"
259   "\n"
260   "#default.project=foo                           # Default project for 'add' command\n"
261   "#default.due=eom                               # Default due date for 'add' command\n"
262   "#default.scheduled=eom                         # Default scheduled date for 'add' command\n"
263   "default.command=next                           # When no arguments are specified\n"
264   "default.timesheet.filter=( +PENDING and start.after:now-4wks ) or ( +COMPLETED and end.after:now-4wks )\n"
265   "\n"
266   "_forcecolor=0                                  # Forces color to be on, even for non TTY output\n"
267   "complete.all.tags=0                            # Include old tag names in '_ags' command\n"
268   "list.all.projects=0                            # Include old project names in 'projects' command\n"
269   "summary.all.projects=0                         # Include old project names in 'summary' command\n"
270   "list.all.tags=0                                # Include old tag names in 'tags' command\n"
271   "print.empty.columns=0                          # Print columns which have no data for any task\n"
272   "debug=0                                        # Display diagnostics\n"
273   "debug.tls=0                                    # Sync diagnostics\n"
274   "sugar=1                                        # Syntactic sugar\n"
275   "obfuscate=0                                    # Obfuscate data for error reporting\n"
276   "fontunderline=1                                # Uses underlines rather than -------\n"
277   "\n"
278   "# WARNING: Please read the documentation (man task-sync) before setting up\n"
279   "#          Taskwarrior for Taskserver synchronization.\n"
280   "#taskd.ca=<certificate file>\n"
281   "#taskd.certificate=<certificate file>\n"
282   "#taskd.credentials=<organization>/<name>/<password>\n"
283   "#taskd.server=<server>:<port>\n"
284   "taskd.trust=strict\n"
285   "#taskd.trust=ignore hostname\n"
286   "#taskd.trust=allow all\n"
287   "taskd.ciphers=NORMAL\n"
288   "\n"
289   "# Aliases - alternate names for commands\n"
290   "alias.rm=delete                                # Alias for the delete command\n"
291   "alias.history=history.monthly                  # Prefer monthly over annual history reports\n"
292   "alias.ghistory=ghistory.monthly                # Prefer monthly graphical over annual history reports\n"
293   "alias.burndown=burndown.weekly                 # Prefer the weekly burndown chart\n"
294   "\n"
295   "# Reports\n"
296   "\n"
297   "report.long.description=All details of tasks\n"
298   "report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,Description\n"
299   "report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,wait.remaining,scheduled,due,until,description\n"
300   "report.long.filter=status:pending -WAITING\n"
301   "report.long.sort=modified-\n"
302   "report.long.context=1\n"
303   "\n"
304   "report.list.description=Most details of tasks\n"
305   "report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n"
306   "report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n"
307   "report.list.filter=status:pending -WAITING\n"
308   "report.list.sort=start-,due+,project+,urgency-\n"
309   "report.list.context=1\n"
310   "\n"
311   "report.ls.description=Few details of tasks\n"
312   "report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n"
313   "report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait.remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n"
314   "report.ls.filter=status:pending -WAITING\n"
315   "report.ls.sort=start-,description+\n"
316   "report.ls.context=1\n"
317   "\n"
318   "report.minimal.description=Minimal details of tasks\n"
319   "report.minimal.labels=ID,Project,Tags,Description\n"
320   "report.minimal.columns=id,project,tags.count,description.count\n"
321   "report.minimal.filter=status:pending\n"
322   "report.minimal.sort=project+/,description+\n"
323   "report.minimal.context=1\n"
324   "\n"
325   "report.newest.description=Newest tasks\n"
326   "report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
327   "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"
328   "report.newest.filter=status:pending\n"
329   "report.newest.sort=entry-\n"
330   "report.newest.context=1\n"
331   "\n"
332   "report.oldest.description=Oldest tasks\n"
333   "report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
334   "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"
335   "report.oldest.filter=status:pending\n"
336   "report.oldest.sort=entry+\n"
337   "report.oldest.context=1\n"
338   "\n"
339   "report.overdue.description=Overdue tasks\n"
340   "report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n"
341   "report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,scheduled.countdown,due,until,description,urgency\n"
342   "report.overdue.filter=status:pending and +OVERDUE\n"
343   "report.overdue.sort=urgency-,due+\n"
344   "report.overdue.context=1\n"
345   "\n"
346   "report.active.description=Active tasks\n"
347   "report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,Description\n"
348   "report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,recur,wait,scheduled.remaining,due,until,description\n"
349   "report.active.filter=status:pending and +ACTIVE\n"
350   "report.active.sort=project+,start+\n"
351   "report.active.context=1\n"
352   "\n"
353   "report.completed.description=Completed tasks\n"
354   "report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n"
355   "report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,recur.indicator,due,description\n"
356   "report.completed.filter=status:completed\n"
357   "report.completed.sort=end+\n"
358   "report.completed.context=1\n"
359   "\n"
360   "report.recurring.description=Recurring Tasks\n"
361   "report.recurring.labels=ID,Active,Age,D,P,Project,Tags,Recur,Sch,Due,Until,Description,Urg\n"
362   "report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
363   "report.recurring.filter=status:pending and (+PARENT or +CHILD)\n"
364   "report.recurring.sort=due+,urgency-,entry+\n"
365   "report.recurring.context=1\n"
366   "\n"
367   "report.waiting.description=Waiting (hidden) tasks\n"
368   "report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n"
369   "report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,recur.indicator,wait,wait.remaining,scheduled,due,until,description\n"
370   "report.waiting.filter=+WAITING\n"
371   "report.waiting.sort=due+,wait+,entry+\n"
372   "report.waiting.context=1\n"
373   "\n"
374   "report.all.description=All tasks\n"
375   "report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
376   "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"
377   "report.all.sort=entry-\n"
378   "report.all.context=1\n"
379   "\n"
380   "report.next.description=Most urgent tasks\n"
381   "report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n"
382   "report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled.countdown,due.relative,until.remaining,description,urgency\n"
383   "report.next.filter=status:pending -WAITING limit:page\n"
384   "report.next.sort=urgency-\n"
385   "report.next.context=1\n"
386   "\n"
387   "report.ready.description=Most urgent actionable tasks\n"
388   "report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n"
389   "report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur.indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n"
390   "report.ready.filter=+READY\n"
391   "report.ready.sort=start-,urgency-\n"
392   "report.ready.context=1\n"
393   "\n"
394   "report.blocked.description=Blocked tasks\n"
395   "report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
396   "report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
397   "report.blocked.sort=due+,priority-,start-,project+\n"
398   "report.blocked.filter=status:pending -WAITING +BLOCKED\n"
399   "report.blocked.context=1\n"
400   "\n"
401   "report.unblocked.description=Unblocked tasks\n"
402   "report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
403   "report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
404   "report.unblocked.sort=due+,priority-,start-,project+\n"
405   "report.unblocked.filter=status:pending -WAITING -BLOCKED\n"
406   "report.unblocked.context=1\n"
407   "\n"
408   "report.blocking.description=Blocking tasks\n"
409   "report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n"
410   "report.blocking.columns=id,uuid.short,start.active,depends,project,tags,recur,wait,scheduled.remaining,due.relative,until.remaining,description.count,urgency\n"
411   "report.blocking.sort=urgency-,due+,entry+\n"
412   "report.blocking.filter=status:pending -WAITING +BLOCKING\n"
413   "report.blocking.context=1\n"
414   "\n"
415   "report.timesheet.filter=(+PENDING and start.after:now-4wks) or (+COMPLETED and end.after:now-4wks)\n"
416   "report.timesheet.context=0\n"
417   "\n";
418 
419 // Supported modifiers, synonyms on the same line.
420 static const char* modifierNames[] =
421 {
422   "before",     "under",    "below",
423   "after",      "over",     "above",
424   "by",
425   "none",
426   "any",
427   "is",         "equals",
428   "isnt",       "not",
429   "has",        "contains",
430   "hasnt",
431   "startswith", "left",
432   "endswith",   "right",
433   "word",
434   "noword"
435 };
436 
437 Context* Context::context;
438 
439 ////////////////////////////////////////////////////////////////////////////////
getContext()440 Context& Context::getContext ()
441 {
442   return *Context::context;
443 }
444 
445 ////////////////////////////////////////////////////////////////////////////////
setContext(Context * context)446 void Context::setContext (Context* context)
447 {
448   Context::context = context;
449 }
450 
451 ////////////////////////////////////////////////////////////////////////////////
~Context()452 Context::~Context ()
453 {
454   for (auto& com : commands)
455     delete com.second;
456 
457   for (auto& col : columns)
458     delete col.second;
459 }
460 
461 ////////////////////////////////////////////////////////////////////////////////
initialize(int argc,const char ** argv)462 int Context::initialize (int argc, const char** argv)
463 {
464   timer_total.start ();
465   int rc = 0;
466   home_dir = getenv ("HOME");
467 
468   std::vector <std::string> searchPaths { TASK_RCDIR };
469 
470   try
471   {
472     ////////////////////////////////////////////////////////////////////////////
473     //
474     // [1] Load the correct config file.
475     //     - Default to ~/.taskrc (ctor).
476     //     - If no ~/.taskrc, use $XDG_CONFIG_HOME/task/taskrc if exists, or
477     //       ~/.config/task/taskrc if $XDG_DATA_HOME is unset
478     //     - Allow $TASKRC override.
479     //     - Allow command line override rc:<file>
480     //     - Load resultant file.
481     //     - Apply command line overrides to the config.
482     //
483     ////////////////////////////////////////////////////////////////////////////
484 
485     bool taskrc_overridden = false;
486 
487     // XDG_CONFIG_HOME doesn't count as an override (no warning header)
488     if (! rc_file.exists ())
489     {
490       // Use XDG_CONFIG_HOME if defined, otherwise default to ~/.config
491       std::string xdg_config_home;
492       const char* env_xdg_config_home = getenv ("XDG_CONFIG_HOME");
493 
494       if (env_xdg_config_home)
495         xdg_config_home = format ("{1}", env_xdg_config_home);
496       else
497         xdg_config_home = format ("{1}/.config", home_dir);
498 
499       // Ensure the path does not end with '/'
500       if (xdg_config_home.back () == '/')
501         xdg_config_home.pop_back();
502 
503       // https://github.com/GothenburgBitFactory/libshared/issues/32
504       std::string rcfile_path = format ("{1}/task/taskrc", xdg_config_home);
505 
506       File maybe_rc_file = File (rcfile_path);
507       if ( maybe_rc_file.exists ())
508         rc_file = maybe_rc_file;
509     }
510 
511     char *override = getenv ("TASKRC");
512     if (override)
513     {
514       rc_file = File (override);
515       taskrc_overridden = true;
516     }
517 
518     taskrc_overridden =
519         CLI2::getOverride (argc, argv, rc_file) || taskrc_overridden;
520 
521     // Artificial scope for timing purposes.
522     {
523       Timer timer;
524       config.parse (configurationDefaults, 1, searchPaths);
525       config.load (rc_file._data, 1, searchPaths);
526       debugTiming (format ("Config::load ({1})", rc_file._data), timer);
527     }
528 
529     CLI2::applyOverrides (argc, argv);
530 
531     if (taskrc_overridden && verbose ("override"))
532       header (format ("TASKRC override: {1}", rc_file._data));
533 
534     ////////////////////////////////////////////////////////////////////////////
535     //
536     // [2] Locate the data directory.
537     //     - Default to ~/.task (ctor).
538     //     - Allow $TASKDATA override.
539     //     - Allow command line override rc.data.location:<dir>
540     //     - Inform TDB2 where to find data.
541     //     - Create the rc_file and data_dir, if necessary.
542     //
543     ////////////////////////////////////////////////////////////////////////////
544 
545     bool taskdata_overridden = false;
546 
547     override = getenv ("TASKDATA");
548     if (override)
549     {
550       data_dir = Directory (override);
551       config.set ("data.location", data_dir._data);
552       taskdata_overridden = true;
553     }
554 
555     taskdata_overridden =
556         CLI2::getDataLocation (argc, argv, data_dir) || taskdata_overridden;
557 
558     if (taskdata_overridden && verbose ("override"))
559       header (format ("TASKDATA override: {1}", data_dir._data));
560 
561     tdb2.set_location (data_dir);
562     createDefaultConfig ();
563 
564     ////////////////////////////////////////////////////////////////////////////
565     //
566     // [3] Instantiate Command objects and capture command entities.
567     //
568     ////////////////////////////////////////////////////////////////////////////
569 
570     Command::factory (commands);
571     for (auto& cmd : commands)
572       cli2.entity ("cmd", cmd.first);
573 
574     ////////////////////////////////////////////////////////////////////////////
575     //
576     // [4] Instantiate Column objects and capture column entities.
577     //
578     ////////////////////////////////////////////////////////////////////////////
579 
580     Column::factory (columns);
581     for (auto& col : columns)
582       cli2.entity ("attribute", col.first);
583 
584     cli2.entity ("pseudo", "limit");
585 
586     ////////////////////////////////////////////////////////////////////////////
587     //
588     // [5] Capture modifier and operator entities.
589     //
590     ////////////////////////////////////////////////////////////////////////////
591 
592     for (auto& modifierName : modifierNames)
593       cli2.entity ("modifier", modifierName);
594 
595     for (auto& op : Eval::getOperators ())
596       cli2.entity ("operator", op);
597 
598     for (auto& op : Eval::getBinaryOperators ())
599       cli2.entity ("binary_operator", op);
600 
601     ////////////////////////////////////////////////////////////////////////////
602     //
603     // [6] Complete the Context initialization.
604     //
605     ////////////////////////////////////////////////////////////////////////////
606 
607     initializeColorRules ();
608     staticInitialization ();
609     propagateDebug ();
610     loadAliases ();
611 
612     ////////////////////////////////////////////////////////////////////////////
613     //
614     // [7] Parse the command line.
615     //
616     ////////////////////////////////////////////////////////////////////////////
617 
618     for (int i = 0; i < argc; i++)
619       cli2.add (argv[i]);
620 
621     cli2.analyze ();
622 
623     // Extract a recomposed command line.
624     auto foundDefault = false;
625     auto foundAssumed = false;
626     std::string combined;
627     for (auto& a : cli2._args)
628     {
629       if (combined.length ())
630         combined += ' ';
631 
632       combined += a.attribute ("raw");
633 
634       if (a.hasTag ("DEFAULT"))
635         foundDefault = true;
636 
637       if (a.hasTag ("ASSUMED"))
638         foundAssumed = true;
639     }
640 
641     if (verbose ("default")) {
642       if (foundDefault)
643         header ("[" + combined + "]");
644 
645       if (foundAssumed)
646         header ("No command specified - assuming 'information'.");
647     }
648 
649     ////////////////////////////////////////////////////////////////////////////
650     //
651     // [8] Initialize hooks.
652     //
653     ////////////////////////////////////////////////////////////////////////////
654 
655     hooks.initialize ();
656   }
657 
658   catch (const std::string& message)
659   {
660     error (message);
661     rc = 2;
662   }
663 
664   catch (int)
665   {
666     // Hooks can terminate processing by throwing integers.
667     rc = 4;
668   }
669 
670   catch (...)
671   {
672     error ("Unknown error. Please report.");
673     rc = 3;
674   }
675 
676   // On initialization failure...
677   if (rc)
678   {
679     // Dump all debug messages, controlled by rc.debug.
680     if (config.getBoolean ("debug"))
681     {
682       for (auto& d : debugMessages)
683         if (color ())
684           std::cerr << colorizeDebug (d) << '\n';
685         else
686           std::cerr << d << '\n';
687     }
688 
689     // Dump all headers, controlled by 'header' verbosity token.
690     if (verbose ("header"))
691     {
692       for (auto& h : headers)
693         if (color ())
694           std::cerr << colorizeHeader (h) << '\n';
695         else
696           std::cerr << h << '\n';
697     }
698 
699     // Dump all footnotes, controlled by 'footnote' verbosity token.
700     if (verbose ("footnote"))
701     {
702       for (auto& f : footnotes)
703         if (color ())
704           std::cerr << colorizeFootnote (f) << '\n';
705         else
706           std::cerr << f << '\n';
707     }
708 
709     // Dump all errors, non-maskable.
710     // Colorized as footnotes.
711     for (auto& e : errors)
712       if (color ())
713         std::cerr << colorizeFootnote (e) << '\n';
714       else
715         std::cerr << e << '\n';
716   }
717 
718   time_init_us += timer_total.total_us ();
719   return rc;
720 }
721 
722 ////////////////////////////////////////////////////////////////////////////////
run()723 int Context::run ()
724 {
725   int rc;
726   std::string output;
727 
728   try
729   {
730     hooks.onLaunch ();
731     rc = dispatch (output);
732     tdb2.commit ();           // Harmless if called when nothing changed.
733     hooks.onExit ();          // No chance to update data.
734 
735     timer_total.stop ();
736     time_total_us += timer_total.total_us ();
737 
738     std::stringstream s;
739     s << "Perf "
740       << PACKAGE_STRING
741       << ' '
742 #ifdef HAVE_COMMIT
743       << COMMIT
744 #else
745       << '-'
746 #endif
747       << ' '
748       << Datetime ().toISO ()
749 
750       << " init:"   << time_init_us
751       << " load:"   << time_load_us
752       << " gc:"     << (time_gc_us > 0 ? time_gc_us - time_load_us : time_gc_us)
753       << " filter:" << time_filter_us
754       << " commit:" << time_commit_us
755       << " sort:"   << time_sort_us
756       << " render:" << time_render_us
757       << " hooks:"  << time_hooks_us
758       << " other:"  << time_total_us  -
759                        time_init_us   -
760                        time_gc_us     -
761                        time_filter_us -
762                        time_commit_us -
763                        time_sort_us   -
764                        time_render_us -
765                        time_hooks_us
766       << " total:"  << time_total_us
767       << '\n';
768     debug (s.str ());
769   }
770 
771   catch (const std::string& message)
772   {
773     error (message);
774     rc = 2;
775   }
776 
777   catch (int)
778   {
779     // Hooks can terminate processing by throwing integers.
780     rc = 4;
781   }
782 
783   catch (...)
784   {
785     error ("Unknown error. Please report.");
786     rc = 3;
787   }
788 
789   // Dump all debug messages, controlled by rc.debug.
790   if (config.getBoolean ("debug"))
791   {
792     for (auto& d : debugMessages)
793       if (color ())
794         std::cerr << colorizeDebug (d) << '\n';
795       else
796         std::cerr << d << '\n';
797   }
798 
799   // Dump all headers, controlled by 'header' verbosity token.
800   if (verbose ("header"))
801   {
802     for (auto& h : headers)
803       if (color ())
804         std::cerr << colorizeHeader (h) << '\n';
805       else
806         std::cerr << h << '\n';
807   }
808 
809   // Dump the report output.
810   std::cout << output;
811 
812   // Dump all footnotes, controlled by 'footnote' verbosity token.
813   if (verbose ("footnote"))
814   {
815     for (auto& f : footnotes)
816       if (color ())
817         std::cerr << colorizeFootnote (f) << '\n';
818       else
819         std::cerr << f << '\n';
820   }
821 
822   // Dump all errors, non-maskable.
823   // Colorized as footnotes.
824   for (auto& e : errors)
825     if (color ())
826       std::cerr << colorizeError (e) << '\n';
827     else
828       std::cerr << e << '\n';
829 
830   return rc;
831 }
832 
833 ////////////////////////////////////////////////////////////////////////////////
834 // Dispatch to the command found by the CLI parser.
dispatch(std::string & out)835 int Context::dispatch (std::string &out)
836 {
837   // Autocomplete args against keywords.
838   std::string command = cli2.getCommand ();
839   if (command != "")
840   {
841     updateXtermTitle ();
842     updateVerbosity ();
843 
844     Command* c = commands[command];
845     assert (c);
846 
847     // The command know whether they need a GC.
848     if (c->needs_gc () &&
849         ! tdb2.read_only ())
850     {
851       run_gc = config.getBoolean ("gc");
852       tdb2.gc ();
853     }
854     else
855     {
856       run_gc = false;
857     }
858 
859 /*
860     // Only read-only commands can be run when TDB2 is read-only.
861     // TODO Implement TDB2::read_only
862     if (tdb2.read_only () && !c->read_only ())
863       throw std::string ("");
864 */
865 
866     // This is something that is only needed for write commands with no other
867     // filter processing.
868     if (c->accepts_modifications () &&
869         ! c->accepts_filter ())
870     {
871       cli2.prepareFilter ();
872     }
873 
874     // With rc.debug.parser == 2, there are more tree dumps than you might want,
875     // but we need the rc.debug.parser == 1 case covered also, with the final
876     // tree.
877     if (config.getBoolean ("debug") &&
878         config.getInteger ("debug.parser") == 1)
879       debug (cli2.dump ("Parse Tree (before command-specifіc processing)"));
880 
881     return c->execute (out);
882   }
883 
884   assert (commands["help"]);
885   return commands["help"]->execute (out);
886 }
887 
888 ////////////////////////////////////////////////////////////////////////////////
getWidth()889 int Context::getWidth ()
890 {
891   // Determine window size.
892   auto width = config.getInteger ("defaultwidth");
893 
894   // A zero width value means 'infinity', which is approximated here by 2^16.
895   if (width == 0)
896     return 65536;
897 
898   if (config.getBoolean ("detection"))
899   {
900     if (terminal_width == 0 &&
901         terminal_height == 0)
902     {
903       unsigned short buff[4];
904       if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &buff) != -1)
905       {
906         terminal_height = buff[0];
907         terminal_width = buff[1];
908       }
909     }
910 
911     width = terminal_width;
912 
913     // Ncurses does this, and perhaps we need to as well, to avoid a problem on
914     // Cygwin where the display goes right up to the terminal width, and causes
915     // an odd color wrapping problem.
916     if (config.getBoolean ("avoidlastcolumn"))
917       --width;
918   }
919 
920   return width;
921 }
922 
923 ////////////////////////////////////////////////////////////////////////////////
getHeight()924 int Context::getHeight ()
925 {
926   // Determine window size.
927   auto height = config.getInteger ("defaultheight");
928 
929   // A zero height value means 'infinity', which is approximated here by 2^16.
930   if (height == 0)
931     return 65536;
932 
933   if (config.getBoolean ("detection"))
934   {
935     if (terminal_width == 0 &&
936         terminal_height == 0)
937     {
938       unsigned short buff[4];
939       if (ioctl (STDOUT_FILENO, TIOCGWINSZ, &buff) != -1)
940       {
941         terminal_height = buff[0];
942         terminal_width = buff[1];
943       }
944     }
945 
946     height = terminal_height;
947   }
948 
949   return height;
950 }
951 
952 ////////////////////////////////////////////////////////////////////////////////
getTaskContext(const std::string & kind,std::string name,bool fallback)953 std::string Context::getTaskContext (const std::string& kind, std::string name, bool fallback /* = true */)
954 {
955   // Consider currently selected context, if none specified
956   if (name.empty ())
957     name = config.get ("context");
958 
959   // Detect if any context is set, and bail out if not
960   if (! name.empty ())
961     debug (format ("Applying context '{1}'", name));
962   else
963   {
964     debug ("No context set");
965     return "";
966   }
967 
968   // Figure out the context string for this kind (read/write)
969   std::string contextString = "";
970 
971   if (! config.has ("context." + name + "." + kind) && kind == "read")
972   {
973     debug ("Specific " + kind + " context for '" + name + "' not defined. ");
974     if (fallback)
975     {
976       debug ("Trying to interpret old-style context definition as read context.");
977       contextString = config.get ("context." + name);
978     }
979   }
980   else
981     contextString = config.get ("context." + name + "." + kind);
982 
983   debug (format ("Detected context string: {1}", contextString.empty() ? "(empty)" : contextString));
984   return contextString;
985 }
986 
987 ////////////////////////////////////////////////////////////////////////////////
color()988 bool Context::color ()
989 {
990   if (determine_color_use)
991   {
992     // What the config says.
993     use_color = config.getBoolean ("color");
994 
995     // Only tty's support color.
996     if (! isatty (STDOUT_FILENO))
997     {
998       // No ioctl.
999       config.set ("detection", "off");
1000       config.set ("color",     "off");
1001 
1002       // Files don't get color.
1003       use_color = false;
1004     }
1005 
1006     // Override.
1007     if (config.getBoolean ("_forcecolor"))
1008     {
1009       config.set ("color", "on");
1010       use_color = true;
1011     }
1012 
1013     // No need to go through this again.
1014     determine_color_use = false;
1015   }
1016 
1017   // Cached result.
1018   return use_color;
1019 }
1020 
1021 ////////////////////////////////////////////////////////////////////////////////
1022 // Support verbosity levels:
1023 //
1024 //   rc.verbose=1          Show all feedback.
1025 //   rc.verbose=0          Show regular feedback.
1026 //   rc.verbose=nothing    Show the absolute minimum.
1027 //   rc.verbose=one,two    Show verbosity for 'one' and 'two' only.
1028 //
1029 // TODO This mechanism is clunky, and should slowly evolve into something more
1030 //      logical and consistent.  This should probably mean that 'nothing' should
1031 //      take the place of '0'.
verbose(const std::string & token)1032 bool Context::verbose (const std::string& token)
1033 {
1034   if (verbosity.empty ())
1035   {
1036     verbosity_legacy = config.getBoolean ("verbose");
1037     for (auto& token : split (config.get ("verbose"), ','))
1038       verbosity.insert (token);
1039 
1040     // Regular feedback means almost everything.
1041     // This odd test is to see if a Boolean-false value is a real one, which
1042     // means it is not 1/true/T/yes/on, but also should not be one of the
1043     // valid tokens either.
1044     if (! verbosity_legacy && ! verbosity.empty ())
1045     {
1046       std::string v = *(verbosity.begin ());
1047       if (v != "nothing"  &&
1048           v != "affected" &&  // This list must be complete.
1049           v != "blank"    &&  //
1050           v != "context"  &&  //
1051           v != "default"  &&  //
1052           v != "edit"     &&  //
1053           v != "filter"   &&  //
1054           v != "footnote" &&  //
1055           v != "header"   &&  //
1056           v != "label"    &&  //
1057           v != "new-id"   &&  //
1058           v != "new-uuid" &&  //
1059           v != "override" &&  //
1060           v != "project"  &&  //
1061           v != "recur"    &&  //
1062           v != "special"  &&  //
1063           v != "sync")
1064       {
1065         // This list emulates rc.verbose=off in version 1.9.4.
1066         verbosity = {"blank", "label", "new-id", "edit"};
1067       }
1068     }
1069 
1070     // Some flags imply "footnote" verbosity being active.  Make it so.
1071     if (! verbosity.count ("footnote"))
1072     {
1073       // TODO: Some of these may not use footnotes yet.  They should.
1074       for (auto flag : {"affected", "new-id", "new-uuid", "project", "override", "recur"})
1075       {
1076         if (verbosity.count (flag))
1077         {
1078           verbosity.insert ("footnote");
1079           break;
1080         }
1081       }
1082     }
1083 
1084     // Some flags imply "header" verbosity being active.  Make it so.
1085     if (! verbosity.count ("header"))
1086     {
1087       for (auto flag : {"default"})
1088       {
1089         if (verbosity.count (flag))
1090         {
1091           verbosity.insert ("header");
1092           break;
1093         }
1094       }
1095     }
1096   }
1097 
1098   // rc.verbose=true|y|yes|1|on overrides all.
1099   if (verbosity_legacy)
1100     return true;
1101 
1102   // rc.verbose=nothing overrides all.
1103   if (verbosity.size () == 1 &&
1104       *(verbosity.begin ()) == "nothing")
1105     return false;
1106 
1107   // Specific token match.
1108   if (verbosity.count (token))
1109     return true;
1110 
1111   return false;
1112 }
1113 
1114 ////////////////////////////////////////////////////////////////////////////////
getColumns() const1115 const std::vector <std::string> Context::getColumns () const
1116 {
1117   std::vector <std::string> output;
1118   for (auto& col : columns)
1119     output.push_back (col.first);
1120 
1121   return output;
1122 }
1123 
1124 ////////////////////////////////////////////////////////////////////////////////
1125 // A value of zero mean unlimited.
1126 // A value of 'page' means however many screen lines there are.
1127 // A value of a positive integer is a row/task limit.
getLimits(int & rows,int & lines)1128 void Context::getLimits (int& rows, int& lines)
1129 {
1130   rows = 0;
1131   lines = 0;
1132 
1133   // This is an integer specified as a filter (limit:10).
1134   auto limit = config.get ("limit");
1135   if (limit != "")
1136   {
1137     if (limit == "page")
1138     {
1139       rows = 0;
1140       lines = getHeight ();
1141     }
1142     else
1143     {
1144       rows = (int) strtol (limit.c_str (), nullptr, 10);
1145       lines = 0;
1146     }
1147   }
1148 }
1149 
1150 ////////////////////////////////////////////////////////////////////////////////
1151 // The 'Task' object, among others, is shared between projects.  To make this
1152 // easier, it has been decoupled from Context.
staticInitialization()1153 void Context::staticInitialization ()
1154 {
1155   CLI2::minimumMatchLength           = config.getInteger ("abbreviation.minimum");
1156   Lexer::minimumMatchLength          = config.getInteger ("abbreviation.minimum");
1157 
1158   Task::defaultProject               = config.get ("default.project");
1159   Task::defaultDue                   = config.get ("default.due");
1160   Task::defaultScheduled             = config.get ("default.scheduled");
1161 
1162   Task::searchCaseSensitive          = Variant::searchCaseSensitive = config.getBoolean ("search.case.sensitive");
1163   Task::regex                        = Variant::searchUsingRegex    = config.getBoolean ("regex");
1164   Lexer::dateFormat                  = Variant::dateFormat          = config.get ("dateformat");
1165 
1166   Datetime::isoEnabled               = config.getBoolean ("date.iso");
1167   Datetime::standaloneDateEnabled    = false;
1168   Datetime::standaloneTimeEnabled    = false;
1169   Duration::standaloneSecondsEnabled = false;
1170 
1171   TDB2::debug_mode                   = config.getBoolean ("debug");
1172 
1173   for (auto& rc : config)
1174   {
1175     if (rc.first.substr (0, 4) == "uda." &&
1176         rc.first.substr (rc.first.length () - 7, 7) == ".values")
1177     {
1178       std::string name = rc.first.substr (4, rc.first.length () - 7 - 4);
1179       auto values = split (rc.second, ',');
1180 
1181       for (auto r = values.rbegin(); r != values.rend (); ++r)
1182         Task::customOrder[name].push_back (*r);
1183     }
1184   }
1185 
1186   for (auto& col : columns)
1187   {
1188     Task::attributes[col.first] = col.second->type ();
1189     Lexer::attributes[col.first] = col.second->type ();
1190   }
1191 
1192   Task::urgencyProjectCoefficient     = config.getReal ("urgency.project.coefficient");
1193   Task::urgencyActiveCoefficient      = config.getReal ("urgency.active.coefficient");
1194   Task::urgencyScheduledCoefficient   = config.getReal ("urgency.scheduled.coefficient");
1195   Task::urgencyWaitingCoefficient     = config.getReal ("urgency.waiting.coefficient");
1196   Task::urgencyBlockedCoefficient     = config.getReal ("urgency.blocked.coefficient");
1197   Task::urgencyAnnotationsCoefficient = config.getReal ("urgency.annotations.coefficient");
1198   Task::urgencyTagsCoefficient        = config.getReal ("urgency.tags.coefficient");
1199   Task::urgencyDueCoefficient         = config.getReal ("urgency.due.coefficient");
1200   Task::urgencyBlockingCoefficient    = config.getReal ("urgency.blocking.coefficient");
1201   Task::urgencyAgeCoefficient         = config.getReal ("urgency.age.coefficient");
1202   Task::urgencyAgeMax                 = config.getReal ("urgency.age.max");
1203 
1204   // Tag- and project-specific coefficients.
1205   for (auto& var : config.all ())
1206     if (var.substr (0, 13) == "urgency.user." ||
1207         var.substr (0, 12) == "urgency.uda.")
1208       Task::coefficients[var] = config.getReal (var);
1209 }
1210 
1211 ////////////////////////////////////////////////////////////////////////////////
createDefaultConfig()1212 void Context::createDefaultConfig ()
1213 {
1214   // Do we need to create a default rc?
1215   if (rc_file._data != "" && ! rc_file.exists ())
1216   {
1217     if (config.getBoolean ("confirmation") &&
1218         ! confirm ( format ("A configuration file could not be found in {1}\n\nWould you like a sample {2} created, so Taskwarrior can proceed?", home_dir, rc_file._data)))
1219       throw std::string ("Cannot proceed without rc file.");
1220 
1221     // Override data.location in the defaults.
1222     auto loc = configurationDefaults.find ("data.location=~/.task");
1223     //                                 loc+0^          +14^   +21^
1224 
1225     Datetime now;
1226     std::stringstream contents;
1227     contents << "# [Created by "
1228              << PACKAGE_STRING
1229              << ' '
1230              << now.toString ("m/d/Y H:N:S")
1231              << "]\n"
1232              << configurationDefaults.substr (0, loc + 14)
1233              << data_dir._original
1234              << "\n\n# To use the default location of the XDG directories,\n"
1235              << "# move this configuration file from ~/.taskrc to ~/.config/task/taskrc and uncomment below\n"
1236              << "\n#data.location=~/.local/share/task\n"
1237              << "#hooks.location=~/.config/task/hooks\n"
1238              << "\n# Color theme (uncomment one to use)\n"
1239              << "#include light-16.theme\n"
1240              << "#include light-256.theme\n"
1241              << "#include dark-16.theme\n"
1242              << "#include dark-256.theme\n"
1243              << "#include dark-red-256.theme\n"
1244              << "#include dark-green-256.theme\n"
1245              << "#include dark-blue-256.theme\n"
1246              << "#include dark-violets-256.theme\n"
1247              << "#include dark-yellow-green.theme\n"
1248              << "#include dark-gray-256.theme\n"
1249              << "#include dark-gray-blue-256.theme\n"
1250              << "#include solarized-dark-256.theme\n"
1251              << "#include solarized-light-256.theme\n"
1252              << "#include no-color.theme\n"
1253              << '\n';
1254 
1255     // Write out the new file.
1256     if (! File::write (rc_file._data, contents.str ()))
1257       throw format ("Could not write to '{1}'.", rc_file._data);
1258   }
1259 
1260   // Create data location, if necessary.
1261   Directory d (data_dir);
1262   if (! d.exists ())
1263   {
1264     if (config.getBoolean ("exit.on.missing.db"))
1265       throw std::string ("Error: rc.data.location does not exist - exiting according to rc.exit.on.missing.db setting.");
1266 
1267     d.create ();
1268 
1269     if (config.has ("hooks.location"))
1270       d = Directory (config.get ("hooks.location"));
1271     else
1272       d += "hooks";
1273 
1274     d.create ();
1275   }
1276 }
1277 
1278 ////////////////////////////////////////////////////////////////////////////////
decomposeSortField(const std::string & field,std::string & key,bool & ascending,bool & breakIndicator)1279 void Context::decomposeSortField (
1280   const std::string& field,
1281   std::string& key,
1282   bool& ascending,
1283   bool& breakIndicator)
1284 {
1285   int length = field.length ();
1286 
1287   int decoration = 1;
1288   breakIndicator = false;
1289   if (field[length - decoration] == '/')
1290   {
1291     breakIndicator = true;
1292     ++decoration;
1293   }
1294 
1295   if (field[length - decoration] == '+')
1296   {
1297     ascending = true;
1298     key = field.substr (0, length - decoration);
1299   }
1300   else if (field[length - decoration] == '-')
1301   {
1302     ascending = false;
1303     key = field.substr (0, length - decoration);
1304   }
1305   else
1306   {
1307     ascending = true;
1308     key = field;
1309   }
1310 }
1311 
1312 ////////////////////////////////////////////////////////////////////////////////
debugTiming(const std::string & details,const Timer & timer)1313 void Context::debugTiming (const std::string& details, const Timer& timer)
1314 {
1315   std::stringstream out;
1316   out << "Timer "
1317       << details
1318       << ' '
1319       << std::setprecision (6)
1320       << std::fixed
1321       << timer.total_us () / 1.0e6
1322       << " sec";
1323   debug (out.str ());
1324 }
1325 
1326 ////////////////////////////////////////////////////////////////////////////////
1327 // This capability is to answer the question of 'what did I just do to generate
1328 // this output?'.
updateXtermTitle()1329 void Context::updateXtermTitle ()
1330 {
1331   if (config.getBoolean ("xterm.title") && isatty (STDOUT_FILENO))
1332   {
1333     auto command = cli2.getCommand ();
1334     std::string title;
1335 
1336     for (auto a = cli2._args.begin (); a != cli2._args.end (); ++a)
1337     {
1338       if (a != cli2._args.begin ())
1339         title += ' ';
1340 
1341       title += a->attribute ("raw");
1342     }
1343 
1344     std::cout << "]0;task " << command << ' ' << title << "";
1345   }
1346 }
1347 
1348 ////////////////////////////////////////////////////////////////////////////////
1349 // This function allows a clean output if the command is a helper subcommand.
updateVerbosity()1350 void Context::updateVerbosity ()
1351 {
1352   auto command = cli2.getCommand ();
1353   if (command != "" &&
1354       command[0] == '_')
1355   {
1356     verbosity = {"nothing"};
1357   }
1358 }
1359 
1360 ////////////////////////////////////////////////////////////////////////////////
loadAliases()1361 void Context::loadAliases ()
1362 {
1363   for (auto& i : config)
1364     if (i.first.substr (0, 6) == "alias.")
1365       cli2.alias (i.first.substr (6), i.second);
1366 }
1367 
1368 ////////////////////////////////////////////////////////////////////////////////
1369 // Using the general rc.debug setting automaticalls sets debug.tls, debug.hooks
1370 // and debug.parser, unless they already have values, which by default they do
1371 // not.
propagateDebug()1372 void Context::propagateDebug ()
1373 {
1374   if (config.getBoolean ("debug"))
1375   {
1376     if (! config.has ("debug.tls"))
1377       config.set ("debug.tls", 2);
1378 
1379     if (! config.has ("debug.hooks"))
1380       config.set ("debug.hooks", 1);
1381 
1382     if (! config.has ("debug.parser"))
1383       config.set ("debug.parser", 1);
1384   }
1385   else
1386   {
1387     if ((config.has ("debug.hooks")  && config.getInteger ("debug.hooks")) ||
1388         (config.has ("debug.parser") && config.getInteger ("debug.parser")) )
1389       config.set ("debug", true);
1390   }
1391 }
1392 
1393 ////////////////////////////////////////////////////////////////////////////////
1394 // No duplicates.
header(const std::string & input)1395 void Context::header (const std::string& input)
1396 {
1397   if (input.length () &&
1398       std::find (headers.begin (), headers.end (), input) == headers.end ())
1399     headers.push_back (input);
1400 }
1401 
1402 ////////////////////////////////////////////////////////////////////////////////
1403 // No duplicates.
footnote(const std::string & input)1404 void Context::footnote (const std::string& input)
1405 {
1406   if (input.length () &&
1407       std::find (footnotes.begin (), footnotes.end (), input) == footnotes.end ())
1408     footnotes.push_back (input);
1409 }
1410 
1411 ////////////////////////////////////////////////////////////////////////////////
1412 // No duplicates.
error(const std::string & input)1413 void Context::error (const std::string& input)
1414 {
1415   if (input.length () &&
1416       std::find (errors.begin (), errors.end (), input) == errors.end ())
1417     errors.push_back (input);
1418 }
1419 
1420 ////////////////////////////////////////////////////////////////////////////////
debug(const std::string & input)1421 void Context::debug (const std::string& input)
1422 {
1423   if (input.length ())
1424     debugMessages.push_back (input);
1425 }
1426 
1427 ////////////////////////////////////////////////////////////////////////////////
1428 
1429 // vim ts=2:sw=2
1430