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

GothenburgBitFactory / taskwarrior / 11420355627

19 Oct 2024 08:00PM UTC coverage: 84.853% (+0.6%) from 84.223%
11420355627

push

github

web-flow
Pass rc.weekstart to libshared for ISO8601 weeknum parsing if "monday" (#3654)

* libshared: bump for weekstart, epoch defines, eopww fix

mainly those visible changes, and miscellaneous others

see GothenburgBitFactory/taskwarrior#3623 (weekstart)
see GothenburgBitFactory/taskwarrior#3651 (epoch limit defines)
see GothenburgBitFactory/libshared#73 (eopww fix)

* Initialize libshared's weekstart from user's rc.weekstart config

This enables use of newer libshared code that can parse week numbers
according to ISO8601 instead of existing code which is always using
Sunday-based weeks.  To get ISO behavior, set rc.weekstart=monday.
Default is still Sunday / old algorithm, as before, since Sunday is in
the hardcoded default rcfile.

Weekstart does not yet fix week-relative shortcuts, which will still
always use Monday.

See #3623 for further details.

4 of 6 new or added lines in 2 files covered. (66.67%)

993 existing lines in 25 files now uncovered.

19019 of 22414 relevant lines covered (84.85%)

23067.98 hits per line

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

89.93
/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,215,683✔
489
  assert(Context::context);
1,215,683✔
490
  return *Context::context;
1,215,683✔
491
}
492

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

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

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

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

509
  std::vector<std::string> searchPaths{TASK_RCDIR};
17,992✔
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,498✔
526

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

533
      if (env_xdg_config_home)
4,498✔
534
        xdg_config_home = format("{1}", env_xdg_config_home);
4,498✔
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,498✔
540

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

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

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

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

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

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

566
    if (taskrc_overridden && verbose("override"))
4,498✔
567
      header(format("TASKRC override: {1}", rc_file._data));
4,122✔
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,498✔
581

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

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

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

594
    createDefaultConfig();
4,498✔
595

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

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

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

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

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

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

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

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

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

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

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

637
    initializeColorRules();
4,497✔
638
    staticInitialization();
4,497✔
639
    propagateDebug();
4,497✔
640
    loadAliases();
4,497✔
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,841✔
649

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

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

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

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

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

666
    if (verbose("default")) {
4,495✔
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,495✔
679
  }
4,495✔
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

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 (...) {
×
699
    error("Unknown error. Please report.");
×
700
    rc = 3;
×
701
  }
702

703
  // On initialization failure...
704
  if (rc) {
4,498✔
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,498✔
742
  return rc;
4,498✔
743
}
4,498✔
744

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

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

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

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

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

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

782
  catch (rust::Error& err) {
×
783
    error(err.what());
×
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,495✔
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,495✔
808
    for (auto& h : headers)
11,388✔
809
      if (color())
7,586✔
810
        std::cerr << colorizeHeader(h) << '\n';
364✔
811
      else
812
        std::cerr << h << '\n';
7,222✔
813
  }
814

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

818
  // Dump all footnotes, controlled by 'footnote' verbosity token.
819
  if (verbose("footnote")) {
4,495✔
820
    for (auto& f : footnotes)
5,151✔
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,978✔
830
    if (color())
483✔
831
      std::cerr << colorizeError(e) << '\n';
1✔
832
    else
833
      std::cerr << e << '\n';
482✔
834

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

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

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

850
    // The command know whether they need a GC.
851
    if (c->needs_gc()) {
4,492✔
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,492✔
858
      cli2.prepareFilter();
1,668✔
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,492✔
865
      debug(cli2.dump("Parse Tree (before command-specifÑ–c processing)"));
4✔
866

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

870
  assert(commands["help"]);
×
871
  return commands["help"]->execute(out);
×
872
}
4,492✔
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,135✔
927
                                    bool fallback /* = true */) {
928
  // Consider currently selected context, if none specified
929
  if (name.empty()) name = config.get("context");
3,135✔
930

931
  // Detect if any context is set, and bail out if not
932
  if (!name.empty())
3,135✔
933
    debug(format("Applying context '{1}'", name));
36✔
934
  else {
935
    debug("No context set");
3,099✔
936
    return "";
3,099✔
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,301✔
957
  if (determine_color_use) {
27,301✔
958
    // What the config says.
959
    use_color = config.getBoolean("color");
4,499✔
960

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

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

971
    // Override.
972
    if (config.getBoolean("_forcecolor")) {
4,499✔
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,499✔
979
  }
980

981
  // Cached result.
982
  return use_color;
27,301✔
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,288✔
997
  if (verbosity.empty()) {
45,288✔
998
    verbosity_legacy = config.getBoolean("verbose");
4,499✔
999
    for (auto& token : split(config.get("verbose"), ',')) verbosity.insert(token);
62,487✔
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,499✔
1006
      std::string v = *(verbosity.begin());
4,492✔
1007
      if (v != "nothing" && v != "affected" &&  // This list must be complete.
8,688✔
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,711✔
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,492✔
1028

1029
    // Some flags imply "footnote" verbosity being active.  Make it so.
1030
    if (!verbosity.count("footnote")) {
4,499✔
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,499✔
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,288✔
1053

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

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

1060
  return false;
10,035✔
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,497✔
1096
  CLI2::minimumMatchLength = config.getInteger("abbreviation.minimum");
4,497✔
1097
  Lexer::minimumMatchLength = config.getInteger("abbreviation.minimum");
4,497✔
1098

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

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

1108
  auto weekStart = Datetime::dayOfWeek(config.get("weekstart"));
4,497✔
1109
  if (weekStart != 0 && weekStart != 1)
4,497✔
NEW
1110
    throw std::string(
×
NEW
1111
        "The 'weekstart' configuration variable may only contain 'Sunday' or 'Monday'.");
×
1112
  Datetime::weekstart = weekStart;
4,497✔
1113
  Datetime::isoEnabled = config.getBoolean("date.iso");
4,497✔
1114
  Datetime::standaloneDateEnabled = false;
4,497✔
1115
  Datetime::standaloneTimeEnabled = false;
4,497✔
1116
  Duration::standaloneSecondsEnabled = false;
4,497✔
1117

1118
  TDB2::debug_mode = config.getBoolean("debug");
4,497✔
1119

1120
  for (auto& rc : config) {
1,110,096✔
1121
    if (rc.first.substr(0, 4) == "uda." && rc.first.substr(rc.first.length() - 7, 7) == ".values") {
1,105,599✔
1122
      std::string name = rc.first.substr(4, rc.first.length() - 7 - 4);
4,574✔
1123
      auto values = split(rc.second, ',');
4,574✔
1124

1125
      for (auto r = values.rbegin(); r != values.rend(); ++r) Task::customOrder[name].push_back(*r);
22,789✔
1126
    }
4,574✔
1127
  }
1128

1129
  for (auto& col : columns) {
113,127✔
1130
    Task::attributes[col.first] = col.second->type();
108,630✔
1131
    Lexer::attributes[col.first] = col.second->type();
108,630✔
1132
  }
1133

1134
  Task::urgencyProjectCoefficient = config.getReal("urgency.project.coefficient");
4,497✔
1135
  Task::urgencyActiveCoefficient = config.getReal("urgency.active.coefficient");
4,497✔
1136
  Task::urgencyScheduledCoefficient = config.getReal("urgency.scheduled.coefficient");
4,497✔
1137
  Task::urgencyWaitingCoefficient = config.getReal("urgency.waiting.coefficient");
4,497✔
1138
  Task::urgencyBlockedCoefficient = config.getReal("urgency.blocked.coefficient");
4,497✔
1139
  Task::urgencyAnnotationsCoefficient = config.getReal("urgency.annotations.coefficient");
4,497✔
1140
  Task::urgencyTagsCoefficient = config.getReal("urgency.tags.coefficient");
4,497✔
1141
  Task::urgencyDueCoefficient = config.getReal("urgency.due.coefficient");
4,497✔
1142
  Task::urgencyBlockingCoefficient = config.getReal("urgency.blocking.coefficient");
4,497✔
1143
  Task::urgencyAgeCoefficient = config.getReal("urgency.age.coefficient");
4,497✔
1144
  Task::urgencyAgeMax = config.getReal("urgency.age.max");
4,497✔
1145

1146
  // Tag- and project-specific coefficients.
1147
  for (auto& var : config.all())
1,110,096✔
1148
    if (var.substr(0, 13) == "urgency.user." || var.substr(0, 12) == "urgency.uda.")
1,105,599✔
1149
      Task::coefficients[var] = config.getReal(var);
22,723✔
1150
}
4,497✔
1151

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

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

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

1193
    // Load it so that it takes effect for this run.
1194
    config.load(rc_file);
1✔
1195
  }
1✔
1196
}
4,498✔
1197

1198
////////////////////////////////////////////////////////////////////////////////
1199
void Context::decomposeSortField(const std::string& field, std::string& key, bool& ascending,
5,084✔
1200
                                 bool& breakIndicator) {
1201
  int length = field.length();
5,084✔
1202

1203
  int decoration = 1;
5,084✔
1204
  breakIndicator = false;
5,084✔
1205
  if (field[length - decoration] == '/') {
5,084✔
1206
    breakIndicator = true;
18✔
1207
    ++decoration;
18✔
1208
  }
1209

1210
  if (field[length - decoration] == '+') {
5,084✔
1211
    ascending = true;
2,193✔
1212
    key = field.substr(0, length - decoration);
2,193✔
1213
  } else if (field[length - decoration] == '-') {
2,891✔
1214
    ascending = false;
2,391✔
1215
    key = field.substr(0, length - decoration);
2,391✔
1216
  } else {
1217
    ascending = true;
500✔
1218
    key = field;
500✔
1219
  }
1220
}
5,084✔
1221

1222
////////////////////////////////////////////////////////////////////////////////
1223
void Context::debugTiming(const std::string& details, const Timer& timer) {
4,499✔
1224
  std::stringstream out;
4,499✔
1225
  out << "Timer " << details << ' ' << std::setprecision(6) << std::fixed
4,499✔
1226
      << timer.total_us() / 1.0e6 << " sec";
4,499✔
1227
  debug(out.str());
4,499✔
1228
}
4,499✔
1229

1230
////////////////////////////////////////////////////////////////////////////////
1231
CurrentTask Context::withCurrentTask(const Task* task) { return CurrentTask(*this, task); }
10,878✔
1232

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

1241
    for (auto a = cli2._args.begin(); a != cli2._args.end(); ++a) {
×
1242
      if (a != cli2._args.begin()) title += ' ';
×
1243

1244
      title += a->attribute("raw");
×
1245
    }
1246

1247
    std::cout << " ]0;task " << command << ' ' << title << " ";
×
1248
  }
1249
}
4,492✔
1250

1251
////////////////////////////////////////////////////////////////////////////////
1252
// This function allows a clean output if the command is a helper subcommand.
1253
void Context::updateVerbosity() {
4,492✔
1254
  auto command = cli2.getCommand();
4,492✔
1255
  if (command != "" && command[0] == '_') {
4,492✔
1256
    verbosity = {"nothing"};
720✔
1257
  }
1258
}
4,492✔
1259

1260
////////////////////////////////////////////////////////////////////////////////
1261
void Context::loadAliases() {
4,497✔
1262
  for (auto& i : config)
1,110,104✔
1263
    if (i.first.substr(0, 6) == "alias.") cli2.alias(i.first.substr(6), i.second);
1,105,607✔
1264
}
4,497✔
1265

1266
////////////////////////////////////////////////////////////////////////////////
1267
// Using the general rc.debug setting automaticalls sets debug.hooks
1268
// and debug.parser, unless they already have values, which by default they do
1269
// not.
1270
void Context::propagateDebug() {
4,497✔
1271
  if (config.getBoolean("debug")) {
4,497✔
1272
    if (!config.has("debug.hooks")) config.set("debug.hooks", 1);
4✔
1273

1274
    if (!config.has("debug.parser")) config.set("debug.parser", 1);
4✔
1275
  } else {
1276
    if ((config.has("debug.hooks") && config.getInteger("debug.hooks")) ||
13,477✔
1277
        (config.has("debug.parser") && config.getInteger("debug.parser")))
8,984✔
1278
      config.set("debug", true);
6✔
1279
  }
1280
}
4,497✔
1281

1282
////////////////////////////////////////////////////////////////////////////////
1283
// No duplicates.
1284
void Context::header(const std::string& input) {
8,246✔
1285
  if (input.length() && std::find(headers.begin(), headers.end(), input) == headers.end())
8,246✔
1286
    headers.push_back(input);
8,246✔
1287
}
8,246✔
1288

1289
////////////////////////////////////////////////////////////////////////////////
1290
// No duplicates.
1291
void Context::footnote(const std::string& input) {
2,255✔
1292
  if (input.length() && std::find(footnotes.begin(), footnotes.end(), input) == footnotes.end())
2,255✔
1293
    footnotes.push_back(input);
1,305✔
1294
}
2,255✔
1295

1296
////////////////////////////////////////////////////////////////////////////////
1297
// No duplicates.
1298
void Context::error(const std::string& input) {
492✔
1299
  if (input.length() && std::find(errors.begin(), errors.end(), input) == errors.end())
492✔
1300
    errors.push_back(input);
486✔
1301
}
492✔
1302

1303
////////////////////////////////////////////////////////////////////////////////
1304
void Context::debug(const std::string& input) {
16,409✔
1305
  if (input.length()) debugMessages.push_back(input);
16,409✔
1306
}
16,409✔
1307

1308
////////////////////////////////////////////////////////////////////////////////
1309
CurrentTask::CurrentTask(Context& context, const Task* task)
10,878✔
1310
    : context{context}, previous{context.currentTask} {
10,878✔
1311
  context.currentTask = task;
10,878✔
1312
}
10,878✔
1313

1314
////////////////////////////////////////////////////////////////////////////////
1315
CurrentTask::~CurrentTask() { context.currentTask = previous; }
10,878✔
1316

1317
////////////////////////////////////////////////////////////////////////////////
1318

1319
// 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