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

GothenburgBitFactory / taskwarrior / 9863873185

09 Jul 2024 08:39PM UTC coverage: 84.355% (+0.2%) from 84.172%
9863873185

push

github

web-flow
Add support for task expiration (#3546)

11 of 17 new or added lines in 4 files covered. (64.71%)

1 existing line in 1 file now uncovered.

19287 of 22864 relevant lines covered (84.36%)

20273.16 hits per line

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

6.77
/src/commands/CmdNews.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
#include <CmdNews.h>
29
#include <iostream>
30
#include <cmath>
31
#include <csignal>
32
#include <Table.h>
33
#include <Context.h>
34
#include <Datetime.h>
35
#include <Duration.h>
36
#include <shared.h>
37
#include <format.h>
38
#include <util.h>
39
#include <main.h>
40

41
/* Adding a new version:
42
 * 
43
 * - Add a new `versionX_Y_Z` method to `NewsItem`, and add news items for the new
44
 *   release.
45
 * - Call the new method in `NewsItem.all()`. Calls should be in version order.
46
 * - Test with `task news`.
47
 */
48

49
////////////////////////////////////////////////////////////////////////////////
50
CmdNews::CmdNews ()
4,352✔
51
{
52
  _keyword               = "news";
4,352✔
53
  _usage                 = "task          news";
4,352✔
54
  _description           = "Displays news about the recent releases";
4,352✔
55
  _read_only             = true;
4,352✔
56
  _displays_id           = false;
4,352✔
57
  _needs_gc              = false;
4,352✔
58
  _uses_context          = false;
4,352✔
59
  _accepts_filter        = false;
4,352✔
60
  _accepts_modifications = false;
4,352✔
61
  _accepts_miscellaneous = false;
4,352✔
62
  _category              = Command::Category::misc;
4,352✔
63
}
4,352✔
64

65
////////////////////////////////////////////////////////////////////////////////
66
static void signal_handler (int s)
×
67
{
68
  if (s == SIGINT)
×
69
  {
70
    Color footnote;
×
71
    if (Context::getContext ().color ()) {
×
72
      if (Context::getContext ().config.has ("color.footnote"))
×
73
        footnote = Color (Context::getContext ().config.get ("color.footnote"));
×
74
    }
75

76
    std::cout << "\n\nCome back and read about new features later!\n";
×
77

78
    std::cout << footnote.colorize (
×
79
      "\nIf you enjoy Taskwarrior, please consider supporting the project at:\n"
80
      "    https://github.com/sponsors/GothenburgBitFactory/\n"
81
    );
×
82
    exit (1);
×
83
  }
84
}
85

86
void wait_for_enter ()
×
87
{
88
  signal (SIGINT, signal_handler);
×
89

90
  std::string dummy;
×
91
  std::cout << "\nPress enter to continue..";
×
92
  std::getline (std::cin, dummy);
×
93
  std::cout << "\33[2K\033[A\33[2K";  // Erase current line, move up, and erase again
×
94

95
  signal (SIGINT, SIG_DFL);
×
96
}
97

98
////////////////////////////////////////////////////////////////////////////////
99
// Holds information about single improvement / bug.
100
//
101
NewsItem::NewsItem (
×
102
  Version version,
103
  const std::string& title,
104
  const std::string& bg_title,
105
  const std::string& background,
106
  const std::string& punchline,
107
  const std::string& update,
108
  const std::string& reasoning,
109
  const std::string& actions
110
) {
×
111
  _version = version;
×
112
  _title = title;
×
113
  _bg_title = bg_title;
×
114
  _background = background;
×
115
  _punchline = punchline;
×
116
  _update = update;
×
117
  _reasoning = reasoning;
×
118
  _actions = actions;
×
119
}
120

121
void NewsItem::render () {
×
122
  auto config = Context::getContext ().config;
×
123
  Color header;
×
124
  Color footnote;
×
125
  Color bold;
×
126
  Color underline;
×
127
  if (Context::getContext ().color ()) {
×
128
    bold = Color ("bold");
×
129
    underline = Color ("underline");
×
130
    if (config.has ("color.header"))
×
131
      header = Color (config.get ("color.header"));
×
132
    if (config.has ("color.footnote"))
×
133
      footnote = Color (config.get ("color.footnote"));
×
134
  }
135

136
  // TODO: For some reason, bold cannot be blended in 256-color terminals
137
  // Apply this workaround of colorizing twice.
138
  std::cout << bold.colorize (header.colorize (format ("{1} ({2})\n", _title, _version)));
×
139
  if (_background.size ()) {
×
140
    if (_bg_title.empty ())
×
141
      _bg_title = "Background";
×
142

143
    std::cout << "\n  " << underline.colorize (_bg_title) << std::endl
×
144
              << _background << std::endl;
×
145
  }
146

147
  wait_for_enter ();
×
148

149
  std::cout << "  " << underline.colorize (format ("What changed in {1}?\n", _version));
×
150
  if (_punchline.size ())
×
151
    std::cout << footnote.colorize (format ("{1}\n", _punchline));
×
152

153
  if (_update.size ())
×
154
    std::cout << format ("{1}\n", _update);
×
155

156
  wait_for_enter ();
×
157

158
  if (_reasoning.size ()) {
×
159
    std::cout << "  " << underline.colorize ("What was the motivation behind this feature?\n")
×
160
              << _reasoning << std::endl;
×
161
    wait_for_enter ();
×
162
  }
163

164
  if (_actions.size ()) {
×
165
    std::cout << "  " << underline.colorize ("What do I have to do?\n")
×
166
              << _actions << std::endl;
×
167
    wait_for_enter ();
×
168
  }
169
}
170

171
std::vector<NewsItem> NewsItem::all () {
×
172
  std::vector<NewsItem> items;
×
173
  version2_6_0(items);
×
174
  version3_0_0(items);
×
NEW
175
  version3_1_0(items);
×
UNCOV
176
  return items;
×
177
}
178

179
////////////////////////////////////////////////////////////////////////////////
180
// Generate the highlights for the 2.6.0 version.
181
//
182
// - XDG directory mode (high)
183
// - Support for Unicode 11 characters (high)
184
// - 64 bit values, UDAs, Datetime values until year 9999 (high)
185
// - Config context variables
186
// - Reports outside of context
187
// - Environment variables in taskrc (high)
188
// - Waiting is a virtual concept (high)
189
// - Improved parser and task display mechanism
190
// - The .by attribute modifier
191
// - Exporting a report
192
// - Multi-day holidays
193
void NewsItem::version2_6_0 (std::vector<NewsItem>& items) {
×
194
  Version version("2.6.0");
×
195
  /////////////////////////////////////////////////////////////////////////////
196
  // - Writeable context
197

198
  // Detect whether user uses any contexts
199
  auto config = Context::getContext ().config;
×
200
  std::stringstream advice;
×
201

202
  auto defined = CmdContext::getContexts ();
×
203
  if (defined.size ())
×
204
  {
205
    // Detect the old-style contexts
206
    std::vector<std::string> old_style;
×
207
    std::copy_if (
×
208
      defined.begin(),
209
      defined.end(),
210
      std::back_inserter(old_style),
211
      [&](auto& name){return config.has ("context." + name);}
×
212
    );
213

214
    if (old_style.size ())
×
215
    {
216
      advice << format (
×
217
        "  You have {1} defined contexts, out of which {2} are old-style:\n",
218
        defined.size (),
219
        std::count_if (
220
          defined.begin (),
221
          defined.end (),
222
          [&](auto& name){return config.has ("context." + name);}
×
223
      ));
×
224

225
      for (auto context: defined) {
×
226
        std::string old_definition = config.get ("context." + context);
×
227
        if (old_definition != "")
×
228
          advice << format ("  * {1}: {2}\n", context, old_definition);
×
229
      }
230

231
      advice << "\n"
232
                "  These need to be migrated to new-style, which uses context.<name>.read and\n"
233
                "  context.<name>.write config variables. Please run the following commands:\n";
×
234

235
      for (auto context: defined) {
×
236
        std::string old_definition = config.get ("context." + context);
×
237
        if (old_definition != "")
×
238
          advice << format ("  $ task context define {1} '{2}'\n", context, old_definition);
×
239
      }
240

241
      advice << "\n"
242
                "  Please check these filters are also valid modifications. If a context filter is not\n"
243
                "  a valid modification, you can set the context.<name>.write configuration variable to\n"
244
                "  specify the write context explicitly. Read more in CONTEXT section of man taskrc.";
×
245
    }
246
    else
247
      advice << "  You don't have any old-style contexts defined, so you're good to go as is!";
×
248
  }
249
  else
250
    advice << "  You don't have any contexts defined, so you're good to go as is!\n"
251
              "  Read more about how to use contexts in CONTEXT section of 'man task'.";
×
252

253
  NewsItem writeable_context (
254
    version,
255
    "'Writeable' context",
256
    "Background - what is context?",
257
    "  The 'context' is a feature (introduced in 2.5.0) that allows users to apply a\n"
258
    "  predefined filter to all task reports.\n"
259
    "  \n"
260
    "    $ task context define work \"project:Work or +urgent\"\n"
261
    "    $ task context work\n"
262
    "    Context 'work' set. Use 'task context none' to remove.\n"
263
    "  \n"
264
    "  Now if we proceed to add two tasks:\n"
265
    "    $ task add Talk to Jeff pro:Work\n"
266
    "    $ task add Call mom pro:Personal\n"
267
    "  \n"
268
    "    $ task\n"
269
    "    ID Age   Project Description  Urg\n"
270
    "     1 16s   Work    Talk to Jeff    1\n"
271
    "  \n"
272
    "  The task \"Call mom\" will not be listed, because it does not match\n"
273
    "  the active context (its project is 'Personal' and not 'Work').",
274
    "  The currently active context definition is now applied as default modifications\n"
275
    "  when creating new tasks using 'task add' and 'task log'.",
276
    "  \n"
277
    "  Consider following example, using context 'work' defined as 'project:Work' above:\n"
278
    "  \n"
279
    "    $ task context work\n"
280
    "    $ task add Talk to Jeff\n"
281
    "    $ task\n"
282
    "    ID Age  Project Description  Urg \n"
283
    "     1 1s   Work    Talk to Jeff    1\n"
284
    "            ^^^^^^^\n"
285
    "  \n"
286
    "  Note that project attribute was set to 'Work' automatically.",
287
    "  This was a popular feature request. Now, if you have a context active,\n"
288
    "  newly added tasks no longer \"fall outside\" of the context by default.",
289
    advice.str ()
×
290
  );
×
291
  items.push_back(writeable_context);
×
292

293
  /////////////////////////////////////////////////////////////////////////////
294
  // - 64-bit datetime support
295

296
  NewsItem uint64_support (
297
    version,
298
    "Support for 64-bit timestamps and numeric values",
299
    "",
300
    "",
301
    "  Taskwarrior now supports 64-bit timestamps, making it possible to set due dates\n"
302
    "  and other date attributes beyond 19 January 2038 (limit of 32-bit timestamps).\n",
303
    "  The current limit is 31 December 9999 for display reasons (last 4-digit year).",
304
    "  With each year passing by faster than the last, setting tasks for 2040s\n"
305
    "  is not as unfeasible as it once was.",
306
    "  Don't forget that 50-year anniversary and 'task add' a long-term task today!"
307
  );
×
308
  items.push_back(uint64_support);
×
309

310
  /////////////////////////////////////////////////////////////////////////////
311
  // - Waiting is a virtual status
312

313
  NewsItem waiting_status (
314
    version,
315
    "Deprecation of the status:waiting",
316
    "",
317
    "  If a task has a 'wait' attribute set to a date in the future, it is modified\n"
318
    "  to have a 'waiting' status. Once that date is no longer in the future, the status\n"
319
    "  is modified to back to 'pending'.",
320
    "  The 'waiting' value of status is deprecated, instead users should use +WAITING\n"
321
    "  virtual tag, or explicitly query for wait.after:now (the two are equivalent).",
322
    "  \n"
323
    "  The status:waiting query still works in 2.6.0, but support will be dropped in 3.0.",
324
    "",
325
    "  In your custom report definitions, the following expressions should be replaced:\n"
326
    "  * 'status:pending or status:waiting' should be replaced by 'status:pending'\n"
327
    "  * 'status:pending' should be replaced by 'status:pending -WAITING'"
328
  );
×
329
  items.push_back(waiting_status);
×
330

331
  /////////////////////////////////////////////////////////////////////////////
332
  // - Support for environment variables in the taskrc
333

334
  NewsItem env_vars (
335
    version,
336
    "Environment variables in the taskrc",
337
    "",
338
    "",
339
    "  Taskwarrior now supports expanding environment variables in the taskrc file,\n"
340
    "  allowing users to customize the behaviour of 'task' based on the current env.\n",
341
    "  The environment variables can either be used in paths, or as separate values:\n"
342
    "    data.location=$XDG_DATA_HOME/task/\n"
343
    "    default.project=$PROJECT",
344
    "",
345
    ""
346
  );
×
347
  items.push_back(env_vars);
×
348

349
  /////////////////////////////////////////////////////////////////////////////
350
  // - Reports outside of context
351

352
  NewsItem contextless_reports (
353
    version,
354
    "Context-less reports",
355
    "",
356
    "  By default, every report is affected by currently active context.",
357
    "  You can now make a selected report ignore currently active context by setting\n"
358
    "  'report.<name>.context' configuration variable to 0.",
359
    "",
360
    "  This is useful for users who utilize a single place (such as project:Inbox)\n"
361
    "  to collect their new tasks that are then triaged on a regular basis\n"
362
    "  (such as in GTD methodology).\n"
363
    "  \n"
364
    "  In such a case, defining a report that filters for project:Inbox and making it\n"
365
    "  fully accessible from any context is a major usability improvement.",
366
    ""
367
  );
×
368
  items.push_back(contextless_reports);
×
369

370
  /////////////////////////////////////////////////////////////////////////////
371
  // - Exporting a particular report
372

373
  NewsItem exportable_reports (
374
    version,
375
    "Exporting a particular report",
376
    "",
377
    "",
378
    "  You can now export the tasks listed by a particular report as JSON by simply\n"
379
    "  calling 'task export <report>'.\n",
380
    "  The export mirrors the filter and the sort order of the report.",
381
    "  This feature can be used to quickly process the data displayed in a particular\n"
382
    "  report using other CLI tools. For example, the following oneliner\n"
383
    "  \n"
384
    "      $ task export next | jq '.[].urgency' | datamash mean 1\n"
385
    "      3.3455535142857\n"
386
    "  \n"
387
    "  combines jq and GNU datamash to compute average urgency of the tasks displayed\n"
388
    "  in the 'next' report.",
389
    ""
390
  );
×
391
  items.push_back(exportable_reports);
×
392

393
  /////////////////////////////////////////////////////////////////////////////
394
  // - Multi-day holidays
395

396
  NewsItem multi_holidays (
397
    version,
398
    "Multi-day holidays",
399
    "",
400
    "  Holidays are currently used in 'task calendar' to visualize the workload during\n"
401
    "  the upcoming weeks/months. Up to date country-specific holiday data files can be\n"
402
    "  obtained from our website, holidata.net.",
403
    "  Instead of single-day holiday entries only, Taskwarrior now supports holidays\n"
404
    "  that span a range of days (i.e. vacation).\n",
405
    "  Use a holiday.<name>.start and holiday.<name>.end to configure a multi-day holiday:\n"
406
    "  \n"
407
    "      holiday.sysadmin.name=System Administrator Appreciation Week\n"
408
    "      holiday.sysadmin.start=20100730\n"
409
    "      holiday.sysadmin.end=20100805",
410
    "",
411
    ""
412
  );
×
413
  items.push_back(multi_holidays);
×
414

415
  /////////////////////////////////////////////////////////////////////////////
416
  // - Unicode 12
417

418
  NewsItem unicode_12 (
419
    version,
420
    "Extended Unicode support (Unicode 12)",
421
    "",
422
    "",
423
    "  The support for Unicode character set was improved to support Unicode 12.\n"
424
    "  This means better support for various language-specific characters - and emojis!",
425
    "",
426
    "  Extended unicode support for language-specific characters helps non-English speakers.\n"
427
    "  While most users don't enter emojis as task metadata, automated task creation tools,\n"
428
    "  such as bugwarrior, might create tasks with exotic Unicode data.",
429
    "  You can try it out - 'task add Prepare for an 👽 invasion!'"
430
  );
×
431
  items.push_back(unicode_12);
×
432

433
  /////////////////////////////////////////////////////////////////////////////
434
  // - The .by attribute modifier
435

436
  NewsItem by_modifier (
437
    version,
438
    "The .by attribute modifier",
439
    "",
440
    "",
441
    "  A new attribute modifier '.by' was introduced, equivalent to the operator '<='.\n",
442
    "  This modifier can be used to list all tasks due by the end of the months,\n"
443
    "  including the last day of the month, using: 'due.by:eom' query",
444
    "  There was no convenient way to express '<=' relation using attribute modifiers.\n"
445
    "  As a workaround, instead of 'due.by:eom' one could use 'due.before:eom+1d',\n"
446
    "  but that requires a certain amount of mental overhead.",
447
    ""
448
  );
×
449
  items.push_back(by_modifier);
×
450

451
  /////////////////////////////////////////////////////////////////////////////
452
  // - Context-specific configuration overrides
453

454
  NewsItem context_config (
455
    version,
456
    "Context-specific configuration overrides",
457
    "",
458
    "",
459
    "  Any context can now define context-specific configuration overrides\n"
460
    "  via context.<name>.rc.<setting>=<value>.\n",
461
    "  This allows the user to customize the behaviour of Taskwarrior in a given context,\n"
462
    "  for example, to change the default command in the 'work' context to 'overdue':\n"
463
    "\n"
464
    "      $ task config context.work.rc.default.command overdue\n"
465
    "\n"
466
    "  Another example would be to ensure that while context 'work' is active, tasks get\n"
467
    "  stored in a ~/.worktasks directory:\n"
468
    "\n"
469
    "      $ task config context.work.rc.data.location=~/.worktasks",
470
    "",
471
    ""
472
  );
×
473
  items.push_back(context_config);
×
474

475
  /////////////////////////////////////////////////////////////////////////////
476
  // - XDG config home support
477

478
  NewsItem xdg_support (
479
    version,
480
    "Support for XDG Base Directory Specification",
481
    "",
482
    "  The XDG Base Directory specification provides standard locations to store\n"
483
    "  application data, configuration, state, and cached data in order to keep $HOME\n"
484
    "  clutter-free. The locations are usually set to ~/.local/share, ~/.config,\n"
485
    "  ~/.local/state and ~/.cache respectively.",
486
    "  If taskrc is not found at '~/.taskrc', Taskwarrior will attempt to find it\n"
487
    "  at '$XDG_CONFIG_HOME/task/taskrc' (defaults to '~/.config/task/taskrc').",
488
    "",
489
    "  This allows users to fully follow XDG Base Directory Spec by moving their taskrc:\n"
490
    "      $ mkdir $XDG_CONFIG_HOME/task\n"
491
    "      $ mv ~/.taskrc $XDG_CONFIG_HOME/task/taskrc\n\n"
492
    "  and further setting:\n"
493
    "      data.location=$XDG_DATA_HOME/task/\n"
494
    "      hooks.location=$XDG_CONFIG_HOME/task/hooks/\n\n"
495
    "  Solutions in the past required symlinks or more cumbersome configuration overrides.",
496
    "  If you configure your data.location and hooks.location as above, ensure\n"
497
    "  that the XDG_DATA_HOME and XDG_CONFIG_HOME environment variables are set,\n"
498
    "  otherwise they're going to expand to empty string. Alternatively you can\n"
499
    "  hardcode the desired paths on your system."
500
  );
×
501
  items.push_back(xdg_support);
×
502

503
  /////////////////////////////////////////////////////////////////////////////
504
  // - Update holiday data
505

506
  NewsItem holidata_2022 (
507
    version,
508
    "Updated holiday data for 2022",
509
    "",
510
    "",
511
    "  Holiday data has been refreshed for 2022 and five more holiday locales\n"
512
    "  have been added: fr-CA, hu-HU, pt-BR, sk-SK and sv-FI.",
513
    "",
514
    "  Refreshing the holiday data is part of every release. The addition of the new\n"
515
    "  locales allows us to better support users in those particular countries."
516
  );
×
517
  items.push_back(holidata_2022);
×
518
}
519

520
void NewsItem::version3_0_0 (std::vector<NewsItem>& items) {
×
521
  Version version("3.0.0");
×
522
  NewsItem sync {
523
    version,
524
    /*title=*/"New data model and sync backend",
525
    /*bg_title=*/"",
526
    /*background=*/"",
527
    /*punchline=*/
528
    "The sync functionality for Taskwarrior has been rewritten entirely, and no longer\n"
529
    "supports taskserver/taskd. The most robust solution is a cloud-storage backend,\n"
530
    "although a less-mature taskchampion-sync-server is also available. See `task-sync(5)`\n"
531
    "For details. As part of this change, the on-disk storage format has also changed.\n",
532
    /*update=*/
533
    "This is a breaking upgrade: you must export your task database from 2.x and re-import\n"
534
    "it into 3.x. Hooks run during task import, so if you have any hooks defined,\n"
535
    "temporarily disable them for this operation.\n\n"
536
    "See https://taskwarrior.org/docs/upgrade-3/ for information on upgrading to Taskwarrior 3.0.",
537
  };
×
538
  items.push_back(sync);
×
539
}
540

NEW
541
void NewsItem::version3_1_0 (std::vector<NewsItem>& items) {
×
NEW
542
  Version version("3.1.0");
×
543
  NewsItem sync {
544
    version,
545
    /*title=*/"Purging and Expiring Tasks",
546
    /*bg_title=*/"",
547
    /*background=*/"",
548
    /*punchline=*/
549
    "Support for `task purge` has been restored, and new support added for automatically expiring\n"
550
    "old tasks.\n\n"
551
    /*update=*/
552
    "The `task purge` command removes tasks entirely, in contrast to `task delete` which merely sets\n"
553
    "the task status to 'Deleted'. This functionality existed in versions 2.x but was temporarily\n"
554
    "removed in 3.0.\n\n"
555
    "The new `expiration.on-sync` configuration parameter controls automatic expiration of old tasks.\n"
556
    "An old task is one with status 'Deleted' that has not been modified in 180 days. This\n"
557
    "functionality is optional and not enabled by default."
NEW
558
  };
×
NEW
559
  items.push_back(sync);
×
560
}
561

562
////////////////////////////////////////////////////////////////////////////////
563
int CmdNews::execute (std::string& output)
×
564
{
565
  auto words = Context::getContext ().cli2.getWords ();
×
566
  auto config = Context::getContext ().config;
×
567

568
  // Supress compiler warning about unused argument
569
  output = "";
×
570

571
  std::vector<NewsItem> items = NewsItem::all();
×
572
  Version news_version(Context::getContext ().config.get ("news.version"));
×
573
  Version current_version = Version::Current();
×
574

575
  // 2.6.0 is the earliest version with news support.
576
  if (!news_version.is_valid())
×
577
    news_version = Version("2.6.0");
×
578

579
  signal (SIGINT, signal_handler);
×
580

581
  // Remove items that have already been shown
582
    items.erase (
×
583
      std::remove_if (items.begin (), items.end (), [&](const NewsItem& n){return n._version <= news_version;}),
×
584
      items.end ()
×
585
    );
586

587

588
  Color bold = Color ("bold");
×
589
  if (items.empty ()) {
×
590
    std::cout << bold.colorize ("You are up to date!\n");
×
591
  } else {
592
    // Print release notes
593
    std::cout << bold.colorize (format (
×
594
      "\n"
595
      "================================================\n"
596
      "Taskwarrior {1} through {2} Release Highlights\n"
597
      "================================================\n",
598
      news_version,
599
      current_version));
×
600

601
    for (unsigned short i=0; i < items.size (); i++) {
×
602
      std::cout << format ("\n({1}/{2}) ", i+1, items.size ());
×
603
      items[i].render ();
×
604
    }
605
    std::cout << "Thank you for catching up on the new features!\n";
×
606
  }
607
  wait_for_enter ();
×
608

609
  // Display outro
610
  Datetime now;
×
611
  Datetime beginning (2006, 11, 29);
×
612
  Duration development_time = Duration (now - beginning);
×
613

614
  Color underline = Color ("underline");
×
615

616
  std::stringstream outro;
×
617
  outro << underline.colorize (bold.colorize ("Taskwarrior crowdfunding\n"));
×
618
  outro << format (
×
619
    "Taskwarrior has been in development for {1} years but its survival\n"
620
    "depends on your support!\n\n"
621
    "Please consider joining our {2} fundraiser to help us fund maintenance\n"
622
    "and development of new features:\n\n",
623
    std::lround (static_cast<float>(development_time.days ()) / 365.25),
×
624
    now.year ()
625
  );
×
626
  outro << bold.colorize("    https://github.com/sponsors/GothenburgBitFactory/\n\n");
×
627
  outro << "Perks are available for our sponsors.\n";
×
628

629
  std::cout << outro.str ();
×
630

631
  // Set a mark in the config to remember which version's release notes were displayed
632
  if (news_version != current_version)
×
633
  {
634
    std::cout << "UPDATING\n";
×
635
    CmdConfig::setConfigVariable ("news.version", std::string(current_version), false);
×
636

637
    // Revert back to default signal handling after displaying the outro
638
    signal (SIGINT, SIG_DFL);
×
639

640
    std::string question = format (
641
      "\nWould you like to open Taskwarrior {1} fundraising campaign to read more?",
642
      now.year ()
643
    );
×
644

645
    std::vector <std::string> options {"yes", "no"};
×
646
    std::vector <std::string> matches;
×
647

648
    std::cout << question << " (YES/no) ";
×
649

650
    std::string answer;
×
651
    std::getline (std::cin, answer);
×
652

653
    if (std::cin.eof () || trim (answer).empty ())
×
654
      answer = "yes";
×
655
    else
656
      lowerCase (trim (answer));
×
657

658
    autoComplete (answer, options, matches, 1); // Hard-coded 1.
×
659

660
    if (matches.size () == 1 && matches[0] == "yes")
×
661
#if defined (DARWIN)
662
      system ("open 'https://github.com/sponsors/GothenburgBitFactory/'");
663
#else
664
      system ("xdg-open 'https://github.com/sponsors/GothenburgBitFactory/'");
×
665
#endif
666

667
    std::cout << std::endl;
×
668
  }
669
  else
670
    wait_for_enter ();  // Do not display the outro and footnote at once
×
671

672
  return 0;
×
673
}
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

© 2026 Coveralls, Inc