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

GothenburgBitFactory / taskwarrior / 10152339701

29 Jul 2024 09:45PM UTC coverage: 84.437% (+0.07%) from 84.372%
10152339701

push

github

web-flow
Merge pull request #3566 from felixschurk/add-clang-format

Add clang-format to enforce style guide

12359 of 13760 new or added lines in 147 files covered. (89.82%)

123 existing lines in 42 files now uncovered.

19070 of 22585 relevant lines covered (84.44%)

19724.02 hits per line

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

91.72
/src/ViewTask.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 <ViewTask.h>
32
#include <format.h>
33
#include <main.h>
34
#include <utf8.h>
35
#include <util.h>
36

37
#include <numeric>
38

39
////////////////////////////////////////////////////////////////////////////////
40
ViewTask::ViewTask()
641✔
41
    : _width(0),
641✔
42
      _left_margin(0),
641✔
43
      _header(0),
641✔
44
      _sort_header(0),
641✔
45
      _odd(0),
641✔
46
      _even(0),
641✔
47
      _intra_padding(1),
641✔
48
      _intra_odd(0),
641✔
49
      _intra_even(0),
641✔
50
      _extra_padding(0),
641✔
51
      _extra_odd(0),
641✔
52
      _extra_even(0),
641✔
53
      _truncate_lines(0),
641✔
54
      _truncate_rows(0),
641✔
55
      _lines(0),
641✔
56
      _rows(0) {}
1,282✔
57

58
////////////////////////////////////////////////////////////////////////////////
59
ViewTask::~ViewTask() {
641✔
60
  for (auto& col : _columns) delete col;
4,221✔
61

62
  _columns.clear();
641✔
63
}
641✔
64

65
////////////////////////////////////////////////////////////////////////////////
66
//   |<---------- terminal width ---------->|
67
//
68
//         +-------+  +-------+  +-------+
69
//         |header |  |header |  |header |
70
//   +--+--+-------+--+-------+--+-------+--+
71
//   |ma|ex|cell   |in|cell   |in|cell   |ex|
72
//   +--+--+-------+--+-------+--+-------+--+
73
//   |ma|ex|cell   |in|cell   |in|cell   |ex|
74
//   +--+--+-------+--+-------+--+-------+--+
75
//
76
//   margin        - indentation for the whole table
77
//   extrapadding  - left and right padding for the whole table
78
//   intrapadding  - padding between columns
79
//
80
//
81
// Layout Algorithm:
82
//   - Height is irrelevant
83
//   - Determine the usable horizontal space for N columns:
84
//
85
//       usable = width - ma - (ex * 2) - (in * (N - 1))
86
//
87
//   - Look at every column, for every task, and determine the minimum and
88
//     maximum widths.  The minimum is the length of the largest indivisible
89
//     word, and the maximum is the full length of the value.
90
//   - If there is sufficient terminal width to display every task using the
91
//     maximum width, then do so.
92
//   - If there is insufficient terminal width to display every task using the
93
//     minimum width, then there is no layout solution.  Error.
94
//   - Otherwise there is a need for column wrapping.  Calculate the overage,
95
//     which is the difference between the sum of the minimum widths and the
96
//     usable width.
97
//   - Start by using all the minimum column widths, and distribute the overage
98
//     among all columns, one character at a time, while the column width is
99
//     less than the maximum width, and while there is overage remaining.
100
//
101
// Note: a possible enhancement is to proportionally distribute the overage
102
//       according to average data length.
103
//
104
// Note: an enhancement to the 'no solution' problem is to simply force-break
105
//       the larger fields.  If the widest field is W0, and the second widest
106
//       field is W1, then a solution may be achievable by reducing W0 --> W1.
107
//
108
std::string ViewTask::render(std::vector<Task>& data, std::vector<int>& sequence) {
557✔
109
  Timer timer;
557✔
110

111
  bool const obfuscate = Context::getContext().config.getBoolean("obfuscate");
557✔
112
  bool const print_empty_columns = Context::getContext().config.getBoolean("print.empty.columns");
557✔
113
  std::vector<Column*> nonempty_columns;
557✔
114
  std::vector<bool> nonempty_sort;
557✔
115

116
  // Determine minimal, ideal column widths.
117
  std::vector<int> minimal;
557✔
118
  std::vector<int> ideal;
557✔
119

120
  for (unsigned int i = 0; i < _columns.size(); ++i) {
6,842✔
121
    // Headers factor in to width calculations.
122
    unsigned int global_min = 0;
6,285✔
123
    unsigned int global_ideal = global_min;
6,285✔
124

125
    for (unsigned int s = 0; s < sequence.size(); ++s) {
20,229✔
126
      if ((int)s >= _truncate_lines && _truncate_lines != 0) break;
14,097✔
127

128
      if ((int)s >= _truncate_rows && _truncate_rows != 0) break;
14,015✔
129

130
      // Determine minimum and ideal width for this column.
131
      unsigned int min = 0;
13,944✔
132
      unsigned int ideal = 0;
13,944✔
133
      _columns[i]->measure(data[sequence[s]], min, ideal);
13,944✔
134

135
      if (min > global_min) global_min = min;
13,944✔
136
      if (ideal > global_ideal) global_ideal = ideal;
13,944✔
137

138
      // If a fixed-width column was just measured, there is no point repeating
139
      // the measurement for all tasks.
140
      if (_columns[i]->is_fixed_width()) break;
13,944✔
141
    }
142

143
    if (print_empty_columns || global_min != 0) {
6,285✔
144
      unsigned int label_length = utf8_width(_columns[i]->label());
2,639✔
145
      if (label_length > global_min) global_min = label_length;
2,639✔
146
      if (label_length > global_ideal) global_ideal = label_length;
2,639✔
147
      minimal.push_back(global_min);
2,639✔
148
      ideal.push_back(global_ideal);
2,639✔
149
    }
150

151
    if (!print_empty_columns) {
6,285✔
152
      if (global_min != 0)  // Column is nonempty
6,174✔
153
      {
154
        nonempty_columns.push_back(_columns[i]);
2,528✔
155
        nonempty_sort.push_back(_sort[i]);
2,528✔
156
      } else  // Column is empty, drop it
157
      {
158
        // Note: This is safe to do because we set _columns = nonempty_columns
159
        // after iteration over _columns is finished.
160
        delete _columns[i];
3,646✔
161
      }
162
    }
163
  }
164

165
  if (!print_empty_columns) {
557✔
166
    _columns = nonempty_columns;
546✔
167
    _sort = nonempty_sort;
546✔
168
  }
169

170
  int all_extra = _left_margin + (2 * _extra_padding) + ((_columns.size() - 1) * _intra_padding);
557✔
171

172
  // Sum the widths.
173
  int sum_minimal = std::accumulate(minimal.begin(), minimal.end(), 0);
557✔
174
  int sum_ideal = std::accumulate(ideal.begin(), ideal.end(), 0);
557✔
175

176
  // Calculate final column widths.
177
  int overage = _width - sum_minimal - all_extra;
557✔
178
  Context::getContext().debug(format("ViewTask::render min={1} ideal={2} overage={3} width={4}",
557✔
179
                                     sum_minimal + all_extra, sum_ideal + all_extra, overage,
180
                                     _width));
181

182
  std::vector<int> widths;
557✔
183

184
  // Ideal case.  Everything fits.
185
  if (_width == 0 || sum_ideal + all_extra <= _width) {
557✔
186
    widths = ideal;
545✔
187
  }
188

189
  // Not enough for minimum. Decrease certain columns.
190
  else if (overage < 0) {
12✔
191
    // Determine which columns are the longest.
192
    unsigned int longest = 0;
2✔
193
    unsigned int second_longest = 0;
2✔
194
    for (unsigned int j = 0; j < minimal.size(); j++) {
11✔
195
      if (minimal[j] > minimal[longest]) {
9✔
196
        second_longest = longest;
4✔
197
        longest = j;
4✔
198
      } else if (minimal[j] > minimal[second_longest]) {
5✔
199
        second_longest = j;
1✔
200
      }
201
    }
202

203
    // Case 1: Shortening longest column still keeps it longest. Let it bear
204
    //         all the shortening.
205
    widths = minimal;
2✔
206
    if (minimal[longest] + overage >= minimal[second_longest]) widths[longest] += overage;
2✔
207

208
    // Case 2: Shorten the longest column to second longest length. Try to
209
    //         split shortening them evenly.
210
    else {
UNCOV
211
      int decrease = minimal[second_longest] - minimal[longest];
×
212
      widths[longest] += decrease;
×
213
      overage = overage - decrease;
×
214

215
      // Attempt to decrease the two longest columns (at most to two characters)
NEW
216
      if (-overage <= widths[longest] + widths[second_longest] - 4) {
×
217
        // Compute half of the overage, rounding up
218
        int half_overage = overage / 2 + overage % 2;
×
219

220
        // Decrease both larges columns by this amount
221
        widths[longest] += half_overage;
×
222
        widths[second_longest] += half_overage;
×
223
      } else
224
        // If reducing two of the longest solumns to 2 characters is not sufficient, then give up.
NEW
225
        Context::getContext().error(format(
×
226
            "The report has a minimum width of {1} and does not fit in the available width of {2}.",
227
            sum_minimal + all_extra, _width));
228
    }
229
  }
230

231
  // Perfect minimal width.
232
  else if (overage == 0) {
10✔
233
    widths = minimal;
1✔
234
  }
235

236
  // Extra space to share.
237
  else if (overage > 0) {
9✔
238
    widths = minimal;
9✔
239

240
    // Spread 'overage' among columns where width[i] < ideal[i]
241
    bool needed = true;
9✔
242
    while (overage && needed) {
353✔
243
      needed = false;
344✔
244
      for (unsigned int i = 0; i < _columns.size() && overage; ++i) {
877✔
245
        if (widths[i] < ideal[i]) {
533✔
246
          ++widths[i];
344✔
247
          --overage;
344✔
248
          needed = true;
344✔
249
        }
250
      }
251
    }
252
  }
253

254
  // Compose column headers.
255
  unsigned int max_lines = 0;
557✔
256
  std::vector<std::vector<std::string>> headers;
557✔
257
  for (unsigned int c = 0; c < _columns.size(); ++c) {
3,196✔
258
    headers.emplace_back();
2,639✔
259
    _columns[c]->renderHeader(headers[c], widths[c], _sort[c] ? _sort_header : _header);
2,639✔
260

261
    if (headers[c].size() > max_lines) max_lines = headers[c].size();
2,639✔
262
  }
263

264
  // Render column headers.
265
  std::string left_margin = std::string(_left_margin, ' ');
557✔
266
  std::string extra = std::string(_extra_padding, ' ');
557✔
267
  std::string intra = std::string(_intra_padding, ' ');
557✔
268

269
  std::string extra_odd = Context::getContext().color() ? _extra_odd.colorize(extra) : extra;
557✔
270
  std::string extra_even = Context::getContext().color() ? _extra_even.colorize(extra) : extra;
557✔
271
  std::string intra_odd = Context::getContext().color() ? _intra_odd.colorize(intra) : intra;
557✔
272
  std::string intra_even = Context::getContext().color() ? _intra_even.colorize(intra) : intra;
557✔
273

274
  std::string out;
557✔
275
  _lines = 0;
557✔
276
  for (unsigned int i = 0; i < max_lines; ++i) {
1,388✔
277
    out += left_margin + extra;
831✔
278

279
    for (unsigned int c = 0; c < _columns.size(); ++c) {
4,526✔
280
      if (c) out += intra;
3,695✔
281

282
      if (headers[c].size() < max_lines - i)
3,695✔
NEW
283
        out += _header.colorize(std::string(widths[c], ' '));
×
284
      else
285
        out += headers[c][i];
3,695✔
286
    }
287

288
    out += extra;
831✔
289

290
    // Trim right.
291
    out.erase(out.find_last_not_of(' ') + 1);
831✔
292
    out += "\n";
831✔
293

294
    // Stop if the line limit is exceeded.
295
    if (++_lines >= _truncate_lines && _truncate_lines != 0) {
831✔
NEW
296
      Context::getContext().time_render_us += timer.total_us();
×
UNCOV
297
      return out;
×
298
    }
299
  }
300

301
  // Compose, render columns, in sequence.
302
  _rows = 0;
557✔
303
  std::vector<std::vector<std::string>> cells;
557✔
304
  for (unsigned int s = 0; s < sequence.size(); ++s) {
1,815✔
305
    max_lines = 0;
1,264✔
306

307
    // Apply color rules to task.
308
    Color rule_color;
1,264✔
309
    autoColorize(data[sequence[s]], rule_color);
1,264✔
310

311
    // Alternate rows based on |s % 2|
312
    bool odd = (s % 2) ? true : false;
1,264✔
313
    Color row_color;
1,264✔
314
    if (Context::getContext().color()) {
1,264✔
315
      row_color = odd ? _odd : _even;
23✔
316
      row_color.blend(rule_color);
23✔
317
    }
318

319
    for (unsigned int c = 0; c < _columns.size(); ++c) {
7,882✔
320
      cells.emplace_back();
6,618✔
321
      _columns[c]->render(cells[c], data[sequence[s]], widths[c], row_color);
6,618✔
322

323
      if (cells[c].size() > max_lines) max_lines = cells[c].size();
6,618✔
324

325
      if (obfuscate)
6,618✔
326
        if (_columns[c]->type() == "string")
12✔
327
          for (auto& line : cells[c]) line = obfuscateText(line);
12✔
328
    }
329

330
    // Listing breaks are simply blank lines inserted when a column value
331
    // changes.
332
    if (s > 0 && _breaks.size() > 0) {
1,264✔
333
      for (const auto& b : _breaks) {
16✔
334
        if (data[sequence[s - 1]].get(b) != data[sequence[s]].get(b)) {
8✔
335
          out += "\n";
×
336
          ++_lines;
×
337

338
          // Only want one \n, regardless of how many values change.
339
          break;
×
340
        }
341
      }
342
    }
343

344
    for (unsigned int i = 0; i < max_lines; ++i) {
2,596✔
345
      out += left_margin + (odd ? extra_odd : extra_even);
1,333✔
346

347
      for (unsigned int c = 0; c < _columns.size(); ++c) {
8,362✔
348
        if (c) {
7,029✔
349
          if (row_color.nontrivial())
5,696✔
350
            row_color._colorize(out, intra);
70✔
351
          else
352
            out += (odd ? intra_odd : intra_even);
5,626✔
353
        }
354

355
        if (i < cells[c].size())
7,029✔
356
          out += cells[c][i];
5,438✔
357
        else
358
          row_color._colorize(out, std::string(widths[c], ' '));
1,591✔
359
      }
360

361
      out += (odd ? extra_odd : extra_even);
1,333✔
362

363
      // Trim right.
364
      out.erase(out.find_last_not_of(' ') + 1);
1,333✔
365
      out += "\n";
1,333✔
366

367
      // Stop if the line limit is exceeded.
368
      if (++_lines >= _truncate_lines && _truncate_lines != 0) {
1,333✔
369
        Context::getContext().time_render_us += timer.total_us();
1✔
370
        return out;
6✔
371
      }
372
    }
373

374
    cells.clear();
1,263✔
375

376
    // Stop if the row limit is exceeded.
377
    if (++_rows >= _truncate_rows && _truncate_rows != 0) {
1,263✔
378
      Context::getContext().time_render_us += timer.total_us();
5✔
379
      return out;
5✔
380
    }
381
  }
382

383
  Context::getContext().time_render_us += timer.total_us();
551✔
384
  return out;
551✔
385
}
557✔
386

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