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

GothenburgBitFactory / taskwarrior / 20661798515

02 Jan 2026 04:15PM UTC coverage: 85.162% (+0.02%) from 85.145%
20661798515

push

github

djmitche
Lexer: isTag: allow whole tag (except first char) to be any non-space (#3957)

Instead of walking the string and stopping at the first
non-isIdentifier() character (see Lexer::isIdentifierStart(),
isIdentifierNext() and isSingleCharOperator()), walk all the way to the
end of the word.  This allows even punctuation and other characters to
be used in tags.

We still need to use isIdentifierStart() for the first character, to
disambiguate it from a negative number or subtraction.  Apparently there
is no command context available, so the parser cannot "know" whether
it's doing a "task calc" and have different parse rules.  The lexing
seems to happen before breaking the arguments down into commands for
dispatch.

5 of 5 new or added lines in 1 file covered. (100.0%)

17 existing lines in 3 files now uncovered.

19588 of 23001 relevant lines covered (85.16%)

23466.33 hits per line

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

85.38
/src/commands/CmdCalendar.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 <CmdCalendar.h>
31
#include <Context.h>
32
#include <Lexer.h>
33
#include <Table.h>
34
#include <format.h>
35
#include <shared.h>
36
#include <stdlib.h>
37
#include <utf8.h>
38
#include <util.h>
39

40
#include <iomanip>
41
#include <sstream>
42

43
////////////////////////////////////////////////////////////////////////////////
44
CmdCalendar::CmdCalendar() {
4,599✔
45
  _keyword = "calendar";
4,599✔
46
  _usage = "task          calendar [due|<month> <year>|<year>] [y]";
4,599✔
47
  _description = "Shows a calendar, with due tasks marked";
4,599✔
48
  _read_only = true;
4,599✔
49
  _displays_id = true;
4,599✔
50
  _needs_gc = true;
4,599✔
51
  _needs_recur_update = false;
4,599✔
52
  _uses_context = false;
4,599✔
53
  _accepts_filter = false;
4,599✔
54
  _accepts_modifications = false;
4,599✔
55
  _accepts_miscellaneous = true;
4,599✔
56
  _category = Command::Category::graphs;
4,599✔
57
}
4,599✔
58

59
////////////////////////////////////////////////////////////////////////////////
60
int CmdCalendar::execute(std::string& output) {
36✔
61
  int rc = 0;
36✔
62

63
  auto& config = Context::getContext().config;
36✔
64

65
  // Each month requires 28 text columns width.  See how many will actually
66
  // fit.  But if a preference is specified, and it fits, use it.
67
  auto width = Context::getContext().getWidth();
36✔
68
  int preferredMonthsPerLine;
69

70
  if (config.has("calendar.monthsperline"))
72✔
71
    preferredMonthsPerLine = config.getInteger("calendar.monthsperline");
×
72
  else
73
    // Legacy configuration variable value
74
    preferredMonthsPerLine = config.getInteger("monthsperline");
72✔
75

76
  auto monthsThatFit = width / 26;
36✔
77

78
  auto monthsPerLine = monthsThatFit;
36✔
79
  if (preferredMonthsPerLine != 0 && preferredMonthsPerLine < monthsThatFit)
36✔
80
    monthsPerLine = preferredMonthsPerLine;
×
81

82
  // Load the pending tasks.
83
  auto tasks = Context::getContext().tdb2.pending_tasks();
36✔
84

85
  Datetime today;
36✔
86
  auto getPendingDate = false;
36✔
87
  auto monthsToDisplay = 1;
36✔
88
  auto mFrom = today.month();
36✔
89
  auto yFrom = today.year();
36✔
90
  auto mTo = mFrom;
36✔
91
  auto yTo = yFrom;
36✔
92

93
  // Defaults.
94
  monthsToDisplay = monthsPerLine;
36✔
95
  mFrom = today.month();
36✔
96
  yFrom = today.year();
36✔
97

98
  // Set up a vector of commands (1), for autoComplete.
99
  std::vector<std::string> commandNames{"calendar"};
108✔
100

101
  // Set up a vector of keywords, for autoComplete.
102
  std::vector<std::string> keywordNames{"due"};
108✔
103

104
  // Set up a vector of months, for autoComplete.
105
  std::vector<std::string> monthNames;
36✔
106
  for (int i = 1; i <= 12; ++i) monthNames.push_back(Lexer::lowerCase(Datetime::monthName(i)));
468✔
107

108
  // For autoComplete results.
109
  std::vector<std::string> matches;
36✔
110

111
  // Look at all args, regardless of sequence.
112
  auto argMonth = 0;
36✔
113
  auto argYear = 0;
36✔
114
  auto argWholeYear = false;
36✔
115

116
  for (auto& arg : Context::getContext().cli2.getWords()) {
84✔
117
    // Some version of "calendar".
118
    if (autoComplete(Lexer::lowerCase(arg), commandNames, matches,
177✔
119
                     config.getInteger("abbreviation.minimum")) == 1)
59✔
120
      continue;
×
121

122
    // "due".
123
    else if (autoComplete(Lexer::lowerCase(arg), keywordNames, matches,
177✔
124
                          config.getInteger("abbreviation.minimum")) == 1)
59✔
125
      getPendingDate = true;
12✔
126

127
    // "y".
128
    else if (Lexer::lowerCase(arg) == "y")
47✔
129
      argWholeYear = true;
11✔
130

131
    // YYYY.
132
    else if (Lexer::isAllDigits(arg) && arg.length() == 4)
36✔
133
      argYear = strtol(arg.c_str(), nullptr, 10);
11✔
134

135
    // MM.
136
    else if (Lexer::isAllDigits(arg) && arg.length() <= 2) {
25✔
137
      argMonth = strtol(arg.c_str(), nullptr, 10);
12✔
138
      if (argMonth < 1 || argMonth > 12) throw format("Argument '{1}' is not a valid month.", arg);
12✔
139
    }
140

141
    // "January" etc.
142
    else if (autoComplete(Lexer::lowerCase(arg), monthNames, matches,
39✔
143
                          config.getInteger("abbreviation.minimum")) == 1) {
13✔
144
      argMonth = Datetime::monthOfYear(matches[0]);
2✔
145
      if (argMonth == -1) throw format("Argument '{1}' is not a valid month.", arg);
2✔
146
    }
147

148
    else
149
      throw format("Could not recognize argument '{1}'.", arg);
33✔
150
  }
36✔
151

152
  // Supported combinations:
153
  //
154
  //   Command line  monthsToDisplay  mFrom  yFrom  getPendingDate
155
  //   ------------  ---------------  -----  -----  --------------
156
  //   cal             monthsPerLine  today  today           false
157
  //   cal y                      12  today  today           false
158
  //   cal due         monthsPerLine  today  today            true
159
  //   cal YYYY                   12      1    arg           false
160
  //   cal due y                  12  today  today            true
161
  //   cal MM YYYY     monthsPerLine    arg    arg           false
162
  //   cal MM YYYY y              12    arg    arg           false
163

164
  if (argWholeYear || (argYear && !argMonth && !argWholeYear)) monthsToDisplay = 12;
25✔
165

166
  if (!argMonth && argYear)
25✔
167
    mFrom = 1;
3✔
168
  else if (argMonth && argYear)
22✔
169
    mFrom = argMonth;
4✔
170

171
  if (argYear) yFrom = argYear;
25✔
172

173
  // Now begin the data subset and rendering.
174
  if (getPendingDate == true) {
25✔
175
    // Find the oldest pending due date.
176
    Datetime oldest(9999, 12, 31);
8✔
177
    for (auto& task : tasks) {
8✔
178
      auto status = task.getStatus();
×
179
      if (status == Task::pending || status == Task::waiting) {
×
180
        if (task.has("due") && !task.hasTag("nocal")) {
×
181
          Datetime d(task.get("due"));
×
182
          if (d < oldest) oldest = d;
×
183
        }
184
      }
185
    }
186

187
    // Default to current month if no due date is present
188
    if (oldest != Datetime(9999, 12, 31)) {
8✔
189
      mFrom = oldest.month();
×
190
      yFrom = oldest.year();
×
191
    }
192
  }
193

194
  if (config.getBoolean("calendar.offset")) {
50✔
195
    auto moffset = config.getInteger("calendar.offset.value") % 12;
2✔
196
    auto yoffset = config.getInteger("calendar.offset.value") / 12;
1✔
197
    mFrom += moffset;
1✔
198
    yFrom += yoffset;
1✔
199
    if (mFrom < 1) {
1✔
200
      mFrom += 12;
×
201
      yFrom--;
×
202
    } else if (mFrom > 12) {
1✔
UNCOV
203
      mFrom -= 12;
×
UNCOV
204
      yFrom++;
×
205
    }
206
  }
207

208
  mTo = mFrom + monthsToDisplay - 1;
25✔
209
  yTo = yFrom;
25✔
210
  if (mTo > 12) {
25✔
211
    mTo -= 12;
1✔
212
    yTo++;
1✔
213
  }
214

215
  auto details_yFrom = yFrom;
25✔
216
  auto details_mFrom = mFrom;
25✔
217

218
  std::stringstream out;
25✔
219
  out << '\n';
25✔
220

221
  while (yFrom < yTo || (yFrom == yTo && mFrom <= mTo)) {
74✔
222
    auto nextM = mFrom;
49✔
223
    auto nextY = yFrom;
49✔
224

225
    // Print month headers (cheating on the width settings, yes)
226
    for (int i = 0; i < monthsPerLine; i++) {
196✔
227
      auto month = Datetime::monthName(nextM);
147✔
228

229
      //    12345678901234567890123456 = 26 chars wide
230
      //                ^^             = center
231
      //    <------->                  = 13 - (month.length / 2) + 1
232
      //                      <------> = 26 - above
233
      //   +--------------------------+
234
      //   |         July 2009        |
235
      //   |     Mo Tu We Th Fr Sa Su |
236
      //   |  27        1  2  3  4  5 |
237
      //   |  28  6  7  8  9 10 11 12 |
238
      //   |  29 13 14 15 16 17 18 19 |
239
      //   |  30 20 21 22 23 24 25 26 |
240
      //   |  31 27 28 29 30 31       |
241
      //   +--------------------------+
242

243
      auto totalWidth = 26;
147✔
244
      auto labelWidth = month.length() + 5;  // 5 = " 2009"
147✔
245
      auto leftGap = (totalWidth / 2) - (labelWidth / 2);
147✔
246
      auto rightGap = totalWidth - leftGap - labelWidth;
147✔
247

248
      out << std::setw(leftGap) << ' ' << month << ' ' << nextY << std::setw(rightGap) << ' ';
147✔
249

250
      if (++nextM > 12) {
147✔
251
        nextM = 1;
8✔
252
        nextY++;
8✔
253
      }
254
    }
147✔
255

256
    out << '\n'
257
        << optionalBlankLine() << renderMonths(mFrom, yFrom, today, tasks, monthsPerLine) << '\n';
49✔
258

259
    mFrom += monthsPerLine;
49✔
260
    if (mFrom > 12) {
49✔
261
      mFrom -= 12;
8✔
262
      ++yFrom;
8✔
263
    }
264
  }
265

266
  Color color_today(config.get("color.calendar.today"));
50✔
267
  Color color_due(config.get("color.calendar.due"));
50✔
268
  Color color_duetoday(config.get("color.calendar.due.today"));
50✔
269
  Color color_overdue(config.get("color.calendar.overdue"));
50✔
270
  Color color_weekend(config.get("color.calendar.weekend"));
50✔
271
  Color color_holiday(config.get("color.calendar.holiday"));
50✔
272
  Color color_scheduled(config.get("color.calendar.scheduled"));
50✔
273
  Color color_weeknumber(config.get("color.calendar.weeknumber"));
25✔
274

275
  if (Context::getContext().color() && config.getBoolean("calendar.legend")) {
29✔
276
    out << "Legend: " << color_today.colorize("today") << ", " << color_weekend.colorize("weekend")
10✔
277
        << ", ";
6✔
278

279
    // If colorizing due dates, print legend
280
    if (config.get("calendar.details") != "none")
4✔
281
      out << color_due.colorize("due") << ", " << color_duetoday.colorize("due-today") << ", "
10✔
282
          << color_overdue.colorize("overdue") << ", " << color_scheduled.colorize("scheduled")
10✔
283
          << ", ";
8✔
284

285
    // If colorizing holidays, print legend
286
    if (config.get("calendar.holidays") != "none") out << color_holiday.colorize("holiday") << ", ";
4✔
287

288
    out << color_weeknumber.colorize("weeknumber") << '.' << optionalBlankLine() << '\n';
4✔
289
  }
290

291
  if (config.get("calendar.details") == "full" || config.get("calendar.holidays") == "full") {
121✔
292
    --details_mFrom;
5✔
293
    if (details_mFrom == 0) {
5✔
294
      details_mFrom = 12;
5✔
295
      --details_yFrom;
5✔
296
    }
297
    int details_dFrom = Datetime::daysInMonth(details_yFrom, details_mFrom);
5✔
298

299
    ++mTo;
5✔
300
    if (mTo == 13) {
5✔
301
      mTo = 1;
×
302
      ++yTo;
×
303
    }
304

305
    Datetime date_after(details_yFrom, details_mFrom, details_dFrom);
5✔
306
    auto after = date_after.toString(config.get("dateformat"));
5✔
307

308
    Datetime date_before(yTo, mTo, 1);
5✔
309
    auto before = date_before.toString(config.get("dateformat"));
10✔
310

311
    // Table with due date information
312
    if (config.get("calendar.details") == "full") {
10✔
313
      // Assert that 'report' is a valid report.
314
      auto report = config.get("calendar.details.report");
2✔
315
      if (Context::getContext().commands.find(report) == Context::getContext().commands.end())
2✔
316
        throw std::string(
317
            "The setting 'calendar.details.report' must contain a single report name.");
×
318

319
      // TODO Fix this:  cal      --> task
320
      //                 calendar --> taskendar
321

322
      // If the executable was "cal" or equivalent, replace it with "task".
323
      auto executable = Context::getContext().cli2._original_args[0].attribute("raw");
4✔
324
      auto cal = executable.find("cal");
2✔
325
      if (cal != std::string::npos) executable = executable.substr(0, cal) + PACKAGE;
2✔
326

327
      std::vector<std::string> args;
2✔
328
      args.push_back("rc:" + Context::getContext().rc_file._data);
2✔
329
      args.push_back("rc.due:0");
4✔
330
      args.push_back("rc.verbose:label,affected,blank");
2✔
331
      if (Context::getContext().color()) args.push_back("rc._forcecolor:on");
4✔
332
      args.push_back("due.after:" + after);
2✔
333
      args.push_back("due.before:" + before);
2✔
334
      args.push_back("-nocal");
2✔
335
      args.push_back(report);
2✔
336

337
      std::string output;
2✔
338
      ::execute(executable, args, "", output);
2✔
339
      out << output;
2✔
340
    }
2✔
341

342
    // Table with holiday information
343
    if (config.get("calendar.holidays") == "full") {
10✔
344
      Table holTable;
3✔
345
      holTable.width(Context::getContext().getWidth());
3✔
346
      holTable.add("Date");
6✔
347
      holTable.add("Holiday");
3✔
348
      holTable.withColor(Context::getContext().color());
3✔
349
      setHeaderUnderline(holTable);
3✔
350

351
      auto dateFormat = config.get("dateformat.holiday");
3✔
352

353
      std::map<time_t, std::vector<std::string>> hm;  // we need to store multiple holidays per day
3✔
354
      for (auto& it : config)
740✔
355
        if (it.first.substr(0, 8) == "holiday.")
737✔
356
          if (it.first.substr(it.first.size() - 4) == "name") {
4✔
357
            auto holName = it.second;
2✔
358
            auto date = config.get("holiday." + it.first.substr(8, it.first.size() - 13) + ".date");
2✔
359
            auto start =
360
                config.get("holiday." + it.first.substr(8, it.first.size() - 13) + ".start");
2✔
361
            auto end = config.get("holiday." + it.first.substr(8, it.first.size() - 13) + ".end");
2✔
362
            if (!date.empty()) {
2✔
363
              Datetime holDate(date.c_str(), dateFormat);
×
364

365
              if (date_after < holDate && holDate < date_before)
×
366
                hm[holDate.toEpoch()].push_back(holName);
×
367
            }
368
            if (!start.empty() && !end.empty()) {
2✔
369
              Datetime holStart(start.c_str(), dateFormat);
2✔
370
              Datetime holEnd(end.c_str(), dateFormat);
1✔
371

372
              if (date_after < holStart && holStart < date_before)
1✔
373
                hm[holStart.toEpoch()].push_back("Start of " + holName);
1✔
374
              if (date_after < holEnd && holEnd < date_before)
1✔
375
                hm[holEnd.toEpoch()].push_back("End of " + holName);
1✔
376
            }
377
          }
2✔
378

379
      auto format = config.get("report." + config.get("calendar.details.report") + ".dateformat");
3✔
380
      if (format == "") format = config.get("dateformat.report");
9✔
381
      if (format == "") format = config.get("dateformat");
9✔
382

383
      for (auto& hm_it : hm) {
5✔
384
        auto v = hm_it.second;
2✔
385
        Datetime hDate(hm_it.first);
2✔
386
        auto d = hDate.toString(format);
2✔
387
        for (const auto& i : v) {
4✔
388
          auto row = holTable.addRow();
2✔
389
          holTable.set(row, 0, d);
2✔
390
          holTable.set(row, 1, i);
2✔
391
        }
392
      }
2✔
393

394
      out << optionalBlankLine() << holTable.render() << '\n';
3✔
395
    }
3✔
396
  }
5✔
397

398
  output = out.str();
25✔
399
  return rc;
25✔
400
}
260✔
401

402
////////////////////////////////////////////////////////////////////////////////
403
std::string CmdCalendar::renderMonths(int firstMonth, int firstYear, const Datetime& today,
49✔
404
                                      std::vector<Task>& all, int monthsPerLine) {
405
  auto& config = Context::getContext().config;
49✔
406
  auto weekStart = Datetime::weekstart;
49✔
407

408
  // Build table for the number of months to be displayed.
409
  Table view;
49✔
410
  setHeaderUnderline(view);
49✔
411
  view.width(Context::getContext().getWidth());
49✔
412
  view.withColor(Context::getContext().color());
49✔
413
  for (int i = 0; i < (monthsPerLine * 8); i += 8) {
196✔
414
    if (weekStart == 1) {
147✔
415
      view.add("", false);
3✔
416
      view.add(utf8_substr(Datetime::dayName(1), 0, 2), false);
3✔
417
      view.add(utf8_substr(Datetime::dayName(2), 0, 2), false);
3✔
418
      view.add(utf8_substr(Datetime::dayName(3), 0, 2), false);
3✔
419
      view.add(utf8_substr(Datetime::dayName(4), 0, 2), false);
3✔
420
      view.add(utf8_substr(Datetime::dayName(5), 0, 2), false);
3✔
421
      view.add(utf8_substr(Datetime::dayName(6), 0, 2), false);
3✔
422
      view.add(utf8_substr(Datetime::dayName(0), 0, 2), false);
3✔
423
    } else {
424
      view.add("", false);
144✔
425
      view.add(utf8_substr(Datetime::dayName(0), 0, 2), false);
144✔
426
      view.add(utf8_substr(Datetime::dayName(1), 0, 2), false);
144✔
427
      view.add(utf8_substr(Datetime::dayName(2), 0, 2), false);
144✔
428
      view.add(utf8_substr(Datetime::dayName(3), 0, 2), false);
144✔
429
      view.add(utf8_substr(Datetime::dayName(4), 0, 2), false);
144✔
430
      view.add(utf8_substr(Datetime::dayName(5), 0, 2), false);
144✔
431
      view.add(utf8_substr(Datetime::dayName(6), 0, 2), false);
144✔
432
    }
433
  }
434

435
  // At most, we need 6 rows.
436
  view.addRow();
49✔
437
  view.addRow();
49✔
438
  view.addRow();
49✔
439
  view.addRow();
49✔
440
  view.addRow();
49✔
441
  view.addRow();
49✔
442

443
  // Set number of days per month, months to render, and years to render.
444
  std::vector<int> years;
49✔
445
  std::vector<int> months;
49✔
446
  std::vector<int> daysInMonth;
49✔
447
  int thisYear = firstYear;
49✔
448
  int thisMonth = firstMonth;
49✔
449
  for (int i = 0; i < monthsPerLine; i++) {
196✔
450
    if (thisMonth < 13) {
147✔
451
      years.push_back(thisYear);
146✔
452
    } else {
453
      thisMonth -= 12;
1✔
454
      years.push_back(++thisYear);
1✔
455
    }
456
    months.push_back(thisMonth);
147✔
457
    daysInMonth.push_back(Datetime::daysInMonth(thisYear, thisMonth++));
147✔
458
  }
459

460
  auto row = 0;
49✔
461

462
  Color color_today(config.get("color.calendar.today"));
98✔
463
  Color color_due(config.get("color.calendar.due"));
98✔
464
  Color color_duetoday(config.get("color.calendar.due.today"));
98✔
465
  Color color_overdue(config.get("color.calendar.overdue"));
98✔
466
  Color color_weekend(config.get("color.calendar.weekend"));
98✔
467
  Color color_holiday(config.get("color.calendar.holiday"));
98✔
468
  Color color_scheduled(config.get("color.calendar.scheduled"));
98✔
469
  Color color_weeknumber(config.get("color.calendar.weeknumber"));
49✔
470

471
  // Loop through months to be added on this line.
472
  for (int mpl = 0; mpl < monthsPerLine; mpl++) {
196✔
473
    // Reset row counter for subsequent months
474
    if (mpl != 0) row = 0;
147✔
475

476
    // Loop through days in month and add to table.
477
    for (int d = 1; d <= daysInMonth[mpl]; ++d) {
4,603✔
478
      Datetime date(years[mpl], months[mpl], d);
4,456✔
479
      auto dow = date.dayOfWeek();
4,456✔
480
      auto woy = date.week();
4,456✔
481

482
      if (config.getBoolean("displayweeknumber"))
8,912✔
483
        view.set(row, (8 * mpl),
4,456✔
484
                 // Make sure the week number is always 4 columns, space-padded.
485
                 format((woy < 10 ? "   {1}" : "  {1}"), woy), color_weeknumber);
13,368✔
486

487
      // Calculate column id.
488
      auto thisCol = dow +                       // 0 = Sunday
4,456✔
489
                     (weekStart == 1 ? 0 : 1) +  // Offset for weekStart
4,456✔
490
                     (8 * mpl);                  // Columns in 1 month
4,456✔
491

492
      if (thisCol == (8 * mpl)) thisCol += 7;
4,456✔
493

494
      view.set(row, thisCol, d);
4,456✔
495

496
      if (Context::getContext().color()) {
4,456✔
497
        Color cellColor;
180✔
498

499
        // colorize weekends
500
        if (dow == 0 || dow == 6) cellColor.blend(color_weekend);
180✔
501

502
        // colorize holidays
503
        if (config.get("calendar.holidays") != "none") {
360✔
504
          auto dateFormat = config.get("dateformat.holiday");
×
505
          for (auto& hol : config) {
×
506
            if (hol.first.substr(0, 8) == "holiday.") {
×
507
              if (hol.first.substr(hol.first.size() - 4) == "date") {
×
508
                auto value = hol.second;
×
509
                Datetime holDate(value.c_str(), dateFormat);
×
510
                if (holDate.sameDay(date)) cellColor.blend(color_holiday);
×
511
              }
×
512

513
              if (hol.first.substr(hol.first.size() - 5) == "start" &&
×
514
                  config.has("holiday." + hol.first.substr(8, hol.first.size() - 14) + ".end")) {
×
515
                auto start = hol.second;
×
516
                auto end =
517
                    config.get("holiday." + hol.first.substr(8, hol.first.size() - 14) + ".end");
×
518
                Datetime holStart(start.c_str(), dateFormat);
×
519
                Datetime holEnd(end.c_str(), dateFormat);
×
520
                if (holStart <= date && date <= holEnd) cellColor.blend(color_holiday);
×
521
              }
×
522
            }
523
          }
524
        }
×
525

526
        // colorize today
527
        if (today.sameDay(date)) cellColor.blend(color_today);
180✔
528

529
        // colorize due and scheduled tasks
530
        if (config.get("calendar.details") != "none") {
360✔
531
          config.set("due", 0);
360✔
532
          config.set("scheduled", 0);
180✔
533
          // if a date has a task that is due on that day, the due color
534
          // takes precedence over the scheduled color
535
          bool coloredWithDue = false;
180✔
536
          for (auto& task : all) {
270✔
537
            auto status = task.getStatus();
90✔
538
            if ((status == Task::pending || status == Task::waiting) && !task.hasTag("nocal")) {
270✔
539
              if (task.has("scheduled") && !coloredWithDue) {
270✔
540
                std::string scheduled = task.get("scheduled");
×
541
                Datetime scheduleddmy(strtoll(scheduled.c_str(), nullptr, 10));
×
542

543
                if (scheduleddmy.sameDay(date)) {
×
544
                  cellColor.blend(color_scheduled);
×
545
                }
546
              }
×
547
              if (task.has("due")) {
180✔
548
                std::string due = task.get("due");
90✔
549
                Datetime duedmy(strtoll(due.c_str(), nullptr, 10));
90✔
550

551
                if (duedmy.sameDay(date)) {
90✔
552
                  coloredWithDue = true;
1✔
553
                  switch (task.getDateState("due")) {
2✔
554
                    case Task::dateNotDue:
×
555
                      break;
×
556

557
                    case Task::dateAfterToday:
1✔
558
                      cellColor.blend(color_due);
1✔
559
                      break;
1✔
560

561
                    case Task::dateLaterToday:
×
562
                      cellColor.blend(color_duetoday);
×
563
                      break;
×
564

565
                    case Task::dateEarlierToday:
×
566
                    case Task::dateBeforeToday:
567
                      cellColor.blend(color_overdue);
×
568
                      break;
×
569
                  }
570
                }
571
              }
90✔
572
            }
573
          }
574
        }
575

576
        view.set(row, thisCol, cellColor);
180✔
577
      }
578

579
      // Check for end of week, and...
580
      int eow = 6;
4,456✔
581
      if (weekStart == 1) eow = 0;
4,456✔
582
      if (dow == eow && d < daysInMonth[mpl]) row++;
4,456✔
583
    }
584
  }
585

586
  return view.render();
98✔
587
}
49✔
588

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