• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GothenburgBitFactory / taskwarrior / 10336191094

11 Aug 2024 02:06AM UTC coverage: 84.973% (+0.2%) from 84.733%
10336191094

push

github

web-flow
Use TaskChampion 0.7.0, now via cxx instead of hand-rolled FFI (#3588)

TC 0.7.0 introduces a new `TaskData` type that maps to Taskwarrior's
`Task` type more cleanly. It also introduces the idea of gathering lists
of operations and "committing" them to a replica.

A consequence of this change is that TaskChampion no longer
automatically maintains dependency information, so Taskwarrior must do
so, with its `TDB2::dependency_sync` method. This method does a very
similar thing to what TaskChampion had been doing, so this is a shift of
responsibility but not a major performance difference.

Cxx is .. not great. It is missing a lot of useful things that make a
general-purpose bridge impractical:

 - no support for trait objects
 - no support for `Option<T>` (https://github.com/dtolnay/cxx/issues/87)
 - no support for `Vec<Box<..>>`

As a result, some creativity is required in writing the bridge, for
example returning a `Vec<OptionTaskData>` from `all_task_data` to allow
individual `TaskData` values to be "taken" from the vector.

That said, Cxx is the current state-of-the-art, and does a good job of
ensuring memory safety, at the cost of some slightly awkward APIs.

Subsequent work can remove the "TDB2" layer and allow commands and other
parts of Taskwarrior to interface directly with the `Replica`.

255 of 279 new or added lines in 7 files covered. (91.4%)

7 existing lines in 4 files now uncovered.

19011 of 22373 relevant lines covered (84.97%)

23094.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.29
/src/Context.cpp
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
// cmake.h include header must come first
29

30
#include <Context.h>
31
#include <Datetime.h>
32
#include <Duration.h>
33
#include <Eval.h>
34
#include <FS.h>
35
#include <Variant.h>
36
#include <Version.h>
37
#include <assert.h>
38
#include <format.h>
39
#include <main.h>
40
#include <rust/cxx.h>
41
#include <shared.h>
42
#include <stdlib.h>
43
#include <string.h>
44
#include <taskchampion-cpp/lib.h>
45
#include <unistd.h>
46

47
#include <algorithm>
48
#include <fstream>
49
#include <iomanip>
50
#include <iostream>
51
#include <regex>
52
#include <sstream>
53

54
#ifdef HAVE_COMMIT
55
#include <commit.h>
56
#endif
57

58
#include <stdio.h>
59
#include <sys/ioctl.h>
60

61
#ifdef SOLARIS
62
#include <sys/termios.h>
63
#endif
64

65
////////////////////////////////////////////////////////////////////////////////
66
// This string is parsed and used as default values for configuration.
67
// Note: New configuration options should be added to the vim syntax file in
68
// scripts/vim/syntax/taskrc.vim
69
std::string configurationDefaults =
70
    "# Taskwarrior program configuration file.\n"
71
    "# For more documentation, see https://taskwarrior.org or try 'man task', 'man task-color',\n"
72
    "# 'man task-sync' or 'man taskrc'\n"
73
    "\n"
74
    "# Here is an example of entries that use the default, override and blank values\n"
75
    "#   variable=foo   -- By specifying a value, this overrides the default\n"
76
    "#   variable=      -- By specifying no value, this means no default\n"
77
    "#   #variable=foo  -- By commenting out the line, or deleting it, this uses the default\n"
78
    "\n"
79
    "# You can also refence environment variables:\n"
80
    "#   variable=$HOME/task\n"
81
    "#   variable=$VALUE\n"
82
    "\n"
83
    "# Use the command 'task show' to see all defaults and overrides\n"
84
    "\n"
85
    "# Files\n"
86
    "data.location=~/.task\n"
87
    "gc=1                                           # Garbage-collect data files - DO NOT CHANGE "
88
    "unless you are sure\n"
89
    "exit.on.missing.db=0                           # Whether to exit if ~/.task is not found\n"
90
    "hooks=1                                        # Master control switch for hooks\n"
91
    "\n"
92
    "# Terminal\n"
93
    "detection=1                                    # Detects terminal width\n"
94
    "defaultwidth=80                                # Without detection, assumed width\n"
95
    "defaultheight=24                               # Without detection, assumed height\n"
96
    "avoidlastcolumn=0                              # Fixes Cygwin width problem\n"
97
    "hyphenate=1                                    # Hyphenates lines wrapped on non-word-breaks\n"
98
    "#editor=vi                                     # Preferred text editor\n"
99
    "reserved.lines=1                               # Assume a 1-line prompt\n"
100
    "\n"
101
    "# Miscellaneous\n"
102
    "# verbose=                                     # Comma-separated list.  May contain any "
103
    "subset of:\n"
104
    "# "
105
    "affected,blank,context,default,edit,filter,footnote,header,label,new-id,new-uuid,news,"
106
    "override,project,recur,special,sync\n"
107
    "verbose=affected,blank,context,edit,header,footnote,label,new-id,news,project,special,sync,"
108
    "override,recur\n"
109
    "confirmation=1                                 # Confirmation on delete, big changes\n"
110
    "recurrence=1                                   # Enable recurrence\n"
111
    "recurrence.confirmation=prompt                 # Confirmation for propagating changes among "
112
    "recurring tasks (yes/no/prompt)\n"
113
    "allow.empty.filter=1                           # An empty filter gets a warning and requires "
114
    "confirmation\n"
115
    "indent.annotation=2                            # Indent spaces for annotations\n"
116
    "indent.report=0                                # Indent spaces for whole report\n"
117
    "row.padding=0                                  # Left and right padding for each row of "
118
    "report\n"
119
    "column.padding=1                               # Spaces between each column in a report\n"
120
    "bulk=3                                         # 3 or more tasks considered a bulk change and "
121
    "is confirmed\n"
122
    "nag=You have more urgent tasks.                # Nag message to keep you honest\n"  // TODO
123
    "search.case.sensitive=1                        # Setting to no allows case insensitive "
124
    "searches\n"
125
    "active.indicator=*                             # What to show as an active task indicator\n"
126
    "tag.indicator=+                                # What to show as a tag indicator\n"
127
    "dependency.indicator=D                         # What to show as a dependency indicator\n"
128
    "recurrence.indicator=R                         # What to show as a task recurrence indicator\n"
129
    "recurrence.limit=1                             # Number of future recurring pending tasks\n"
130
    "undo.style=side                                # Undo style - can be 'side', or 'diff'\n"
131
    "regex=1                                        # Assume all search/filter strings are "
132
    "regexes\n"
133
    "xterm.title=0                                  # Sets xterm title for some commands\n"
134
    "expressions=infix                              # Prefer infix over postfix expressions\n"
135
    "json.array=1                                   # Enclose JSON output in [ ]\n"
136
    "abbreviation.minimum=2                         # Shortest allowed abbreviation\n"
137
    "news.version=                                  # Latest version highlights read by the user\n"
138
    "purge.on-sync=0                                # Purge old tasks on sync\n"
139
    "\n"
140
    "# Dates\n"
141
    "dateformat=Y-M-D                               # Preferred input and display date format\n"
142
    "dateformat.holiday=YMD                         # Preferred input date format for holidays\n"
143
    "dateformat.edit=Y-M-D H:N:S                    # Preferred display date format when editing\n"
144
    "dateformat.info=Y-M-D H:N:S                    # Preferred display date format for "
145
    "information\n"
146
    "dateformat.report=                             # Preferred display date format for reports\n"
147
    "dateformat.annotation=                         # Preferred display date format for "
148
    "annotations\n"
149
    "date.iso=1                                     # Enable ISO date support\n"
150
    "weekstart=sunday                               # Sunday or Monday only\n"
151
    "displayweeknumber=1                            # Show week numbers on calendar\n"
152
    "due=7                                          # Task is considered due in 7 days\n"
153
    "\n"
154
    "# Calendar controls\n"
155
    "calendar.legend=1                              # Display the legend on calendar\n"
156
    "calendar.details=sparse                        # Calendar shows information for tasks w/due "
157
    "dates: full, sparse or none\n"
158
    "calendar.details.report=list                   # Report to use when showing task information "
159
    "in cal\n"
160
    "calendar.offset=0                              # Apply an offset value to control the first "
161
    "month of the calendar\n"
162
    "calendar.offset.value=-1                       # The number of months the first month of the "
163
    "calendar is moved\n"
164
    "calendar.holidays=none                         # Show public holidays on calendar:full, "
165
    "sparse or none\n"
166
    "#calendar.monthsperline=3                      # Number of calendar months on a line\n"
167
    "\n"
168
    "# Journal controls\n"
169
    "journal.time=0                                 # Record start/stop commands as annotation\n"
170
    "journal.time.start.annotation=Started task     # Annotation description for the start journal "
171
    "entry\n"
172
    "journal.time.stop.annotation=Stopped task      # Annotation description for the stop  journal "
173
    "entry\n"
174
    "journal.info=1                                 # Display task journal with info command\n"
175
    "\n"
176
    "# Dependency controls\n"
177
    "dependency.reminder=1                          # Nags on dependency chain violations\n"
178
    "dependency.confirmation=1                      # Should dependency chain repair be "
179
    "confirmed?\n"
180
    "\n"
181
    "# Urgency Coefficients\n"
182
    "urgency.user.tag.next.coefficient=15.0         # Urgency coefficient for 'next' special tag\n"
183
    "urgency.due.coefficient=12.0                   # Urgency coefficient for due dates\n"
184
    "urgency.blocking.coefficient=8.0               # Urgency coefficient for blocking tasks\n"
185
    "urgency.active.coefficient=4.0                 # Urgency coefficient for active tasks\n"
186
    "urgency.scheduled.coefficient=5.0              # Urgency coefficient for scheduled tasks\n"
187
    "urgency.age.coefficient=2.0                    # Urgency coefficient for age\n"
188
    "urgency.annotations.coefficient=1.0            # Urgency coefficient for annotations\n"
189
    "urgency.tags.coefficient=1.0                   # Urgency coefficient for tags\n"
190
    "urgency.project.coefficient=1.0                # Urgency coefficient for projects\n"
191
    "urgency.blocked.coefficient=-5.0               # Urgency coefficient for blocked tasks\n"
192
    "urgency.waiting.coefficient=-3.0               # Urgency coefficient for waiting status\n"
193
    "urgency.inherit=0                              # Recursively inherit highest urgency value "
194
    "from blocked tasks\n"
195
    "urgency.age.max=365                            # Maximum age in days\n"
196
    "\n"
197
    "#urgency.user.project.foo.coefficient=5.0      # Urgency coefficients for 'foo' project\n"
198
    "#urgency.user.tag.foo.coefficient=5.0          # Urgency coefficients for 'foo' tag\n"
199
    "#urgency.uda.foo.coefficient=5.0               # Urgency coefficients for UDA 'foo'\n"
200
    "\n"
201
    "# Color controls.\n"
202
    "color=1                                        # Enable color\n"
203
    "\n"
204
    "# Here is the rule precedence order, highest to lowest.\n"
205
    "# Note that these are just the color rule names, without the leading 'color.'\n"
206
    "#      and any trailing '.value'.\n"
207
    "rule.precedence.color=deleted,completed,active,keyword.,tag.,project.,overdue,scheduled,due."
208
    "today,due,blocked,blocking,recurring,tagged,uda.\n"
209
    "\n"
210
    "# General decoration\n"
211
    "rule.color.merge=1\n"
212
    "color.label=\n"
213
    "color.label.sort=\n"
214
    "color.alternate=on gray2\n"
215
    "color.header=color3\n"
216
    "color.footnote=color3\n"
217
    "color.warning=bold red\n"
218
    "color.error=white on red\n"
219
    "color.debug=color4\n"
220
    "\n"
221
    "# Task state\n"
222
    "color.completed=\n"
223
    "color.deleted=\n"
224
    "color.active=rgb555 on rgb410\n"
225
    "color.recurring=rgb013\n"
226
    "color.scheduled=on rgb001\n"
227
    "color.until=\n"
228
    "color.blocked=white on color8\n"
229
    "color.blocking=black on color15\n"
230
    "\n"
231
    "# Project\n"
232
    "color.project.none=\n"
233
    "\n"
234
    "# Priority UDA\n"
235
    "color.uda.priority.H=color255\n"
236
    "color.uda.priority.L=color245\n"
237
    "color.uda.priority.M=color250\n"
238
    "\n"
239
    "# Tags\n"
240
    "color.tag.next=rgb440\n"
241
    "color.tag.none=\n"
242
    "color.tagged=rgb031\n"
243
    "\n"
244
    "# Due\n"
245
    "color.due.today=rgb400\n"
246
    "color.due=color1\n"
247
    "color.overdue=color9\n"
248
    "\n"
249
    "# Report: burndown\n"
250
    "color.burndown.done=on rgb010\n"
251
    "color.burndown.pending=on color9\n"
252
    "color.burndown.started=on color11\n"
253
    "\n"
254
    "# Report: history\n"
255
    "color.history.add=color0 on rgb500\n"
256
    "color.history.delete=color0 on rgb550\n"
257
    "color.history.done=color0 on rgb050\n"
258
    "\n"
259
    "# Report: summary\n"
260
    "color.summary.background=white on color0\n"
261
    "color.summary.bar=black on rgb141\n"
262
    "\n"
263
    "# Command: calendar\n"
264
    "color.calendar.due.today=color15 on color1\n"
265
    "color.calendar.due=color0 on color1\n"
266
    "color.calendar.holiday=color0 on color11\n"
267
    "color.calendar.scheduled=rgb013 on color15\n"
268
    "color.calendar.overdue=color0 on color9\n"
269
    "color.calendar.today=color15 on rgb013\n"
270
    "color.calendar.weekend=on color235\n"
271
    "color.calendar.weeknumber=rgb013\n"
272
    "\n"
273
    "# Command: sync\n"
274
    "color.sync.added=rgb010\n"
275
    "color.sync.changed=color11\n"
276
    "color.sync.rejected=color9\n"
277
    "\n"
278
    "# Command: undo\n"
279
    "color.undo.after=color2\n"
280
    "color.undo.before=color1\n"
281
    "\n"
282
    "# UDA priority\n"
283
    "uda.priority.type=string                       # UDA priority is a string type\n"
284
    "uda.priority.label=Priority                    # UDA priority has a display label'\n"
285
    "uda.priority.values=H,M,L,                     # UDA priority values are 'H', 'M', 'L' or ''\n"
286
    "                                               # UDA priority sorting is 'H' > 'M' > 'L' > '' "
287
    "(highest to lowest)\n"
288
    "#uda.priority.default=M                        # UDA priority default value of 'M'\n"
289
    "urgency.uda.priority.H.coefficient=6.0         # UDA priority coefficient for value 'H'\n"
290
    "urgency.uda.priority.M.coefficient=3.9         # UDA priority coefficient for value 'M'\n"
291
    "urgency.uda.priority.L.coefficient=1.8         # UDA priority coefficient for value 'L'\n"
292
    "\n"
293
    "#default.project=foo                           # Default project for 'add' command\n"
294
    "#default.due=eom                               # Default due date for 'add' command\n"
295
    "#default.scheduled=eom                         # Default scheduled date for 'add' command\n"
296
    "default.command=next                           # When no arguments are specified\n"
297
    "default.timesheet.filter=( +PENDING and start.after:now-4wks ) or ( +COMPLETED and "
298
    "end.after:now-4wks )\n"
299
    "\n"
300
    "_forcecolor=0                                  # Forces color to be on, even for non TTY "
301
    "output\n"
302
    "complete.all.tags=0                            # Include old tag names in '_ags' command\n"
303
    "list.all.projects=0                            # Include old project names in 'projects' "
304
    "command\n"
305
    "summary.all.projects=0                         # Include old project names in 'summary' "
306
    "command\n"
307
    "list.all.tags=0                                # Include old tag names in 'tags' command\n"
308
    "print.empty.columns=0                          # Print columns which have no data for any "
309
    "task\n"
310
    "debug=0                                        # Display diagnostics\n"
311
    "sugar=1                                        # Syntactic sugar\n"
312
    "obfuscate=0                                    # Obfuscate data for error reporting\n"
313
    "fontunderline=1                                # Uses underlines rather than -------\n"
314
    "\n"
315
    "# WARNING: Please read the documentation (man task-sync) before setting up\n"
316
    "#          Taskwarrior for Taskserver synchronization.\n"
317
    "\n"
318
    "#sync.encryption_secret                        # Encryption secret for sync to a server\n"
319
    "#sync.server.client_id                         # Client ID for sync to a server\n"
320
    "#sync.server.url                               # URL of the sync server\n"
321
    "#sync.local.server_dir                         # Directory for local sync\n"
322
    "#sync.gcp.credential_path                      # Path to JSON file containing credentials to "
323
    "authenticate GCP Sync\n"
324
    "#sync.gcp.bucket                               # Bucket for sync to GCP\n"
325
    "\n"
326
    "# Aliases - alternate names for commands\n"
327
    "alias.rm=delete                                # Alias for the delete command\n"
328
    "alias.history=history.monthly                  # Prefer monthly over annual history reports\n"
329
    "alias.ghistory=ghistory.monthly                # Prefer monthly graphical over annual history "
330
    "reports\n"
331
    "alias.burndown=burndown.weekly                 # Prefer the weekly burndown chart\n"
332
    "\n"
333
    "# Reports\n"
334
    "\n"
335
    "report.long.description=All details of tasks\n"
336
    "report.long.labels=ID,A,Created,Mod,Deps,P,Project,Tags,Recur,Wait,Sched,Due,Until,"
337
    "Description\n"
338
    "report.long.columns=id,start.active,entry,modified.age,depends,priority,project,tags,recur,"
339
    "wait.remaining,scheduled,due,until,description\n"
340
    "report.long.filter=status:pending -WAITING\n"
341
    "report.long.sort=modified-\n"
342
    "report.long.context=1\n"
343
    "\n"
344
    "report.list.description=Most details of tasks\n"
345
    "report.list.labels=ID,Active,Age,D,P,Project,Tags,R,Sch,Due,Until,Description,Urg\n"
346
    "report.list.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur."
347
    "indicator,scheduled.countdown,due,until.remaining,description.count,urgency\n"
348
    "report.list.filter=status:pending -WAITING\n"
349
    "report.list.sort=start-,due+,project+,urgency-\n"
350
    "report.list.context=1\n"
351
    "\n"
352
    "report.ls.description=Few details of tasks\n"
353
    "report.ls.labels=ID,A,D,Project,Tags,R,Wait,S,Due,Until,Description\n"
354
    "report.ls.columns=id,start.active,depends.indicator,project,tags,recur.indicator,wait."
355
    "remaining,scheduled.countdown,due.countdown,until.countdown,description.count\n"
356
    "report.ls.filter=status:pending -WAITING\n"
357
    "report.ls.sort=start-,description+\n"
358
    "report.ls.context=1\n"
359
    "\n"
360
    "report.minimal.description=Minimal details of tasks\n"
361
    "report.minimal.labels=ID,Project,Tags,Description\n"
362
    "report.minimal.columns=id,project,tags.count,description.count\n"
363
    "report.minimal.filter=status:pending -WAITING\n"
364
    "report.minimal.sort=project+/,description+\n"
365
    "report.minimal.context=1\n"
366
    "\n"
367
    "report.newest.description=Newest tasks\n"
368
    "report.newest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,"
369
    "Description\n"
370
    "report.newest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,"
371
    "project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
372
    "report.newest.filter=status:pending -WAITING\n"
373
    "report.newest.sort=entry-\n"
374
    "report.newest.context=1\n"
375
    "\n"
376
    "report.oldest.description=Oldest tasks\n"
377
    "report.oldest.labels=ID,Active,Created,Age,Mod,D,P,Project,Tags,R,Wait,Sch,Due,Until,"
378
    "Description\n"
379
    "report.oldest.columns=id,start.age,entry,entry.age,modified.age,depends.indicator,priority,"
380
    "project,tags,recur.indicator,wait.remaining,scheduled.countdown,due,until.age,description\n"
381
    "report.oldest.filter=status:pending -WAITING\n"
382
    "report.oldest.sort=entry+\n"
383
    "report.oldest.context=1\n"
384
    "\n"
385
    "report.overdue.description=Overdue tasks\n"
386
    "report.overdue.labels=ID,Active,Age,Deps,P,Project,Tag,R,S,Due,Until,Description,Urg\n"
387
    "report.overdue.columns=id,start.age,entry.age,depends,priority,project,tags,recur.indicator,"
388
    "scheduled.countdown,due,until,description,urgency\n"
389
    "report.overdue.filter=status:pending -WAITING +OVERDUE\n"
390
    "report.overdue.sort=urgency-,due+\n"
391
    "report.overdue.context=1\n"
392
    "\n"
393
    "report.active.description=Active tasks\n"
394
    "report.active.labels=ID,Started,Active,Age,D,P,Project,Tags,Recur,W,Sch,Due,Until,"
395
    "Description\n"
396
    "report.active.columns=id,start,start.age,entry.age,depends.indicator,priority,project,tags,"
397
    "recur,wait,scheduled.remaining,due,until,description\n"
398
    "report.active.filter=status:pending -WAITING +ACTIVE\n"
399
    "report.active.sort=project+,start+\n"
400
    "report.active.context=1\n"
401
    "\n"
402
    "report.completed.description=Completed tasks\n"
403
    "report.completed.labels=ID,UUID,Created,Completed,Age,Deps,P,Project,Tags,R,Due,Description\n"
404
    "report.completed.columns=id,uuid.short,entry,end,entry.age,depends,priority,project,tags,"
405
    "recur.indicator,due,description\n"
406
    "report.completed.filter=status:completed -WAITING \n"
407
    "report.completed.sort=end+\n"
408
    "report.completed.context=1\n"
409
    "\n"
410
    "report.recurring.description=Recurring Tasks\n"
411
    "report.recurring.labels=ID,Active,Age,D,P,Parent,Project,Tags,Recur,Sch,Due,Until,Description,"
412
    "Urg\n"
413
    "report.recurring.columns=id,start.age,entry.age,depends.indicator,priority,parent.short,"
414
    "project,tags,recur,scheduled.countdown,due,until.remaining,description,urgency\n"
415
    "report.recurring.filter=(status:pending -WAITING +CHILD) or (status:recurring -WAITING "
416
    "+PARENT)\n"
417
    "report.recurring.sort=due+,urgency-,entry+\n"
418
    "report.recurring.context=1\n"
419
    "\n"
420
    "report.waiting.description=Waiting (hidden) tasks\n"
421
    "report.waiting.labels=ID,A,Age,D,P,Project,Tags,R,Wait,Remaining,Sched,Due,Until,Description\n"
422
    "report.waiting.columns=id,start.active,entry.age,depends.indicator,priority,project,tags,"
423
    "recur.indicator,wait,wait.remaining,scheduled,due,until,description\n"
424
    "report.waiting.filter=+WAITING\n"
425
    "report.waiting.sort=due+,wait+,entry+\n"
426
    "report.waiting.context=1\n"
427
    "\n"
428
    "report.all.description=All tasks\n"
429
    "report.all.labels=ID,St,UUID,A,Age,Done,D,P,Project,Tags,R,Wait,Sch,Due,Until,Description\n"
430
    "report.all.columns=id,status.short,uuid.short,start.active,entry.age,end.age,depends."
431
    "indicator,priority,project.parent,tags.count,recur.indicator,wait.remaining,scheduled."
432
    "remaining,due,until.remaining,description\n"
433
    "report.all.sort=entry-\n"
434
    "report.all.context=1\n"
435
    "\n"
436
    "report.next.description=Most urgent tasks\n"
437
    "report.next.labels=ID,Active,Age,Deps,P,Project,Tag,Recur,S,Due,Until,Description,Urg\n"
438
    "report.next.columns=id,start.age,entry.age,depends,priority,project,tags,recur,scheduled."
439
    "countdown,due.relative,until.remaining,description,urgency\n"
440
    "report.next.filter=status:pending -WAITING limit:page\n"
441
    "report.next.sort=urgency-\n"
442
    "report.next.context=1\n"
443
    "\n"
444
    "report.ready.description=Most urgent actionable tasks\n"
445
    "report.ready.labels=ID,Active,Age,D,P,Project,Tags,R,S,Due,Until,Description,Urg\n"
446
    "report.ready.columns=id,start.age,entry.age,depends.indicator,priority,project,tags,recur."
447
    "indicator,scheduled.countdown,due.countdown,until.remaining,description,urgency\n"
448
    "report.ready.filter=+READY\n"
449
    "report.ready.sort=start-,urgency-\n"
450
    "report.ready.context=1\n"
451
    "\n"
452
    "report.blocked.description=Blocked tasks\n"
453
    "report.blocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
454
    "report.blocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
455
    "report.blocked.sort=due+,priority-,start-,project+\n"
456
    "report.blocked.filter=status:pending -WAITING +BLOCKED\n"
457
    "report.blocked.context=1\n"
458
    "\n"
459
    "report.unblocked.description=Unblocked tasks\n"
460
    "report.unblocked.columns=id,depends,project,priority,due,start.active,entry.age,description\n"
461
    "report.unblocked.labels=ID,Deps,Proj,Pri,Due,Active,Age,Description\n"
462
    "report.unblocked.sort=due+,priority-,start-,project+\n"
463
    "report.unblocked.filter=status:pending -WAITING -BLOCKED\n"
464
    "report.unblocked.context=1\n"
465
    "\n"
466
    "report.blocking.description=Blocking tasks\n"
467
    "report.blocking.labels=ID,UUID,A,Deps,Project,Tags,R,W,Sch,Due,Until,Description,Urg\n"
468
    "report.blocking.columns=id,uuid.short,start.active,depends,project,tags,recur,wait,scheduled."
469
    "remaining,due.relative,until.remaining,description.count,urgency\n"
470
    "report.blocking.sort=urgency-,due+,entry+\n"
471
    "report.blocking.filter=status:pending -WAITING +BLOCKING\n"
472
    "report.blocking.context=1\n"
473
    "\n"
474
    "report.timesheet.filter=(+PENDING -WAITING start.after:now-4wks) or (+COMPLETED -WAITING "
475
    "end.after:now-4wks)\n"
476
    "report.timesheet.context=0\n"
477
    "\n";
478

479
// Supported modifiers, synonyms on the same line.
480
static const char* modifierNames[] = {
481
    "before",     "under", "below",    "after", "over", "above", "by",       "none",
482
    "any",        "is",    "equals",   "isnt",  "not",  "has",   "contains", "hasnt",
483
    "startswith", "left",  "endswith", "right", "word", "noword"};
484

485
Context* Context::context;
486

487
////////////////////////////////////////////////////////////////////////////////
488
Context& Context::getContext() {
1,211,282✔
489
  assert(Context::context);
1,211,282✔
490
  return *Context::context;
1,211,282✔
491
}
492

493
////////////////////////////////////////////////////////////////////////////////
494
void Context::setContext(Context* context) { Context::context = context; }
4,502✔
495

496
////////////////////////////////////////////////////////////////////////////////
497
Context::~Context() {
4,504✔
498
  for (auto& com : commands) delete com.second;
409,479✔
499

500
  for (auto& col : columns) delete col.second;
113,086✔
501
}
4,504✔
502

503
////////////////////////////////////////////////////////////////////////////////
504
int Context::initialize(int argc, const char** argv) {
4,496✔
505
  timer_total.start();
4,496✔
506
  int rc = 0;
4,496✔
507
  home_dir = getenv("HOME");
4,496✔
508

509
  std::vector<std::string> searchPaths{TASK_RCDIR};
17,984✔
510

511
  try {
512
    ////////////////////////////////////////////////////////////////////////////
513
    //
514
    // [1] Load the correct config file.
515
    //     - Default to ~/.taskrc (ctor).
516
    //     - If no ~/.taskrc, use $XDG_CONFIG_HOME/task/taskrc if exists, or
517
    //       ~/.config/task/taskrc if $XDG_CONFIG_HOME is unset
518
    //     - Allow $TASKRC override.
519
    //     - Allow command line override rc:<file>
520
    //     - Load resultant file.
521
    //     - Apply command line overrides to the config.
522
    //
523
    ////////////////////////////////////////////////////////////////////////////
524

525
    bool taskrc_overridden = false;
4,496✔
526

527
    // XDG_CONFIG_HOME doesn't count as an override (no warning header)
528
    if (!rc_file.exists()) {
4,496✔
529
      // Use XDG_CONFIG_HOME if defined, otherwise default to ~/.config
530
      std::string xdg_config_home;
4,496✔
531
      const char* env_xdg_config_home = getenv("XDG_CONFIG_HOME");
4,496✔
532

533
      if (env_xdg_config_home)
4,496✔
534
        xdg_config_home = format("{1}", env_xdg_config_home);
4,496✔
535
      else
536
        xdg_config_home = format("{1}/.config", home_dir);
×
537

538
      // Ensure the path does not end with '/'
539
      if (xdg_config_home.back() == '/') xdg_config_home.pop_back();
4,496✔
540

541
      // https://github.com/GothenburgBitFactory/libshared/issues/32
542
      std::string rcfile_path = format("{1}/task/taskrc", xdg_config_home);
8,992✔
543

544
      File maybe_rc_file = File(rcfile_path);
4,496✔
545
      if (maybe_rc_file.exists()) rc_file = maybe_rc_file;
4,496✔
546
    }
4,496✔
547

548
    char* override = getenv("TASKRC");
4,496✔
549
    if (override) {
4,496✔
550
      rc_file = File(override);
4,387✔
551
      taskrc_overridden = true;
4,387✔
552
    }
553

554
    taskrc_overridden = CLI2::getOverride(argc, argv, rc_file) || taskrc_overridden;
4,496✔
555

556
    // Artificial scope for timing purposes.
557
    {
558
      Timer timer;
4,496✔
559
      config.parse(configurationDefaults, 1, searchPaths);
4,496✔
560
      config.load(rc_file._data, 1, searchPaths);
4,496✔
561
      debugTiming(format("Config::load ({1})", rc_file._data), timer);
4,496✔
562
    }
563

564
    CLI2::applyOverrides(argc, argv);
4,496✔
565

566
    if (taskrc_overridden && verbose("override"))
4,496✔
567
      header(format("TASKRC override: {1}", rc_file._data));
4,120✔
568

569
    ////////////////////////////////////////////////////////////////////////////
570
    //
571
    // [2] Locate the data directory.
572
    //     - Default to ~/.task (ctor).
573
    //     - Allow $TASKDATA override.
574
    //     - Allow command line override rc.data.location:<dir>
575
    //     - Inform TDB2 where to find data.
576
    //     - Create the rc_file and data_dir, if necessary.
577
    //
578
    ////////////////////////////////////////////////////////////////////////////
579

580
    bool taskdata_overridden = false;
4,496✔
581

582
    override = getenv("TASKDATA");
4,496✔
583
    if (override) {
4,496✔
584
      data_dir = Directory(override);
4,493✔
585
      config.set("data.location", data_dir._data);
4,493✔
586
      taskdata_overridden = true;
4,493✔
587
    }
588

589
    taskdata_overridden = CLI2::getDataLocation(argc, argv, data_dir) || taskdata_overridden;
4,496✔
590

591
    if (taskdata_overridden && verbose("override"))
4,496✔
592
      header(format("TASKDATA override: {1}", data_dir._data));
4,118✔
593

594
    createDefaultConfig();
4,496✔
595

596
    bool create_if_missing = !config.getBoolean("exit.on.missing.db");
4,496✔
597
    tdb2.open_replica(data_dir, create_if_missing);
4,497✔
598

599
    ////////////////////////////////////////////////////////////////////////////
600
    //
601
    // [3] Instantiate Command objects and capture command entities.
602
    //
603
    ////////////////////////////////////////////////////////////////////////////
604

605
    Command::factory(commands);
4,495✔
606
    for (auto& cmd : commands) cli2.entity("cmd", cmd.first);
409,470✔
607

608
    ////////////////////////////////////////////////////////////////////////////
609
    //
610
    // [4] Instantiate Column objects and capture column entities.
611
    //
612
    ////////////////////////////////////////////////////////////////////////////
613

614
    Column::factory(columns);
4,495✔
615
    for (auto& col : columns) cli2.entity("attribute", col.first);
113,077✔
616

617
    cli2.entity("pseudo", "limit");
4,495✔
618

619
    ////////////////////////////////////////////////////////////////////////////
620
    //
621
    // [5] Capture modifier and operator entities.
622
    //
623
    ////////////////////////////////////////////////////////////////////////////
624

625
    for (auto& modifierName : modifierNames) cli2.entity("modifier", modifierName);
103,385✔
626

627
    for (auto& op : Eval::getOperators()) cli2.entity("operator", op);
121,365✔
628

629
    for (auto& op : Eval::getBinaryOperators()) cli2.entity("binary_operator", op);
98,890✔
630

631
    ////////////////////////////////////////////////////////////////////////////
632
    //
633
    // [6] Complete the Context initialization.
634
    //
635
    ////////////////////////////////////////////////////////////////////////////
636

637
    initializeColorRules();
4,495✔
638
    staticInitialization();
4,495✔
639
    propagateDebug();
4,495✔
640
    loadAliases();
4,495✔
641

642
    ////////////////////////////////////////////////////////////////////////////
643
    //
644
    // [7] Parse the command line.
645
    //
646
    ////////////////////////////////////////////////////////////////////////////
647

648
    for (int i = 0; i < argc; i++) cli2.add(argv[i]);
21,832✔
649

650
    cli2.analyze();
4,495✔
651

652
    // Extract a recomposed command line.
653
    auto foundDefault = false;
4,493✔
654
    auto foundAssumed = false;
4,493✔
655
    std::string combined;
4,493✔
656
    for (auto& a : cli2._args) {
24,048✔
657
      if (combined.length()) combined += ' ';
19,555✔
658

659
      combined += a.attribute("raw");
19,555✔
660

661
      if (a.hasTag("DEFAULT")) foundDefault = true;
19,555✔
662

663
      if (a.hasTag("ASSUMED")) foundAssumed = true;
19,555✔
664
    }
665

666
    if (verbose("default")) {
4,493✔
667
      if (foundDefault) header("[" + combined + "]");
8✔
668

669
      if (foundAssumed) header("No command specified - assuming 'information'.");
8✔
670
    }
671

672
    ////////////////////////////////////////////////////////////////////////////
673
    //
674
    // [8] Initialize hooks.
675
    //
676
    ////////////////////////////////////////////////////////////////////////////
677

678
    hooks.initialize();
4,493✔
679
  }
4,493✔
680

681
  catch (const std::string& message) {
3✔
682
    error(message);
2✔
683
    rc = 2;
2✔
684
  }
2✔
685

686
  catch (rust::Error& err) {
1✔
687
    error(err.what());
1✔
688
    rc = 2;
1✔
689
  }
1✔
690

UNCOV
691
  catch (int) {
×
692
    // Hooks can terminate processing by throwing integers.
693
    rc = 4;
×
694
  }
695

696
  catch (const std::regex_error& e) {
×
697
    std::cout << "regex_error caught: " << e.what() << '\n';
×
698
  } catch (...) {
×
NEW
699
    error("Unknown error. Please report.");
×
700
    rc = 3;
×
701
  }
702

703
  // On initialization failure...
704
  if (rc) {
4,496✔
705
    // Dump all debug messages, controlled by rc.debug.
706
    if (config.getBoolean("debug")) {
3✔
707
      for (auto& d : debugMessages)
×
708
        if (color())
×
709
          std::cerr << colorizeDebug(d) << '\n';
×
710
        else
711
          std::cerr << d << '\n';
×
712
    }
713

714
    // Dump all headers, controlled by 'header' verbosity token.
715
    if (verbose("header")) {
3✔
716
      for (auto& h : headers)
9✔
717
        if (color())
6✔
718
          std::cerr << colorizeHeader(h) << '\n';
×
719
        else
720
          std::cerr << h << '\n';
6✔
721
    }
722

723
    // Dump all footnotes, controlled by 'footnote' verbosity token.
724
    if (verbose("footnote")) {
3✔
725
      for (auto& f : footnotes)
4✔
726
        if (color())
1✔
727
          std::cerr << colorizeFootnote(f) << '\n';
×
728
        else
729
          std::cerr << f << '\n';
1✔
730
    }
731

732
    // Dump all errors, non-maskable.
733
    // Colorized as footnotes.
734
    for (auto& e : errors)
6✔
735
      if (color())
3✔
736
        std::cerr << colorizeFootnote(e) << '\n';
×
737
      else
738
        std::cerr << e << '\n';
3✔
739
  }
740

741
  time_init_us += timer_total.total_us();
4,496✔
742
  return rc;
4,496✔
743
}
4,496✔
744

745
////////////////////////////////////////////////////////////////////////////////
746
int Context::run() {
4,493✔
747
  int rc;
748
  std::string output;
4,493✔
749

750
  try {
751
    hooks.onLaunch();
4,493✔
752
    rc = dispatch(output);
4,490✔
753
    hooks.onExit();  // No chance to update data.
4,015✔
754

755
    timer_total.stop();
4,013✔
756
    time_total_us += timer_total.total_us();
4,013✔
757

758
    std::stringstream s;
4,013✔
759
    s << "Perf " << PACKAGE_STRING << ' '
760
#ifdef HAVE_COMMIT
761
      << COMMIT
762
#else
763
      << '-'
764
#endif
765
      << ' ' << Datetime().toISO()
8,026✔
766

767
      << " init:" << time_init_us << " load:" << time_load_us
8,026✔
768
      << " gc:" << (time_gc_us > 0 ? time_gc_us - time_load_us : time_gc_us)
4,013✔
769
      << " filter:" << time_filter_us << " commit:" << time_commit_us << " sort:" << time_sort_us
4,013✔
770
      << " render:" << time_render_us << " hooks:" << time_hooks_us << " other:"
4,013✔
771
      << time_total_us - time_init_us - time_gc_us - time_filter_us - time_commit_us -
4,013✔
772
             time_sort_us - time_render_us - time_hooks_us
4,013✔
773
      << " total:" << time_total_us << '\n';
4,013✔
774
    debug(s.str());
4,013✔
775
  }
4,013✔
776

777
  catch (const std::string& message) {
480✔
778
    error(message);
462✔
779
    rc = 2;
462✔
780
  }
462✔
781

NEW
782
  catch (rust::Error& err) {
×
NEW
783
    error(err.what());
×
NEW
784
    rc = 2;
×
785
  }
786

787
  catch (int) {
18✔
788
    // Hooks can terminate processing by throwing integers.
789
    rc = 4;
18✔
790
  }
18✔
791

792
  catch (...) {
×
793
    error("Unknown error. Please report.");
×
794
    rc = 3;
×
795
  }
796

797
  // Dump all debug messages, controlled by rc.debug.
798
  if (config.getBoolean("debug")) {
4,493✔
799
    for (auto& d : debugMessages)
176✔
800
      if (color())
166✔
801
        std::cerr << colorizeDebug(d) << '\n';
8✔
802
      else
803
        std::cerr << d << '\n';
158✔
804
  }
805

806
  // Dump all headers, controlled by 'header' verbosity token.
807
  if (verbose("header")) {
4,493✔
808
    for (auto& h : headers)
11,382✔
809
      if (color())
7,582✔
810
        std::cerr << colorizeHeader(h) << '\n';
364✔
811
      else
812
        std::cerr << h << '\n';
7,218✔
813
  }
814

815
  // Dump the report output.
816
  std::cout << output;
4,493✔
817

818
  // Dump all footnotes, controlled by 'footnote' verbosity token.
819
  if (verbose("footnote")) {
4,493✔
820
    for (auto& f : footnotes)
5,149✔
821
      if (color())
1,283✔
822
        std::cerr << colorizeFootnote(f) << '\n';
69✔
823
      else
824
        std::cerr << f << '\n';
1,214✔
825
  }
826

827
  // Dump all errors, non-maskable.
828
  // Colorized as footnotes.
829
  for (auto& e : errors)
4,975✔
830
    if (color())
482✔
831
      std::cerr << colorizeError(e) << '\n';
1✔
832
    else
833
      std::cerr << e << '\n';
481✔
834

835
  return rc;
4,493✔
836
}
4,493✔
837

838
////////////////////////////////////////////////////////////////////////////////
839
// Dispatch to the command found by the CLI parser.
840
int Context::dispatch(std::string& out) {
4,490✔
841
  // Autocomplete args against keywords.
842
  std::string command = cli2.getCommand();
4,490✔
843
  if (command != "") {
4,490✔
844
    updateXtermTitle();
4,490✔
845
    updateVerbosity();
4,490✔
846

847
    Command* c = commands[command];
4,490✔
848
    assert(c);
4,490✔
849

850
    // The command know whether they need a GC.
851
    if (c->needs_gc()) {
4,490✔
852
      tdb2.gc();
913✔
853
    }
854

855
    // This is something that is only needed for write commands with no other
856
    // filter processing.
857
    if (c->accepts_modifications() && !c->accepts_filter()) {
4,490✔
858
      cli2.prepareFilter();
1,667✔
859
    }
860

861
    // With rc.debug.parser == 2, there are more tree dumps than you might want,
862
    // but we need the rc.debug.parser == 1 case covered also, with the final
863
    // tree.
864
    if (config.getBoolean("debug") && config.getInteger("debug.parser") == 1)
4,490✔
865
      debug(cli2.dump("Parse Tree (before command-specifÑ–c processing)"));
4✔
866

867
    return c->execute(out);
4,490✔
868
  }
869

870
  assert(commands["help"]);
×
871
  return commands["help"]->execute(out);
×
872
}
4,490✔
873

874
////////////////////////////////////////////////////////////////////////////////
875
int Context::getWidth() {
1,033✔
876
  // Determine window size.
877
  auto width = config.getInteger("defaultwidth");
1,033✔
878

879
  // A zero width value means 'infinity', which is approximated here by 2^16.
880
  if (width == 0) return 65536;
1,033✔
881

882
  if (config.getBoolean("detection")) {
1,020✔
883
    if (terminal_width == 0 && terminal_height == 0) {
1✔
884
      unsigned short buff[4];
885
      if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &buff) != -1) {
1✔
886
        terminal_height = buff[0];
×
887
        terminal_width = buff[1];
×
888
      }
889
    }
890

891
    width = terminal_width;
1✔
892

893
    // Ncurses does this, and perhaps we need to as well, to avoid a problem on
894
    // Cygwin where the display goes right up to the terminal width, and causes
895
    // an odd color wrapping problem.
896
    if (config.getBoolean("avoidlastcolumn")) --width;
1✔
897
  }
898

899
  return width;
1,020✔
900
}
901

902
////////////////////////////////////////////////////////////////////////////////
903
int Context::getHeight() {
72✔
904
  // Determine window size.
905
  auto height = config.getInteger("defaultheight");
72✔
906

907
  // A zero height value means 'infinity', which is approximated here by 2^16.
908
  if (height == 0) return 65536;
72✔
909

910
  if (config.getBoolean("detection")) {
72✔
911
    if (terminal_width == 0 && terminal_height == 0) {
×
912
      unsigned short buff[4];
913
      if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &buff) != -1) {
×
914
        terminal_height = buff[0];
×
915
        terminal_width = buff[1];
×
916
      }
917
    }
918

919
    height = terminal_height;
×
920
  }
921

922
  return height;
72✔
923
}
924

925
////////////////////////////////////////////////////////////////////////////////
926
std::string Context::getTaskContext(const std::string& kind, std::string name,
3,134✔
927
                                    bool fallback /* = true */) {
928
  // Consider currently selected context, if none specified
929
  if (name.empty()) name = config.get("context");
3,134✔
930

931
  // Detect if any context is set, and bail out if not
932
  if (!name.empty())
3,134✔
933
    debug(format("Applying context '{1}'", name));
36✔
934
  else {
935
    debug("No context set");
3,098✔
936
    return "";
3,098✔
937
  }
938

939
  // Figure out the context string for this kind (read/write)
940
  std::string contextString = "";
36✔
941

942
  if (!config.has("context." + name + "." + kind) && kind == "read") {
36✔
943
    debug("Specific " + kind + " context for '" + name + "' not defined. ");
1✔
944
    if (fallback) {
1✔
945
      debug("Trying to interpret old-style context definition as read context.");
1✔
946
      contextString = config.get("context." + name);
1✔
947
    }
948
  } else
949
    contextString = config.get("context." + name + "." + kind);
35✔
950

951
  debug(format("Detected context string: {1}", contextString.empty() ? "(empty)" : contextString));
36✔
952
  return contextString;
36✔
953
}
36✔
954

955
////////////////////////////////////////////////////////////////////////////////
956
bool Context::color() {
27,293✔
957
  if (determine_color_use) {
27,293✔
958
    // What the config says.
959
    use_color = config.getBoolean("color");
4,497✔
960

961
    // Only tty's support color.
962
    if (!isatty(STDOUT_FILENO)) {
4,497✔
963
      // No ioctl.
964
      config.set("detection", "off");
4,497✔
965
      config.set("color", "off");
4,497✔
966

967
      // Files don't get color.
968
      use_color = false;
4,497✔
969
    }
970

971
    // Override.
972
    if (config.getBoolean("_forcecolor")) {
4,497✔
973
      config.set("color", "on");
183✔
974
      use_color = true;
183✔
975
    }
976

977
    // No need to go through this again.
978
    determine_color_use = false;
4,497✔
979
  }
980

981
  // Cached result.
982
  return use_color;
27,293✔
983
}
984

985
////////////////////////////////////////////////////////////////////////////////
986
// Support verbosity levels:
987
//
988
//   rc.verbose=1          Show all feedback.
989
//   rc.verbose=0          Show regular feedback.
990
//   rc.verbose=nothing    Show the absolute minimum.
991
//   rc.verbose=one,two    Show verbosity for 'one' and 'two' only.
992
//
993
// TODO This mechanism is clunky, and should slowly evolve into something more
994
//      logical and consistent.  This should probably mean that 'nothing' should
995
//      take the place of '0'.
996
bool Context::verbose(const std::string& token) {
45,269✔
997
  if (verbosity.empty()) {
45,269✔
998
    verbosity_legacy = config.getBoolean("verbose");
4,497✔
999
    for (auto& token : split(config.get("verbose"), ',')) verbosity.insert(token);
62,457✔
1000

1001
    // Regular feedback means almost everything.
1002
    // This odd test is to see if a Boolean-false value is a real one, which
1003
    // means it is not 1/true/T/yes/on, but also should not be one of the
1004
    // valid tokens either.
1005
    if (!verbosity_legacy && !verbosity.empty()) {
4,497✔
1006
      std::string v = *(verbosity.begin());
4,490✔
1007
      if (v != "nothing" && v != "affected" &&  // This list must be complete.
8,684✔
1008
          v != "blank" &&                       //
37✔
1009
          v != "context" &&                     //
36✔
1010
          v != "default" &&                     //
36✔
1011
          v != "edit" &&                        //
35✔
1012
          v != "filter" &&                      //
35✔
1013
          v != "footnote" &&                    //
35✔
1014
          v != "header" &&                      //
33✔
1015
          v != "label" &&                       //
32✔
1016
          v != "new-id" &&                      //
31✔
1017
          v != "new-uuid" &&                    //
29✔
1018
          v != "news" &&                        //
26✔
1019
          v != "override" &&                    //
26✔
1020
          v != "project" &&                     //
25✔
1021
          v != "recur" &&                       //
24✔
1022
          v != "special" &&                     //
8,707✔
1023
          v != "sync") {
23✔
1024
        // This list emulates rc.verbose=off in version 1.9.4.
1025
        verbosity = {"blank", "label", "new-id", "edit"};
115✔
1026
      }
1027
    }
4,490✔
1028

1029
    // Some flags imply "footnote" verbosity being active.  Make it so.
1030
    if (!verbosity.count("footnote")) {
4,497✔
1031
      // TODO: Some of these may not use footnotes yet.  They should.
1032
      for (auto flag : {"affected", "new-id", "new-uuid", "project", "override", "recur"}) {
2,267✔
1033
        if (verbosity.count(flag)) {
1,960✔
1034
          verbosity.insert("footnote");
76✔
1035
          break;
76✔
1036
        }
1037
      }
1038
    }
1039

1040
    // Some flags imply "header" verbosity being active.  Make it so.
1041
    if (!verbosity.count("header")) {
4,497✔
1042
      for (auto flag : {"default"}) {
766✔
1043
        if (verbosity.count(flag)) {
383✔
1044
          verbosity.insert("header");
×
1045
          break;
×
1046
        }
1047
      }
1048
    }
1049
  }
1050

1051
  // rc.verbose=true|y|yes|1|on overrides all.
1052
  if (verbosity_legacy) return true;
45,269✔
1053

1054
  // rc.verbose=nothing overrides all.
1055
  if (verbosity.size() == 1 && *(verbosity.begin()) == "nothing") return false;
45,187✔
1056

1057
  // Specific token match.
1058
  if (verbosity.count(token)) return true;
40,653✔
1059

1060
  return false;
10,029✔
1061
}
1062

1063
////////////////////////////////////////////////////////////////////////////////
1064
const std::vector<std::string> Context::getColumns() const {
1✔
1065
  std::vector<std::string> output;
1✔
1066
  for (auto& col : columns) output.push_back(col.first);
25✔
1067

1068
  return output;
1✔
1069
}
1070

1071
////////////////////////////////////////////////////////////////////////////////
1072
// A value of zero mean unlimited.
1073
// A value of 'page' means however many screen lines there are.
1074
// A value of a positive integer is a row/task limit.
1075
void Context::getLimits(int& rows, int& lines) {
750✔
1076
  rows = 0;
750✔
1077
  lines = 0;
750✔
1078

1079
  // This is an integer specified as a filter (limit:10).
1080
  auto limit = config.get("limit");
1,500✔
1081
  if (limit != "") {
750✔
1082
    if (limit == "page") {
67✔
1083
      rows = 0;
58✔
1084
      lines = getHeight();
58✔
1085
    } else {
1086
      rows = (int)strtol(limit.c_str(), nullptr, 10);
9✔
1087
      lines = 0;
9✔
1088
    }
1089
  }
1090
}
750✔
1091

1092
////////////////////////////////////////////////////////////////////////////////
1093
// The 'Task' object, among others, is shared between projects.  To make this
1094
// easier, it has been decoupled from Context.
1095
void Context::staticInitialization() {
4,495✔
1096
  CLI2::minimumMatchLength = config.getInteger("abbreviation.minimum");
4,495✔
1097
  Lexer::minimumMatchLength = config.getInteger("abbreviation.minimum");
4,495✔
1098

1099
  Task::defaultProject = config.get("default.project");
4,495✔
1100
  Task::defaultDue = config.get("default.due");
4,495✔
1101
  Task::defaultScheduled = config.get("default.scheduled");
4,495✔
1102

1103
  Task::searchCaseSensitive = Variant::searchCaseSensitive =
4,495✔
1104
      config.getBoolean("search.case.sensitive");
4,495✔
1105
  Task::regex = Variant::searchUsingRegex = config.getBoolean("regex");
4,495✔
1106
  Lexer::dateFormat = Variant::dateFormat = config.get("dateformat");
4,495✔
1107

1108
  Datetime::isoEnabled = config.getBoolean("date.iso");
4,495✔
1109
  Datetime::standaloneDateEnabled = false;
4,495✔
1110
  Datetime::standaloneTimeEnabled = false;
4,495✔
1111
  Duration::standaloneSecondsEnabled = false;
4,495✔
1112

1113
  TDB2::debug_mode = config.getBoolean("debug");
4,495✔
1114

1115
  for (auto& rc : config) {
1,109,604✔
1116
    if (rc.first.substr(0, 4) == "uda." && rc.first.substr(rc.first.length() - 7, 7) == ".values") {
1,105,109✔
1117
      std::string name = rc.first.substr(4, rc.first.length() - 7 - 4);
4,572✔
1118
      auto values = split(rc.second, ',');
4,572✔
1119

1120
      for (auto r = values.rbegin(); r != values.rend(); ++r) Task::customOrder[name].push_back(*r);
22,779✔
1121
    }
4,572✔
1122
  }
1123

1124
  for (auto& col : columns) {
113,077✔
1125
    Task::attributes[col.first] = col.second->type();
108,582✔
1126
    Lexer::attributes[col.first] = col.second->type();
108,582✔
1127
  }
1128

1129
  Task::urgencyProjectCoefficient = config.getReal("urgency.project.coefficient");
4,495✔
1130
  Task::urgencyActiveCoefficient = config.getReal("urgency.active.coefficient");
4,495✔
1131
  Task::urgencyScheduledCoefficient = config.getReal("urgency.scheduled.coefficient");
4,495✔
1132
  Task::urgencyWaitingCoefficient = config.getReal("urgency.waiting.coefficient");
4,495✔
1133
  Task::urgencyBlockedCoefficient = config.getReal("urgency.blocked.coefficient");
4,495✔
1134
  Task::urgencyAnnotationsCoefficient = config.getReal("urgency.annotations.coefficient");
4,495✔
1135
  Task::urgencyTagsCoefficient = config.getReal("urgency.tags.coefficient");
4,495✔
1136
  Task::urgencyDueCoefficient = config.getReal("urgency.due.coefficient");
4,495✔
1137
  Task::urgencyBlockingCoefficient = config.getReal("urgency.blocking.coefficient");
4,495✔
1138
  Task::urgencyAgeCoefficient = config.getReal("urgency.age.coefficient");
4,495✔
1139
  Task::urgencyAgeMax = config.getReal("urgency.age.max");
4,495✔
1140

1141
  // Tag- and project-specific coefficients.
1142
  for (auto& var : config.all())
1,109,604✔
1143
    if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.")
1,105,109✔
1144
      Task::coefficients[var] = config.getReal(var);
22,713✔
1145
}
4,495✔
1146

1147
////////////////////////////////////////////////////////////////////////////////
1148
void Context::createDefaultConfig() {
4,496✔
1149
  // Do we need to create a default rc?
1150
  if (rc_file._data != "" && !rc_file.exists()) {
4,496✔
1151
    if (config.getBoolean("confirmation") &&
3✔
1152
        !confirm(format("A configuration file could not be found in {1}\n\nWould you like a sample "
2✔
1153
                        "{2} created, so Taskwarrior can proceed?",
1154
                        home_dir, rc_file._data)))
1✔
1155
      throw std::string("Cannot proceed without rc file.");
×
1156

1157
    Datetime now;
1✔
1158
    std::stringstream contents;
1✔
1159
    contents << "# [Created by " << PACKAGE_STRING << ' ' << now.toString("m/d/Y H:N:S") << "]\n"
2✔
1160
             << "data.location=" << data_dir._original << "\n"
1✔
1161
             << "news.version=" << Version::Current() << "\n"
3✔
1162
             << "\n# To use the default location of the XDG directories,\n"
1163
             << "# move this configuration file from ~/.taskrc to ~/.config/task/taskrc and update "
1164
                "location config as follows:\n"
1165
             << "\n#data.location=~/.local/share/task\n"
1166
             << "#hooks.location=~/.config/task/hooks\n"
1167
             << "\n# Color theme (uncomment one to use)\n"
1168
             << "#include light-16.theme\n"
1169
             << "#include light-256.theme\n"
1170
             << "#include dark-16.theme\n"
1171
             << "#include dark-256.theme\n"
1172
             << "#include dark-red-256.theme\n"
1173
             << "#include dark-green-256.theme\n"
1174
             << "#include dark-blue-256.theme\n"
1175
             << "#include dark-violets-256.theme\n"
1176
             << "#include dark-yellow-green.theme\n"
1177
             << "#include dark-gray-256.theme\n"
1178
             << "#include dark-gray-blue-256.theme\n"
1179
             << "#include solarized-dark-256.theme\n"
1180
             << "#include solarized-light-256.theme\n"
1181
             << "#include no-color.theme\n"
1182
             << '\n';
1✔
1183

1184
    // Write out the new file.
1185
    if (!File::write(rc_file._data, contents.str()))
1✔
1186
      throw format("Could not write to '{1}'.", rc_file._data);
×
1187

1188
    // Load it so that it takes effect for this run.
1189
    config.load(rc_file);
1✔
1190
  }
1✔
1191
}
4,496✔
1192

1193
////////////////////////////////////////////////////////////////////////////////
1194
void Context::decomposeSortField(const std::string& field, std::string& key, bool& ascending,
5,038✔
1195
                                 bool& breakIndicator) {
1196
  int length = field.length();
5,038✔
1197

1198
  int decoration = 1;
5,038✔
1199
  breakIndicator = false;
5,038✔
1200
  if (field[length - decoration] == '/') {
5,038✔
1201
    breakIndicator = true;
18✔
1202
    ++decoration;
18✔
1203
  }
1204

1205
  if (field[length - decoration] == '+') {
5,038✔
1206
    ascending = true;
2,186✔
1207
    key = field.substr(0, length - decoration);
2,186✔
1208
  } else if (field[length - decoration] == '-') {
2,852✔
1209
    ascending = false;
2,346✔
1210
    key = field.substr(0, length - decoration);
2,346✔
1211
  } else {
1212
    ascending = true;
506✔
1213
    key = field;
506✔
1214
  }
1215
}
5,038✔
1216

1217
////////////////////////////////////////////////////////////////////////////////
1218
void Context::debugTiming(const std::string& details, const Timer& timer) {
4,497✔
1219
  std::stringstream out;
4,497✔
1220
  out << "Timer " << details << ' ' << std::setprecision(6) << std::fixed
4,497✔
1221
      << timer.total_us() / 1.0e6 << " sec";
4,497✔
1222
  debug(out.str());
4,497✔
1223
}
4,497✔
1224

1225
////////////////////////////////////////////////////////////////////////////////
1226
CurrentTask Context::withCurrentTask(const Task* task) { return CurrentTask(*this, task); }
10,875✔
1227

1228
////////////////////////////////////////////////////////////////////////////////
1229
// This capability is to answer the question of 'what did I just do to generate
1230
// this output?'.
1231
void Context::updateXtermTitle() {
4,490✔
1232
  if (config.getBoolean("xterm.title") && isatty(STDOUT_FILENO)) {
4,490✔
1233
    auto command = cli2.getCommand();
×
1234
    std::string title;
×
1235

1236
    for (auto a = cli2._args.begin(); a != cli2._args.end(); ++a) {
×
1237
      if (a != cli2._args.begin()) title += ' ';
×
1238

1239
      title += a->attribute("raw");
×
1240
    }
1241

1242
    std::cout << " ]0;task " << command << ' ' << title << " ";
×
1243
  }
1244
}
4,490✔
1245

1246
////////////////////////////////////////////////////////////////////////////////
1247
// This function allows a clean output if the command is a helper subcommand.
1248
void Context::updateVerbosity() {
4,490✔
1249
  auto command = cli2.getCommand();
4,490✔
1250
  if (command != "" && command[0] == '_') {
4,490✔
1251
    verbosity = {"nothing"};
720✔
1252
  }
1253
}
4,490✔
1254

1255
////////////////////////////////////////////////////////////////////////////////
1256
void Context::loadAliases() {
4,495✔
1257
  for (auto& i : config)
1,109,612✔
1258
    if (i.first.substr(0, 6) == "alias.") cli2.alias(i.first.substr(6), i.second);
1,105,117✔
1259
}
4,495✔
1260

1261
////////////////////////////////////////////////////////////////////////////////
1262
// Using the general rc.debug setting automaticalls sets debug.hooks
1263
// and debug.parser, unless they already have values, which by default they do
1264
// not.
1265
void Context::propagateDebug() {
4,495✔
1266
  if (config.getBoolean("debug")) {
4,495✔
1267
    if (!config.has("debug.hooks")) config.set("debug.hooks", 1);
4✔
1268

1269
    if (!config.has("debug.parser")) config.set("debug.parser", 1);
4✔
1270
  } else {
1271
    if ((config.has("debug.hooks") && config.getInteger("debug.hooks")) ||
13,471✔
1272
        (config.has("debug.parser") && config.getInteger("debug.parser")))
8,980✔
1273
      config.set("debug", true);
6✔
1274
  }
1275
}
4,495✔
1276

1277
////////////////////////////////////////////////////////////////////////////////
1278
// No duplicates.
1279
void Context::header(const std::string& input) {
8,242✔
1280
  if (input.length() && std::find(headers.begin(), headers.end(), input) == headers.end())
8,242✔
1281
    headers.push_back(input);
8,242✔
1282
}
8,242✔
1283

1284
////////////////////////////////////////////////////////////////////////////////
1285
// No duplicates.
1286
void Context::footnote(const std::string& input) {
2,254✔
1287
  if (input.length() && std::find(footnotes.begin(), footnotes.end(), input) == footnotes.end())
2,254✔
1288
    footnotes.push_back(input);
1,305✔
1289
}
2,254✔
1290

1291
////////////////////////////////////////////////////////////////////////////////
1292
// No duplicates.
1293
void Context::error(const std::string& input) {
491✔
1294
  if (input.length() && std::find(errors.begin(), errors.end(), input) == errors.end())
491✔
1295
    errors.push_back(input);
485✔
1296
}
491✔
1297

1298
////////////////////////////////////////////////////////////////////////////////
1299
void Context::debug(const std::string& input) {
16,402✔
1300
  if (input.length()) debugMessages.push_back(input);
16,402✔
1301
}
16,402✔
1302

1303
////////////////////////////////////////////////////////////////////////////////
1304
CurrentTask::CurrentTask(Context& context, const Task* task)
10,875✔
1305
    : context{context}, previous{context.currentTask} {
10,875✔
1306
  context.currentTask = task;
10,875✔
1307
}
10,875✔
1308

1309
////////////////////////////////////////////////////////////////////////////////
1310
CurrentTask::~CurrentTask() { context.currentTask = previous; }
10,875✔
1311

1312
////////////////////////////////////////////////////////////////////////////////
1313

1314
// vim ts=2:sw=2
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc