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 <CmdHistory.h>
29 #include <sstream>
30 #include <Context.h>
31 #include <Filter.h>
32 #include <Table.h>
33 #include <main.h>
34 #include <format.h>
35 #include <util.h>
36 #include <Datetime.h>
37
38 #define STRING_CMD_HISTORY_YEAR "Year"
39 #define STRING_CMD_HISTORY_MONTH "Month"
40 #define STRING_CMD_HISTORY_DAY "Day"
41 #define STRING_CMD_HISTORY_ADDED "Added"
42 #define STRING_CMD_HISTORY_COMP "Completed"
43 #define STRING_CMD_HISTORY_DEL "Deleted"
44
45 ////////////////////////////////////////////////////////////////////////////////
46 template<class HistoryStrategy>
CmdHistoryBase()47 CmdHistoryBase<HistoryStrategy>::CmdHistoryBase ()
48 {
49 _keyword = HistoryStrategy::keyword;
50 _usage = HistoryStrategy::usage;
51 _description = HistoryStrategy::description;
52
53 _read_only = true;
54 _displays_id = false;
55 _needs_gc = false;
56 _uses_context = true;
57 _accepts_filter = true;
58 _accepts_modifications = false;
59 _accepts_miscellaneous = false;
60 _category = Command::Category::graphs;
61 }
62
63 ////////////////////////////////////////////////////////////////////////////////
64 template<class HistoryStrategy>
outputGraphical(std::string & output)65 void CmdHistoryBase<HistoryStrategy>::outputGraphical (std::string& output)
66 {
67 auto widthOfBar = Context::getContext ().getWidth () - HistoryStrategy::labelWidth;
68
69 // Now build the view.
70 Table view;
71 setHeaderUnderline (view);
72 view.width (Context::getContext ().getWidth ());
73
74 HistoryStrategy::setupTableDates (view);
75
76 view.add ("Number Added/Completed/Deleted", true, false); // Fixed.
77
78 Color color_add (Context::getContext ().config.get ("color.history.add"));
79 Color color_done (Context::getContext ().config.get ("color.history.done"));
80 Color color_delete (Context::getContext ().config.get ("color.history.delete"));
81 Color label (Context::getContext ().config.get ("color.label"));
82
83 // Determine the longest line, and the longest "added" line.
84 auto maxAddedLine = 0;
85 auto maxRemovedLine = 0;
86 for (auto& i : groups)
87 {
88 if (completedGroup[i.first] + deletedGroup[i.first] > maxRemovedLine)
89 maxRemovedLine = completedGroup[i.first] + deletedGroup[i.first];
90
91 if (addedGroup[i.first] > maxAddedLine)
92 maxAddedLine = addedGroup[i.first];
93 }
94
95 auto maxLine = maxAddedLine + maxRemovedLine;
96 if (maxLine > 0)
97 {
98 unsigned int leftOffset = (widthOfBar * maxAddedLine) / maxLine;
99
100 auto totalAdded = 0;
101 auto totalCompleted = 0;
102 auto totalDeleted = 0;
103
104 time_t priorTime = 0;
105 auto row = 0;
106 for (auto& i : groups)
107 {
108 row = view.addRow ();
109
110 totalAdded += addedGroup[i.first];
111 totalCompleted += completedGroup[i.first];
112 totalDeleted += deletedGroup[i.first];
113
114 HistoryStrategy::insertRowDate (view, row, i.first, priorTime);
115 priorTime = i.first;
116
117 unsigned int addedBar = (widthOfBar * addedGroup[i.first]) / maxLine;
118 unsigned int completedBar = (widthOfBar * completedGroup[i.first]) / maxLine;
119 unsigned int deletedBar = (widthOfBar * deletedGroup[i.first]) / maxLine;
120
121 std::string bar;
122 if (Context::getContext ().color ())
123 {
124 std::string aBar;
125 if (addedGroup[i.first])
126 {
127 aBar = format (addedGroup[i.first]);
128 while (aBar.length () < addedBar)
129 aBar = ' ' + aBar;
130 }
131
132 std::string cBar;
133 if (completedGroup[i.first])
134 {
135 cBar = format (completedGroup[i.first]);
136 while (cBar.length () < completedBar)
137 cBar = ' ' + cBar;
138 }
139
140 std::string dBar;
141 if (deletedGroup[i.first])
142 {
143 dBar = format (deletedGroup[i.first]);
144 while (dBar.length () < deletedBar)
145 dBar = ' ' + dBar;
146 }
147
148 bar += std::string (leftOffset - aBar.length (), ' ');
149 bar += color_add.colorize (aBar);
150 bar += color_done.colorize (cBar);
151 bar += color_delete.colorize (dBar);
152 }
153 else
154 {
155 std::string aBar; while (aBar.length () < addedBar) aBar += '+';
156 std::string cBar; while (cBar.length () < completedBar) cBar += 'X';
157 std::string dBar; while (dBar.length () < deletedBar) dBar += '-';
158
159 bar += std::string (leftOffset - aBar.length (), ' ');
160 bar += aBar + cBar + dBar;
161 }
162
163 view.set (row, HistoryStrategy::dateFieldCount + 0, bar);
164 }
165 }
166
167 std::stringstream out;
168 if (view.rows ())
169 {
170 out << optionalBlankLine ()
171 << view.render ()
172 << '\n';
173
174 if (Context::getContext ().color ())
175 out << format ("Legend: {1}, {2}, {3}",
176 color_add.colorize (STRING_CMD_HISTORY_ADDED),
177 color_done.colorize (STRING_CMD_HISTORY_COMP),
178 color_delete.colorize (STRING_CMD_HISTORY_DEL))
179 << optionalBlankLine ()
180 << '\n';
181 else
182 out << "Legend: + Added, X Completed, - Deleted\n";
183 }
184 else
185 {
186 Context::getContext ().footnote ("No tasks.");
187 rc = 1;
188 }
189
190 output = out.str ();
191 }
192
193 ////////////////////////////////////////////////////////////////////////////////
194 template<class HistoryStrategy>
outputTabular(std::string & output)195 void CmdHistoryBase<HistoryStrategy>::outputTabular (std::string& output)
196 {
197 Table view;
198 setHeaderUnderline (view);
199 view.width (Context::getContext ().getWidth ());
200
201 HistoryStrategy::setupTableDates (view);
202
203 view.add (STRING_CMD_HISTORY_ADDED, false);
204 view.add (STRING_CMD_HISTORY_COMP, false);
205 view.add (STRING_CMD_HISTORY_DEL, false);
206 view.add ("Net", false);
207
208 auto totalAdded = 0;
209 auto totalCompleted = 0;
210 auto totalDeleted = 0;
211
212 auto row = 0;
213 time_t lastTime = 0;
214 for (auto& i : groups)
215 {
216 row = view.addRow ();
217
218 totalAdded += addedGroup [i.first];
219 totalCompleted += completedGroup [i.first];
220 totalDeleted += deletedGroup [i.first];
221
222 HistoryStrategy::insertRowDate (view, row, i.first, lastTime);
223 lastTime = i.first;
224
225 auto net = 0;
226
227 if (addedGroup.find (i.first) != addedGroup.end ())
228 {
229 view.set (row, HistoryStrategy::dateFieldCount + 0, addedGroup[i.first]);
230 net +=addedGroup[i.first];
231 }
232
233 if (completedGroup.find (i.first) != completedGroup.end ())
234 {
235 view.set (row, HistoryStrategy::dateFieldCount + 1, completedGroup[i.first]);
236 net -= completedGroup[i.first];
237 }
238
239 if (deletedGroup.find (i.first) != deletedGroup.end ())
240 {
241 view.set (row, HistoryStrategy::dateFieldCount + 2, deletedGroup[i.first]);
242 net -= deletedGroup[i.first];
243 }
244
245 Color net_color;
246 if (Context::getContext ().color () && net)
247 net_color = net > 0
248 ? Color (Color::red)
249 : Color (Color::green);
250
251 view.set (row, HistoryStrategy::dateFieldCount + 3, net, net_color);
252 }
253
254 if (view.rows ())
255 {
256 row = view.addRow();
257 view.set (row, 1, " ");
258 row = view.addRow ();
259
260 Color row_color;
261 if (Context::getContext ().color ())
262 row_color = Color (Color::nocolor, Color::nocolor, false, true, false);
263
264 view.set (row, HistoryStrategy::dateFieldCount - 1, "Average", row_color);
265 view.set (row, HistoryStrategy::dateFieldCount + 0, totalAdded / (view.rows () - 2), row_color);
266 view.set (row, HistoryStrategy::dateFieldCount + 1, totalCompleted / (view.rows () - 2), row_color);
267 view.set (row, HistoryStrategy::dateFieldCount + 2, totalDeleted / (view.rows () - 2), row_color);
268 view.set (row, HistoryStrategy::dateFieldCount + 3, (totalAdded - totalCompleted - totalDeleted) / (view.rows () - 2), row_color);
269 }
270
271 std::stringstream out;
272 if (view.rows ())
273 out << optionalBlankLine ()
274 << view.render ()
275 << '\n';
276 else
277 {
278 Context::getContext ().footnote ("No tasks.");
279 rc = 1;
280 }
281
282 output = out.str ();
283 }
284
285 ////////////////////////////////////////////////////////////////////////////i
286 class MonthlyHistoryStrategy
287 {
288 public:
getRelevantDate(const Datetime & dt)289 static Datetime getRelevantDate (const Datetime& dt)
290 {
291 return dt.startOfMonth ();
292 }
293
setupTableDates(Table & view)294 static void setupTableDates (Table& view)
295 {
296 view.add (STRING_CMD_HISTORY_YEAR, true);
297 view.add (STRING_CMD_HISTORY_MONTH, true);
298 }
299
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)300 static void insertRowDate (
301 Table& view,
302 int row,
303 time_t rowTime,
304 time_t lastTime)
305 {
306 Datetime dt (rowTime);
307 int m, d, y;
308 dt.toYMD (y, m, d);
309
310 Datetime last_dt (lastTime);
311 int last_m, last_d, last_y;
312 last_dt.toYMD (last_y, last_m, last_d);
313
314 if (y != last_y)
315 view.set (row, 0, y);
316
317 view.set (row, 1, Datetime::monthName (m));
318 }
319
320 static constexpr const char* keyword = "history.monthly";
321 static constexpr const char* usage = "task <filter> history.monthly";
322 static constexpr const char* description = "Shows a report of task history, by month";
323 static constexpr unsigned int dateFieldCount = 2;
324 static constexpr bool graphical = false;
325 static constexpr unsigned int labelWidth = 0; // unused.
326 };
327
328 ////////////////////////////////////////////////////////////////////////////////
329 template<class HistoryStrategy>
execute(std::string & output)330 int CmdHistoryBase<HistoryStrategy>::execute (std::string& output)
331 {
332 rc = 0;
333
334 // TODO is this necessary?
335 groups.clear ();
336 addedGroup.clear ();
337 deletedGroup.clear ();
338 completedGroup.clear ();
339
340 // Apply filter.
341 handleUntil ();
342 handleRecurrence ();
343 Filter filter;
344 std::vector <Task> filtered;
345 filter.subset (filtered);
346
347 for (auto& task : filtered)
348 {
349 Datetime entry (task.get_date ("entry"));
350
351 Datetime end;
352 if (task.has ("end"))
353 end = Datetime (task.get_date ("end"));
354
355 auto epoch = HistoryStrategy::getRelevantDate (entry).toEpoch ();
356 groups[epoch] = 0;
357
358 // Every task has an entry date, but exclude templates.
359 if (task.getStatus () != Task::recurring)
360 ++addedGroup[epoch];
361
362 // All deleted tasks have an end date.
363 if (task.getStatus () == Task::deleted)
364 {
365 epoch = HistoryStrategy::getRelevantDate (end).toEpoch ();
366 groups[epoch] = 0;
367 ++deletedGroup[epoch];
368 }
369
370 // All completed tasks have an end date.
371 else if (task.getStatus () == Task::completed)
372 {
373 epoch = HistoryStrategy::getRelevantDate (end).toEpoch ();
374 groups[epoch] = 0;
375 ++completedGroup[epoch];
376 }
377 }
378
379 // Now build the view.
380 if (HistoryStrategy::graphical)
381 this->outputGraphical (output);
382 else
383 this->outputTabular (output);
384
385 return rc;
386 }
387
388 ////////////////////////////////////////////////////////////////////////////i
389 class MonthlyGHistoryStrategy
390 {
391 public:
getRelevantDate(const Datetime & dt)392 static Datetime getRelevantDate (const Datetime& dt)
393 {
394 return dt.startOfMonth ();
395 }
396
setupTableDates(Table & view)397 static void setupTableDates (Table& view)
398 {
399 view.add (STRING_CMD_HISTORY_YEAR, true);
400 view.add (STRING_CMD_HISTORY_MONTH, true);
401 }
402
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)403 static void insertRowDate (
404 Table& view,
405 int row,
406 time_t rowTime,
407 time_t lastTime)
408 {
409 Datetime dt (rowTime);
410 int m, d, y;
411 dt.toYMD (y, m, d);
412
413 Datetime last_dt (lastTime);
414 int last_m, last_d, last_y;
415 last_dt.toYMD (last_y, last_m, last_d);
416
417 if (y != last_y)
418 view.set (row, 0, y);
419
420 view.set (row, 1, Datetime::monthName (m));
421 }
422
423 static constexpr const char* keyword = "ghistory.monthly";
424 static constexpr const char* usage = "task <filter> ghistory.monthly";
425 static constexpr const char* description = "Shows a graphical report of task history, by month";
426 static constexpr unsigned int dateFieldCount = 2;
427 static constexpr bool graphical = true;
428 static constexpr unsigned int labelWidth = 15; // length '2017 September ' = 15
429 };
430
431 ////////////////////////////////////////////////////////////////////////////i
432 class AnnualGHistoryStrategy
433 {
434 public:
getRelevantDate(const Datetime & dt)435 static Datetime getRelevantDate (const Datetime& dt)
436 {
437 return dt.startOfYear ();
438 }
439
setupTableDates(Table & view)440 static void setupTableDates (Table& view)
441 {
442 view.add (STRING_CMD_HISTORY_YEAR, true);
443 }
444
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)445 static void insertRowDate (
446 Table& view,
447 int row,
448 time_t rowTime,
449 time_t lastTime)
450 {
451 Datetime dt (rowTime);
452 int m, d, y;
453 dt.toYMD (y, m, d);
454
455 Datetime last_dt (lastTime);
456 int last_m, last_d, last_y;
457 last_dt.toYMD (last_y, last_m, last_d);
458
459 if (y != last_y)
460 view.set (row, 0, y);
461 }
462
463 static constexpr const char* keyword = "ghistory.annual";
464 static constexpr const char* usage = "task <filter> ghistory.annual";
465 static constexpr const char* description = "Shows a graphical report of task history, by year";
466 static constexpr unsigned int dateFieldCount = 1;
467 static constexpr bool graphical = true;
468 static constexpr unsigned int labelWidth = 5; // length '2017 ' = 5
469 };
470
471 ////////////////////////////////////////////////////////////////////////////i
472 class AnnualHistoryStrategy
473 {
474 public:
getRelevantDate(const Datetime & dt)475 static Datetime getRelevantDate (const Datetime& dt)
476 {
477 return dt.startOfYear ();
478 }
479
setupTableDates(Table & view)480 static void setupTableDates (Table& view)
481 {
482 view.add (STRING_CMD_HISTORY_YEAR, true);
483 }
484
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)485 static void insertRowDate (
486 Table& view,
487 int row,
488 time_t rowTime,
489 time_t lastTime)
490 {
491 Datetime dt (rowTime);
492 int m, d, y;
493 dt.toYMD (y, m, d);
494
495 Datetime last_dt (lastTime);
496 int last_m, last_d, last_y;
497 last_dt.toYMD (last_y, last_m, last_d);
498
499 if (y != last_y)
500 view.set (row, 0, y);
501 }
502
503 static constexpr const char* keyword = "history.annual";
504 static constexpr const char* usage = "task <filter> history.annual";
505 static constexpr const char* description = "Shows a report of task history, by year";
506 static constexpr unsigned int dateFieldCount = 1;
507 static constexpr bool graphical = false;
508 static constexpr unsigned int labelWidth = 0; // unused.
509 };
510
511
512 ////////////////////////////////////////////////////////////////////////////i
513 class DailyHistoryStrategy
514 {
515 public:
getRelevantDate(const Datetime & dt)516 static Datetime getRelevantDate (const Datetime& dt)
517 {
518 return dt.startOfDay ();
519 }
520
setupTableDates(Table & view)521 static void setupTableDates (Table& view)
522 {
523 view.add (STRING_CMD_HISTORY_YEAR, true);
524 view.add (STRING_CMD_HISTORY_MONTH, true);
525 view.add (STRING_CMD_HISTORY_DAY, false);
526 }
527
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)528 static void insertRowDate (
529 Table& view,
530 int row,
531 time_t rowTime,
532 time_t lastTime)
533 {
534 Datetime dt (rowTime);
535 int m, d, y;
536 dt.toYMD (y, m, d);
537
538 Datetime last_dt (lastTime);
539 int last_m, last_d, last_y;
540 last_dt.toYMD (last_y, last_m, last_d);
541
542 bool y_changed = (y != last_y) || (lastTime == 0);
543 bool m_changed = (m != last_m) || (lastTime == 0);
544
545 if (y_changed)
546 view.set (row, 0, y);
547
548 if (y_changed || m_changed)
549 view.set (row, 1, Datetime::monthName (m));
550
551 view.set (row, 2, d);
552 }
553
554 static constexpr const char* keyword = "history.daily";
555 static constexpr const char* usage = "task <filter> history.daily";
556 static constexpr const char* description = "Shows a report of task history, by day";
557 static constexpr unsigned int dateFieldCount = 3;
558 static constexpr bool graphical = false;
559 static constexpr unsigned int labelWidth = 0; // unused.
560 };
561
562 ////////////////////////////////////////////////////////////////////////////i
563 class DailyGHistoryStrategy
564 {
565 public:
getRelevantDate(const Datetime & dt)566 static Datetime getRelevantDate (const Datetime& dt)
567 {
568 return dt.startOfDay ();
569 }
570
setupTableDates(Table & view)571 static void setupTableDates (Table& view)
572 {
573 view.add (STRING_CMD_HISTORY_YEAR, true);
574 view.add (STRING_CMD_HISTORY_MONTH, true);
575 view.add (STRING_CMD_HISTORY_DAY, false);
576 }
577
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)578 static void insertRowDate (
579 Table& view,
580 int row,
581 time_t rowTime,
582 time_t lastTime)
583 {
584 Datetime dt (rowTime);
585 int m, d, y;
586 dt.toYMD (y, m, d);
587
588 Datetime last_dt (lastTime);
589 int last_m, last_d, last_y;
590 last_dt.toYMD (last_y, last_m, last_d);
591
592 bool y_changed = (y != last_y) || (lastTime == 0);
593 bool m_changed = (m != last_m) || (lastTime == 0);
594
595 if (y_changed)
596 view.set (row, 0, y);
597
598 if (y_changed || m_changed)
599 view.set (row, 1, Datetime::monthName (m));
600
601 view.set (row, 2, d);
602 }
603
604 static constexpr const char* keyword = "ghistory.daily";
605 static constexpr const char* usage = "task <filter> ghistory.daily";
606 static constexpr const char* description = "Shows a graphical report of task history, by day";
607 static constexpr unsigned int dateFieldCount = 3;
608 static constexpr bool graphical = true;
609 static constexpr unsigned int labelWidth = 19; // length '2017 September Day ' = 19
610 };
611
612 ////////////////////////////////////////////////////////////////////////////i
613 class WeeklyHistoryStrategy
614 {
615 public:
getRelevantDate(const Datetime & dt)616 static Datetime getRelevantDate (const Datetime& dt)
617 {
618 return dt.startOfWeek ();
619 }
620
setupTableDates(Table & view)621 static void setupTableDates (Table& view)
622 {
623 view.add (STRING_CMD_HISTORY_YEAR, true);
624 view.add (STRING_CMD_HISTORY_MONTH, true);
625 view.add (STRING_CMD_HISTORY_DAY, false);
626 }
627
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)628 static void insertRowDate (
629 Table& view,
630 int row,
631 time_t rowTime,
632 time_t lastTime)
633 {
634 Datetime dt (rowTime);
635 int m, d, y;
636 dt.toYMD (y, m, d);
637
638 Datetime last_dt (lastTime);
639 int last_m, last_d, last_y;
640 last_dt.toYMD (last_y, last_m, last_d);
641
642 bool y_changed = (y != last_y) || (lastTime == 0);
643 bool m_changed = (m != last_m) || (lastTime == 0);
644
645 if (y_changed)
646 view.set (row, 0, y);
647
648 if (y_changed || m_changed)
649 view.set (row, 1, Datetime::monthName (m));
650
651 view.set (row, 2, d);
652 }
653
654 static constexpr const char* keyword = "history.weekly";
655 static constexpr const char* usage = "task <filter> history.weekly";
656 static constexpr const char* description = "Shows a report of task history, by week";
657 static constexpr unsigned int dateFieldCount = 3;
658 static constexpr bool graphical = false;
659 static constexpr unsigned int labelWidth = 0; // unused.
660 };
661
662 ////////////////////////////////////////////////////////////////////////////i
663 class WeeklyGHistoryStrategy
664 {
665 public:
getRelevantDate(const Datetime & dt)666 static Datetime getRelevantDate (const Datetime& dt)
667 {
668 return dt.startOfWeek ();
669 }
670
setupTableDates(Table & view)671 static void setupTableDates (Table& view)
672 {
673 view.add (STRING_CMD_HISTORY_YEAR, true);
674 view.add (STRING_CMD_HISTORY_MONTH, true);
675 view.add (STRING_CMD_HISTORY_DAY, false);
676 }
677
insertRowDate(Table & view,int row,time_t rowTime,time_t lastTime)678 static void insertRowDate (
679 Table& view,
680 int row,
681 time_t rowTime,
682 time_t lastTime)
683 {
684 Datetime dt (rowTime);
685 int m, d, y;
686 dt.toYMD (y, m, d);
687
688 Datetime last_dt (lastTime);
689 int last_m, last_d, last_y;
690 last_dt.toYMD (last_y, last_m, last_d);
691
692 bool y_changed = (y != last_y) || (lastTime == 0);
693 bool m_changed = (m != last_m) || (lastTime == 0);
694
695 if (y_changed)
696 view.set (row, 0, y);
697
698 if (y_changed || m_changed)
699 view.set (row, 1, Datetime::monthName (m));
700
701 view.set (row, 2, d);
702 }
703
704 static constexpr const char* keyword = "ghistory.weekly";
705 static constexpr const char* usage = "task <filter> ghistory.weekly";
706 static constexpr const char* description = "Shows a graphical report of task history, by week";
707 static constexpr unsigned int dateFieldCount = 3;
708 static constexpr bool graphical = true;
709 static constexpr unsigned int labelWidth = 19; // length '2017 September Day ' = 19
710 };
711
712
713 // Explicit instantiations, avoiding cpp-inclusion or implementation in header
714 template class CmdHistoryBase<DailyHistoryStrategy>;
715 template class CmdHistoryBase<WeeklyHistoryStrategy>;
716 template class CmdHistoryBase<MonthlyHistoryStrategy>;
717 template class CmdHistoryBase<AnnualHistoryStrategy>;
718 template class CmdHistoryBase<DailyGHistoryStrategy>;
719 template class CmdHistoryBase<WeeklyGHistoryStrategy>;
720 template class CmdHistoryBase<MonthlyGHistoryStrategy>;
721 template class CmdHistoryBase<AnnualGHistoryStrategy>;
722
723 ////////////////////////////////////////////////////////////////////////////////
724