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 <CmdCustom.h>
29 #include <random>
30 #include <sstream>
31 #include <map>
32 #include <vector>
33 #include <algorithm>
34 #include <stdlib.h>
35 #include <Context.h>
36 #include <Filter.h>
37 #include <Lexer.h>
38 #include <ViewTask.h>
39 #include <format.h>
40 #include <shared.h>
41 #include <util.h>
42 #include <main.h>
43 
44 ////////////////////////////////////////////////////////////////////////////////
CmdCustom(const std::string & keyword,const std::string & usage,const std::string & description)45 CmdCustom::CmdCustom (
46   const std::string& keyword,
47   const std::string& usage,
48   const std::string& description)
49 {
50   _keyword               = keyword;
51   _usage                 = usage;
52   _description           = description;
53   _read_only             = true;
54   _displays_id           = true;
55   _needs_gc              = true;
56   _uses_context          = true;
57   _accepts_filter        = true;
58   _accepts_modifications = false;
59   _accepts_miscellaneous = false;
60   _category              = Category::report;
61 }
62 
63 ////////////////////////////////////////////////////////////////////////////////
64 // Whether a report uses context is defined by the report.<name>.context
65 // configuration variable.
66 //
uses_context() const67 bool CmdCustom::uses_context () const
68 {
69   auto config = Context::getContext ().config;
70   auto key = "report." + _keyword + ".context";
71 
72   if (config.has (key))
73     return config.getBoolean (key);
74   else
75     return _uses_context;
76 }
77 
78 ////////////////////////////////////////////////////////////////////////////////
execute(std::string & output)79 int CmdCustom::execute (std::string& output)
80 {
81   auto rc = 0;
82 
83   // Load report configuration.
84   auto reportColumns = Context::getContext ().config.get ("report." + _keyword + ".columns");
85   auto reportLabels  = Context::getContext ().config.get ("report." + _keyword + ".labels");
86   auto reportSort    = Context::getContext ().config.get ("report." + _keyword + ".sort");
87   auto reportFilter  = Context::getContext ().config.get ("report." + _keyword + ".filter");
88 
89   auto columns = split (reportColumns, ',');
90   validateReportColumns (columns);
91 
92   auto labels = split (reportLabels, ',');
93 
94   if (columns.size () != labels.size () && labels.size () != 0)
95     throw format ("There are different numbers of columns and labels for report '{1}'.", _keyword);
96 
97   auto sortOrder = split (reportSort, ',');
98   if (sortOrder.size () != 0 &&
99       sortOrder[0] != "none")
100     validateSortColumns (sortOrder);
101 
102   // Add the report filter to any existing filter.
103   if (reportFilter != "")
104     Context::getContext ().cli2.addFilter (reportFilter);
105 
106   // Make sure reccurent tasks are generated.
107   handleUntil ();
108   handleRecurrence ();
109 
110   // Apply filter.
111   Filter filter;
112   std::vector <Task> filtered;
113   filter.subset (filtered);
114 
115   std::vector <int> sequence;
116   if (sortOrder.size () &&
117       sortOrder[0] == "none")
118   {
119     // Assemble a sequence vector that represents the tasks listed in
120     // Context::getContext ().cli2._uuid_ranges, in the order in which they appear. This
121     // equates to no sorting, just a specified order.
122     sortOrder.clear ();
123     for (auto& i : Context::getContext ().cli2._uuid_list)
124       for (unsigned int t = 0; t < filtered.size (); ++t)
125         if (filtered[t].get ("uuid") == i)
126           sequence.push_back (t);
127   }
128   else
129   {
130     // There is a sortOrder, so sorting will take place, which means the initial
131     // order of sequence is ascending.
132     for (unsigned int i = 0; i < filtered.size (); ++i)
133       sequence.push_back (i);
134 
135     // Sort the tasks.
136     if (sortOrder.size ())
137       sort_tasks (filtered, sequence, reportSort);
138   }
139 
140   // Configure the view.
141   ViewTask view;
142   view.width (Context::getContext ().getWidth ());
143   view.leftMargin (Context::getContext ().config.getInteger ("indent.report"));
144   view.extraPadding (Context::getContext ().config.getInteger ("row.padding"));
145   view.intraPadding (Context::getContext ().config.getInteger ("column.padding"));
146 
147   if (Context::getContext ().color ())
148   {
149     Color label (Context::getContext ().config.get ("color.label"));
150     view.colorHeader (label);
151 
152     Color label_sort (Context::getContext ().config.get ("color.label.sort"));
153     view.colorSortHeader (label_sort);
154 
155     // If an alternating row color is specified, notify the table.
156     Color alternate (Context::getContext ().config.get ("color.alternate"));
157     if (alternate.nontrivial ())
158     {
159       view.colorOdd (alternate);
160       view.intraColorOdd (alternate);
161     }
162   }
163 
164   // Capture columns that are sorted.
165   std::vector <std::string> sortColumns;
166 
167   // Add the break columns, if any.
168   for (const auto& so : sortOrder)
169   {
170     std::string name;
171     bool ascending;
172     bool breakIndicator;
173     Context::getContext ().decomposeSortField (so, name, ascending, breakIndicator);
174 
175     if (breakIndicator)
176       view.addBreak (name);
177 
178     sortColumns.push_back (name);
179   }
180 
181   // Add the columns and labels.
182   for (unsigned int i = 0; i < columns.size (); ++i)
183   {
184     Column* c = Column::factory (columns[i], _keyword);
185     if (i < labels.size ())
186       c->setLabel (labels[i]);
187 
188     bool sort = std::find (sortColumns.begin (), sortColumns.end (), c->name ()) != sortColumns.end ()
189                   ? true
190                   : false;
191 
192     view.add (c, sort);
193   }
194 
195   // How many lines taken up by table header?
196   int table_header = 0;
197   if (Context::getContext ().verbose ("label"))
198   {
199     if (Context::getContext ().color () && Context::getContext ().config.getBoolean ("fontunderline"))
200       table_header = 1;  // Underlining doesn't use extra line.
201     else
202       table_header = 2;  // Dashes use an extra line.
203   }
204 
205   // Report output can be limited by rows or lines.
206   auto maxrows = 0;
207   auto maxlines = 0;
208   Context::getContext ().getLimits (maxrows, maxlines);
209 
210   // Adjust for fluff in the output.
211   if (maxlines)
212     maxlines -= table_header
213               + (Context::getContext ().verbose ("blank") ? 1 : 0)
214               + (Context::getContext ().verbose ("footnote") ? Context::getContext ().footnotes.size () : 0)
215               + (Context::getContext ().verbose ("affected") ? 1 : 0)
216               + Context::getContext ().config.getInteger ("reserved.lines");  // For prompt, etc.
217 
218   // Render.
219   std::stringstream out;
220   if (filtered.size ())
221   {
222     view.truncateRows (maxrows);
223     view.truncateLines (maxlines);
224 
225     out << optionalBlankLine ()
226         << view.render (filtered, sequence)
227         << optionalBlankLine ();
228 
229     // Print the number of rendered tasks
230     if (Context::getContext ().verbose ("affected"))
231     {
232       out << (filtered.size () == 1
233                 ?  "1 task"
234                 : format ("{1} tasks", filtered.size ()));
235 
236       if (maxrows && maxrows < (int)filtered.size ())
237         out << ", " << format ("{1} shown", maxrows);
238 
239       if (maxlines && maxlines < (int)filtered.size ())
240         out << ", "
241             << format ("truncated to {1} lines", maxlines - table_header);
242 
243       out << '\n';
244     }
245   }
246   else
247   {
248     Context::getContext ().footnote ("No matches.");
249     rc = 1;
250   }
251 
252   // Inform user about the new release higlights if not presented yet
253   if (Context::getContext ().config.get ("news.version") != "2.6.0")
254   {
255     std::random_device device;
256     std::mt19937 random_generator(device());
257     std::uniform_int_distribution<std::mt19937::result_type> twentyfive_percent(1, 4);
258 
259     std::string NEWS_NOTICE = (
260       "Recently upgraded to 2.6.0. "
261       "Please run 'task news' to read higlights about the new release."
262     );
263 
264     // 1 in 10 chance to display the message.
265     if (twentyfive_percent(random_generator) == 4)
266     {
267       if (Context::getContext ().verbose ("footnote"))
268         Context::getContext ().footnote (NEWS_NOTICE);
269       else if (Context::getContext ().verbose ("header"))
270         Context::getContext ().header (NEWS_NOTICE);
271     }
272   }
273 
274 
275   feedback_backlog ();
276   output = out.str ();
277   return rc;
278 }
279 
280 ////////////////////////////////////////////////////////////////////////////////
validateReportColumns(std::vector<std::string> & columns)281 void CmdCustom::validateReportColumns (std::vector <std::string>& columns)
282 {
283   for (auto& col : columns)
284     legacyColumnMap (col);
285 }
286 
287 ////////////////////////////////////////////////////////////////////////////////
validateSortColumns(std::vector<std::string> & columns)288 void CmdCustom::validateSortColumns (std::vector <std::string>& columns)
289 {
290   for (auto& col : columns)
291     legacySortColumnMap (col);
292 }
293 
294 ////////////////////////////////////////////////////////////////////////////////
295