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

tstack / lnav / 17589970077-2502

09 Sep 2025 05:00PM UTC coverage: 65.196% (-5.0%) from 70.225%
17589970077-2502

push

github

tstack
[format] add fields for source file/line

Knowing the source file/line context in a log
message can help find log messages when using
log2src.

56 of 70 new or added lines in 2 files covered. (80.0%)

13954 existing lines in 210 files now uncovered.

45516 of 69814 relevant lines covered (65.2%)

404154.37 hits per line

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

56.7
/src/timeline_source.cc
1
/**
2
 * Copyright (c) 2023, Timothy Stack
3
 *
4
 * All rights reserved.
5
 *
6
 * Redistribution and use in source and binary forms, with or without
7
 * modification, are permitted provided that the following conditions are met:
8
 *
9
 * * Redistributions of source code must retain the above copyright notice, this
10
 * list of conditions and the following disclaimer.
11
 * * Redistributions in binary form must reproduce the above copyright notice,
12
 * this list of conditions and the following disclaimer in the documentation
13
 * and/or other materials provided with the distribution.
14
 * * Neither the name of Timothy Stack nor the names of its contributors
15
 * may be used to endorse or promote products derived from this software
16
 * without specific prior written permission.
17
 *
18
 * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY
19
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
20
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
21
 * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY
22
 * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
23
 * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
24
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
25
 * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
26
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
27
 * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
28
 */
29

30
#include <chrono>
31

32
#include "timeline_source.hh"
33

34
#include <time.h>
35

36
#include "base/humanize.hh"
37
#include "base/humanize.time.hh"
38
#include "base/itertools.enumerate.hh"
39
#include "base/itertools.hh"
40
#include "base/keycodes.hh"
41
#include "base/math_util.hh"
42
#include "command_executor.hh"
43
#include "crashd.client.hh"
44
#include "intervaltree/IntervalTree.h"
45
#include "lnav_util.hh"
46
#include "md4cpp.hh"
47
#include "sql_util.hh"
48
#include "sysclip.hh"
49

50
using namespace std::chrono_literals;
51
using namespace lnav::roles::literals;
52
using namespace md4cpp::literals;
53

54
static const std::vector<std::chrono::seconds> TIME_SPANS = {
55
    5min,
56
    15min,
57
    1h,
58
    2h,
59
    4h,
60
    8h,
61
    24h,
62
    7 * 24h,
63
    30 * 24h,
64
    365 * 24h,
65
};
66

67
static constexpr size_t MAX_OPID_WIDTH = 60;
68

69
size_t
70
abbrev_ftime(char* datebuf,
12✔
71
             size_t db_size,
72
             const struct tm& lb_tm,
73
             const struct tm& dt)
74
{
75
    char lb_fmt[32] = " ";
12✔
76
    bool same = true;
12✔
77

78
    if (lb_tm.tm_year == dt.tm_year) {
12✔
79
        strcat(lb_fmt, "    ");
12✔
80
    } else {
UNCOV
81
        same = false;
×
UNCOV
82
        strcat(lb_fmt, "%Y");
×
83
    }
84
    if (same && lb_tm.tm_mon == dt.tm_mon) {
12✔
85
        strcat(lb_fmt, "   ");
12✔
86
    } else {
87
        if (!same) {
×
88
            strcat(lb_fmt, "-");
×
89
        }
UNCOV
90
        same = false;
×
UNCOV
91
        strcat(lb_fmt, "%m");
×
92
    }
93
    if (same && lb_tm.tm_mday == dt.tm_mday) {
12✔
94
        strcat(lb_fmt, "   ");
12✔
95
    } else {
96
        if (!same) {
×
97
            strcat(lb_fmt, "-");
×
98
        }
UNCOV
99
        same = false;
×
UNCOV
100
        strcat(lb_fmt, "%d");
×
101
    }
102
    if (same && lb_tm.tm_hour == dt.tm_hour) {
12✔
103
        strcat(lb_fmt, "   ");
12✔
104
    } else {
105
        if (!same) {
×
106
            strcat(lb_fmt, "T");
×
107
        }
UNCOV
108
        same = false;
×
UNCOV
109
        strcat(lb_fmt, "%H");
×
110
    }
111
    if (same && lb_tm.tm_min == dt.tm_min) {
12✔
112
        strcat(lb_fmt, "   ");
12✔
113
    } else {
114
        if (!same) {
×
115
            strcat(lb_fmt, ":");
×
116
        }
UNCOV
117
        same = false;
×
UNCOV
118
        strcat(lb_fmt, "%M");
×
119
    }
120
    return strftime(datebuf, db_size, lb_fmt, &dt);
12✔
121
}
122

123
std::vector<attr_line_t>
UNCOV
124
timeline_preview_overlay::list_overlay_menu(const listview_curses& lv,
×
125
                                            vis_line_t line)
126
{
127
    static constexpr auto MENU_WIDTH = 25;
128

129
    const auto* tc = dynamic_cast<const textview_curses*>(&lv);
×
130
    std::vector<attr_line_t> retval;
×
131

UNCOV
132
    if (tc->tc_text_selection_active || !tc->tc_selected_text) {
×
133
        return retval;
×
134
    }
135

136
    const auto& sti = tc->tc_selected_text.value();
×
137

138
    if (sti.sti_line != line) {
×
139
        return retval;
×
140
    }
141
    auto title = " Actions "_status_title;
×
UNCOV
142
    auto left = std::max(0, sti.sti_x - 2);
×
143
    auto dim = lv.get_dimensions();
×
144
    auto menu_line = vis_line_t{1};
×
145

UNCOV
146
    if (left + MENU_WIDTH >= dim.second) {
×
147
        left = dim.second - MENU_WIDTH;
×
148
    }
149

UNCOV
150
    this->los_menu_items.clear();
×
151

152
    retval.emplace_back(attr_line_t().pad_to(left).append(title));
×
153
    {
154
        auto start = left;
×
155
        attr_line_t al;
×
156

157
        al.append(":clipboard:"_emoji)
×
UNCOV
158
            .append(" Copy  ")
×
159
            .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS));
×
160
        this->los_menu_items.emplace_back(
×
161
            menu_line,
162
            line_range{start, start + (int) al.length()},
×
163
            [](const std::string& value) {
×
UNCOV
164
                auto clip_res = sysclip::open(sysclip::type_t::GENERAL);
×
165
                if (clip_res.isErr()) {
×
UNCOV
166
                    log_error("unable to open clipboard: %s",
×
167
                              clip_res.unwrapErr().c_str());
168
                    return;
×
169
                }
170

171
                auto clip_pipe = clip_res.unwrap();
×
UNCOV
172
                fwrite(value.c_str(), 1, value.length(), clip_pipe.in());
×
UNCOV
173
            });
×
174
        retval.emplace_back(attr_line_t().pad_to(left).append(al));
×
175
    }
176

UNCOV
177
    return retval;
×
UNCOV
178
}
×
179

180
timeline_header_overlay::timeline_header_overlay(
579✔
181
    const std::shared_ptr<timeline_source>& src)
579✔
182
    : gho_src(src)
579✔
183
{
184
}
579✔
185

186
bool
187
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
24✔
188
                                             int y,
189
                                             int bottom,
190
                                             attr_line_t& value_out)
191
{
192
    if (y >= 3) {
24✔
193
        return false;
6✔
194
    }
195

196
    if (this->gho_src->gs_time_order.empty()) {
18✔
UNCOV
197
        if (y == 0) {
×
UNCOV
198
            value_out.append("No operations found"_error);
×
199
            return true;
×
200
        }
201

UNCOV
202
        return false;
×
203
    }
204

205
    auto lb = this->gho_src->gs_lower_bound;
18✔
206
    struct tm lb_tm;
207
    auto ub = this->gho_src->gs_upper_bound;
18✔
208
    struct tm ub_tm;
209
    auto bounds
210
        = this->gho_src->get_time_bounds_for(lv.get_selection().value_or(0_vl));
18✔
211

212
    if (bounds.first < lb) {
18✔
213
        lb = bounds.first;
18✔
214
    }
215
    if (ub < bounds.second) {
18✔
216
        ub = bounds.second;
18✔
217
    }
218

219
    secs2tm(lb.tv_sec, &lb_tm);
18✔
220
    secs2tm(ub.tv_sec, &ub_tm);
18✔
221

222
    struct tm sel_lb_tm;
223
    secs2tm(bounds.first.tv_sec, &sel_lb_tm);
18✔
224
    struct tm sel_ub_tm;
225
    secs2tm(bounds.second.tv_sec, &sel_ub_tm);
18✔
226

227
    auto width = lv.get_dimensions().second - 1;
18✔
228

229
    char datebuf[64];
230

231
    if (y == 0) {
18✔
232
        double span = ub.tv_sec - lb.tv_sec;
6✔
233
        double per_ch = span / (double) width;
6✔
234
        strftime(datebuf, sizeof(datebuf), " %Y-%m-%dT%H:%M", &lb_tm);
6✔
235
        value_out.append(datebuf);
6✔
236

237
        auto duration_str = humanize::time::duration::from_tv(ub - lb)
6✔
238
                                .with_resolution(1min)
6✔
239
                                .to_string();
6✔
240
        auto duration_pos = width / 2 - duration_str.size() / 2;
6✔
241
        value_out.pad_to(duration_pos).append(duration_str);
6✔
242

243
        auto upper_size
244
            = strftime(datebuf, sizeof(datebuf), "%Y-%m-%dT%H:%M ", &ub_tm);
6✔
245
        auto upper_pos = width - upper_size;
6✔
246

247
        value_out.pad_to(upper_pos).append(datebuf);
6✔
248

249
        auto lr = line_range{};
6✔
250
        if (lb.tv_sec < bounds.first.tv_sec) {
6✔
UNCOV
251
            auto start_diff = bounds.first.tv_sec - lb.tv_sec;
×
UNCOV
252
            lr.lr_start = start_diff / per_ch;
×
253
        } else {
254
            lr.lr_start = 0;
6✔
255
        }
256
        if (lb.tv_sec < bounds.second.tv_sec) {
6✔
257
            auto start_diff = bounds.second.tv_sec - lb.tv_sec;
6✔
258
            lr.lr_end = start_diff / per_ch;
6✔
259
        } else {
UNCOV
260
            lr.lr_end = 1;
×
261
        }
262
        if (lr.lr_start == lr.lr_end) {
6✔
UNCOV
263
            lr.lr_end += 1;
×
264
        }
265

266
        value_out.get_attrs().emplace_back(
12✔
267
            lr, VC_ROLE.value(role_t::VCR_CURSOR_LINE));
12✔
268
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
6✔
269
    } else if (y == 1) {
18✔
270
        abbrev_ftime(datebuf, sizeof(datebuf), lb_tm, sel_lb_tm);
6✔
271
        value_out.appendf(FMT_STRING(" {}"), datebuf);
24✔
272

273
        auto duration_str
274
            = humanize::time::duration::from_tv(bounds.second - bounds.first)
6✔
275
                  .with_resolution(1min)
6✔
276
                  .to_string();
6✔
277
        auto duration_pos = width / 2 - duration_str.size() / 2;
6✔
278
        value_out.pad_to(duration_pos).append(duration_str);
6✔
279

280
        auto upper_size
281
            = abbrev_ftime(datebuf, sizeof(datebuf), ub_tm, sel_ub_tm);
6✔
282
        auto upper_pos = width - upper_size - 1;
6✔
283
        value_out.pad_to(upper_pos).append(datebuf);
6✔
284
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_CURSOR_LINE));
6✔
285
    } else {
6✔
286
        value_out.append("   Duration   "_h1)
6✔
287
            .append("|", VC_GRAPHIC.value(NCACS_VLINE))
6✔
288
            .append(" ")
6✔
289
            .append("\u2718"_error)
6✔
290
            .append("\u25b2"_warning)
6✔
291
            .append(" ")
6✔
292
            .append("|", VC_GRAPHIC.value(NCACS_VLINE))
12✔
293
            .append(" Operation"_h1);
6✔
294
        auto hdr_attrs = text_attrs::with_underline();
6✔
295
        value_out.get_attrs().emplace_back(line_range{0, -1},
12✔
296
                                           VC_STYLE.value(hdr_attrs));
12✔
297
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
6✔
298
    }
6✔
299

300
    return true;
18✔
301
}
302
void
303
timeline_header_overlay::list_value_for_overlay(
304✔
304
    const listview_curses& lv,
305
    vis_line_t line,
306
    std::vector<attr_line_t>& value_out)
307
{
308
    if (!this->gho_show_details) {
304✔
309
        return;
304✔
310
    }
311

312
    if (lv.get_selection() != line) {
×
UNCOV
313
        return;
×
314
    }
315

316
    if (line >= this->gho_src->gs_time_order.size()) {
×
UNCOV
317
        return;
×
318
    }
319

UNCOV
320
    const auto& row = this->gho_src->gs_time_order[line].get();
×
321

322
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
UNCOV
323
        return;
×
324
    }
325

UNCOV
326
    auto width = lv.get_dimensions().second;
×
327

328
    if (width < 37) {
×
UNCOV
329
        return;
×
330
    }
331

332
    width -= 37;
×
333
    double span = row.or_value.otr_range.duration().count();
×
UNCOV
334
    double per_ch = span / (double) width;
×
335

336
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
UNCOV
337
        value_out.resize(value_out.size() + 1);
×
338

339
        auto& al = value_out.back();
×
340
        auto& attrs = al.get_attrs();
×
341
        auto total_msgs = sub.ostr_level_stats.lls_total_count;
×
UNCOV
342
        auto duration = sub.ostr_range.tr_end - sub.ostr_range.tr_begin;
×
343
        auto duration_str = fmt::format(
344
            FMT_STRING(" {: >13}"),
×
345
            humanize::time::duration::from_tv(duration).to_string());
×
346
        al.pad_to(14)
×
347
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
×
348
            .append(" ")
×
349
            .append(lnav::roles::error(humanize::sparkline(
×
350
                sub.ostr_level_stats.lls_error_count, total_msgs)))
×
351
            .append(lnav::roles::warning(humanize::sparkline(
×
352
                sub.ostr_level_stats.lls_warning_count, total_msgs)))
×
353
            .append(" ")
×
354
            .append(lnav::roles::identifier(sub.ostr_subid.to_string()))
×
355
            .append(row.or_max_subid_width
×
356
                        - sub.ostr_subid.utf8_length().unwrapOr(
×
357
                            row.or_max_subid_width),
×
358
                    ' ')
UNCOV
359
            .append(sub.ostr_description);
×
360
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
×
361

UNCOV
362
        auto start_diff = (double) to_mstime(sub.ostr_range.tr_begin
×
363
                                             - row.or_value.otr_range.tr_begin);
×
364
        auto end_diff = (double) to_mstime(sub.ostr_range.tr_end
×
365
                                           - row.or_value.otr_range.tr_begin);
×
366

367
        auto lr = line_range{
UNCOV
368
            (int) (32 + (start_diff / per_ch)),
×
369
            (int) (32 + (end_diff / per_ch)),
×
370
            line_range::unit::codepoint,
371
        };
372

UNCOV
373
        if (lr.lr_start == lr.lr_end) {
×
374
            lr.lr_end += 1;
×
375
        }
376

UNCOV
377
        auto block_attrs = text_attrs::with_reverse();
×
378
        attrs.emplace_back(lr, VC_STYLE.value(block_attrs));
×
379
    }
380
    if (!value_out.empty()) {
×
UNCOV
381
        value_out.back().get_attrs().emplace_back(
×
382
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
383
    }
384
}
385
std::optional<attr_line_t>
386
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
387
                                                 vis_line_t line)
388
{
UNCOV
389
    if (lv.get_overlay_selection()) {
×
UNCOV
390
        return attr_line_t("\u258C Sub-operations: Press ")
×
391
            .append("Esc"_hotkey)
×
UNCOV
392
            .append(" to exit this panel");
×
393
    }
394
    return attr_line_t("\u258C Sub-operations: Press ")
×
395
        .append("CTRL-]"_hotkey)
×
396
        .append(" to focus on this panel");
×
397
}
398

399
timeline_source::timeline_source(textview_curses& log_view,
579✔
400
                                 logfile_sub_source& lss,
401
                                 textview_curses& preview_view,
402
                                 plain_text_source& preview_source,
403
                                 statusview_curses& preview_status_view,
404
                                 timeline_status_source& preview_status_source)
579✔
405
    : gs_log_view(log_view), gs_lss(lss), gs_preview_view(preview_view),
579✔
406
      gs_preview_source(preview_source),
579✔
407
      gs_preview_status_view(preview_status_view),
579✔
408
      gs_preview_status_source(preview_status_source)
579✔
409
{
410
    this->tss_supports_filtering = true;
579✔
411
    this->gs_preview_view.set_overlay_source(&this->gs_preview_overlay);
579✔
412
}
579✔
413

414
bool
UNCOV
415
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
416
{
UNCOV
417
    switch (ch.eff_text[0]) {
×
UNCOV
418
        case 'q':
×
419
        case KEY_ESCAPE: {
UNCOV
420
            if (this->gs_preview_focused) {
×
421
                this->gs_preview_focused = false;
×
UNCOV
422
                this->gs_preview_view.set_height(5_vl);
×
423
            }
424
            this->tss_view->tc_cursor_role = role_t::VCR_CURSOR_LINE;
×
UNCOV
425
            this->gs_preview_view.tc_cursor_role
×
426
                = role_t::VCR_DISABLED_CURSOR_LINE;
×
427
            this->gs_preview_status_view.set_enabled(this->gs_preview_focused);
×
428
            break;
×
429
        }
430
        case '\n':
×
431
        case '\r':
432
        case NCKEY_ENTER: {
433
            this->gs_preview_focused = !this->gs_preview_focused;
×
434
            this->gs_preview_status_view.set_enabled(this->gs_preview_focused);
×
UNCOV
435
            if (this->gs_preview_focused) {
×
436
                auto height = this->tss_view->get_dimensions().first;
×
437

UNCOV
438
                if (height > 5) {
×
439
                    this->gs_preview_view.set_height(height - 3_vl);
×
440
                }
441
                this->tss_view->tc_cursor_role
×
442
                    = role_t::VCR_DISABLED_CURSOR_LINE;
×
UNCOV
443
                this->gs_preview_view.tc_cursor_role = role_t::VCR_CURSOR_LINE;
×
444
            } else {
445
                this->tss_view->tc_cursor_role = role_t::VCR_CURSOR_LINE;
×
UNCOV
446
                this->gs_preview_view.tc_cursor_role
×
447
                    = role_t::VCR_DISABLED_CURSOR_LINE;
×
448
                this->gs_preview_view.set_height(5_vl);
×
449
            }
UNCOV
450
            return true;
×
451
        }
452
    }
453
    if (this->gs_preview_focused) {
×
454
        log_debug("to preview");
×
UNCOV
455
        return this->gs_preview_view.handle_key(ch);
×
456
    }
457

UNCOV
458
    return false;
×
459
}
460

461
bool
UNCOV
462
timeline_source::text_handle_mouse(
×
463
    textview_curses& tc,
464
    const listview_curses::display_line_content_t&,
465
    mouse_event& me)
466
{
UNCOV
467
    auto nci = ncinput{};
×
468
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
UNCOV
469
        nci.id = '\r';
×
UNCOV
470
        nci.eff_text[0] = '\r';
×
UNCOV
471
        this->list_input_handle_key(tc, nci);
×
472
    }
473

474
    return false;
×
475
}
476

477
std::pair<timeval, timeval>
478
timeline_source::get_time_bounds_for(int line)
319✔
479
{
480
    const auto low_index = this->tss_view->get_top();
319✔
481
    auto high_index
482
        = std::min(this->tss_view->get_bottom(),
319✔
483
                   vis_line_t((int) this->gs_time_order.size() - 1));
319✔
484
    const auto& low_row = this->gs_time_order[low_index].get();
319✔
485
    const auto& high_row = this->gs_time_order[high_index].get();
319✔
486
    auto low_tv_sec = low_row.or_value.otr_range.tr_begin.tv_sec;
319✔
487
    auto high_tv_sec = high_row.or_value.otr_range.tr_begin.tv_sec;
319✔
488

489
    for (auto index = low_index; index <= high_index; index += 1_vl) {
638✔
490
        const auto& row = this->gs_time_order[index].get();
319✔
491

492
        if (row.or_value.otr_range.tr_end.tv_sec > high_tv_sec) {
319✔
493
            high_tv_sec = row.or_value.otr_range.tr_end.tv_sec;
311✔
494
        }
495
    }
496
    auto duration = std::chrono::seconds{high_tv_sec - low_tv_sec};
319✔
497
    auto span_iter
498
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
319✔
499
    if (span_iter == TIME_SPANS.end()) {
319✔
UNCOV
500
        --span_iter;
×
501
    }
502
    auto round_to = (*span_iter) == 5min
319✔
503
        ? 60
319✔
UNCOV
504
        : ((*span_iter) == 15min ? 60 * 15 : 60 * 60);
×
505
    auto span_secs = span_iter->count() - round_to;
319✔
506
    struct timeval lower_tv = {
319✔
507
        rounddown(low_row.or_value.otr_range.tr_begin.tv_sec, round_to),
319✔
508
        0,
509
    };
319✔
510
    lower_tv.tv_sec -= span_secs / 2;
319✔
511
    struct timeval upper_tv = {
319✔
512
        static_cast<time_t>(roundup(high_tv_sec, round_to)),
319✔
513
        0,
514
    };
319✔
515
    upper_tv.tv_sec += span_secs / 2;
319✔
516

517
    return {lower_tv, upper_tv};
638✔
518
}
519

520
size_t
521
timeline_source::text_line_count()
6,790✔
522
{
523
    return this->gs_time_order.size();
6,790✔
524
}
525

526
line_info
527
timeline_source::text_value_for_line(textview_curses& tc,
301✔
528
                                     int line,
529
                                     std::string& value_out,
530
                                     text_sub_source::line_flags_t flags)
531
{
532
    if (line < (ssize_t) this->gs_time_order.size()) {
301✔
533
        const auto& row = this->gs_time_order[line].get();
301✔
534
        auto duration
535
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
301✔
536
        auto duration_str = fmt::format(
537
            FMT_STRING(" {: >13}"),
903✔
538
            humanize::time::duration::from_tv(duration).to_string());
602✔
539

540
        this->gs_rendered_line.clear();
301✔
541

542
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
301✔
543
        auto truncated_name = row.or_name.to_string();
301✔
544
        truncate_to(truncated_name, MAX_OPID_WIDTH);
301✔
545
        this->gs_rendered_line
546
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
602✔
547
            .append("  ")
301✔
548
            .append(lnav::roles::error(humanize::sparkline(
903✔
549
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
301✔
550
            .append(lnav::roles::warning(humanize::sparkline(
903✔
551
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
301✔
552
            .append("  ")
301✔
553
            .append(lnav::roles::identifier(truncated_name))
602✔
554
            .append(this->gs_opid_width
903✔
555
                        - utf8_string_length(truncated_name)
602✔
556
                              .unwrapOr(this->gs_opid_width),
301✔
557
                    ' ')
558
            .append(row.or_description);
301✔
559
        this->gs_rendered_line.with_attr_for_all(
301✔
560
            VC_ROLE.value(role_t::VCR_COMMENT));
602✔
561

562
        value_out = this->gs_rendered_line.get_string();
301✔
563
    }
301✔
564

565
    return {};
301✔
566
}
567

568
void
569
timeline_source::text_attrs_for_line(textview_curses& tc,
301✔
570
                                     int line,
571
                                     string_attrs_t& value_out)
572
{
573
    if (line < (ssize_t) this->gs_time_order.size()) {
301✔
574
        const auto& row = this->gs_time_order[line].get();
301✔
575

576
        value_out = this->gs_rendered_line.get_attrs();
301✔
577

578
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
301✔
579
        auto sel_bounds = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
301✔
580

581
        if (row.or_value.otr_range.tr_begin <= sel_bounds.second
301✔
582
            && sel_bounds.first <= row.or_value.otr_range.tr_end)
301✔
583
        {
584
            static const int INDENT = 22;
585

586
            auto width = tc.get_dimensions().second;
301✔
587

588
            if (width > INDENT) {
301✔
589
                width -= INDENT;
301✔
590
                double span
301✔
591
                    = sel_bounds.second.tv_sec - sel_bounds.first.tv_sec;
301✔
592
                double per_ch = span / (double) width;
301✔
593

594
                if (row.or_value.otr_range.tr_begin <= sel_bounds.first) {
301✔
UNCOV
595
                    lr.lr_start = INDENT;
×
596
                } else {
597
                    auto start_diff = row.or_value.otr_range.tr_begin.tv_sec
301✔
598
                        - sel_bounds.first.tv_sec;
301✔
599

600
                    lr.lr_start = INDENT + start_diff / per_ch;
301✔
601
                }
602

603
                if (sel_bounds.second < row.or_value.otr_range.tr_end) {
301✔
UNCOV
604
                    lr.lr_end = -1;
×
605
                } else {
606
                    auto end_diff = row.or_value.otr_range.tr_end.tv_sec
301✔
607
                        - sel_bounds.first.tv_sec;
301✔
608

609
                    lr.lr_end = INDENT + end_diff / per_ch;
301✔
610
                    if (lr.lr_start == lr.lr_end) {
301✔
611
                        lr.lr_end += 1;
262✔
612
                    }
613
                }
614

615
                auto block_attrs = text_attrs::with_reverse();
301✔
616
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
301✔
617
            }
301✔
618
        }
619
        auto alt_row_index = line % 4;
301✔
620
        if (alt_row_index == 2 || alt_row_index == 3) {
301✔
621
            value_out.emplace_back(line_range{0, -1},
147✔
622
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
294✔
623
        }
624
    }
625
}
301✔
626

627
size_t
UNCOV
628
timeline_source::text_size_for_line(textview_curses& tc,
×
629
                                    int line,
630
                                    text_sub_source::line_flags_t raw)
631
{
632
    return this->gs_total_width;
×
633
}
634

635
bool
636
timeline_source::rebuild_indexes()
23✔
637
{
638
    auto& bm = this->tss_view->get_bookmarks();
23✔
639
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
23✔
640
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
23✔
641

642
    bm_errs.clear();
23✔
643
    bm_warns.clear();
23✔
644

645
    this->gs_lower_bound = {};
23✔
646
    this->gs_upper_bound = {};
23✔
647
    this->gs_opid_width = 0;
23✔
648
    this->gs_total_width = 0;
23✔
649
    this->gs_filtered_count = 0;
23✔
650
    this->gs_active_opids.clear();
23✔
651
    this->gs_descriptions.clear();
23✔
652
    this->gs_subid_map.clear();
23✔
653
    this->gs_allocator.reset();
23✔
654
    this->gs_preview_source.clear();
23✔
655
    this->gs_preview_rows.clear();
23✔
656
    this->gs_preview_status_source.get_description().clear();
23✔
657

658
    auto min_log_time_opt = this->get_min_row_time();
23✔
659
    auto max_log_time_opt = this->get_max_row_time();
23✔
660
    auto max_desc_width = size_t{0};
23✔
661

662
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
47✔
663
        if (ld->get_file_ptr() == nullptr) {
24✔
664
            continue;
1✔
665
        }
666
        if (!ld->is_visible()) {
24✔
667
            continue;
1✔
668
        }
669

670
        ld->get_file_ptr()->enable_cache();
23✔
671
        auto format = ld->get_file_ptr()->get_format();
23✔
672
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
673
            ld->get_file_ptr()->get_opids());
23✔
674
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,027✔
675
            auto& otr = pair.second;
1,004✔
676
            auto active_iter = this->gs_active_opids.find(pair.first);
1,004✔
677
            if (active_iter == this->gs_active_opids.end()) {
1,004✔
678
                auto opid = pair.first.to_owned(this->gs_allocator);
1,004✔
679
                auto active_emp_res = this->gs_active_opids.emplace(
2,008✔
680
                    opid,
681
                    opid_row{
1,004✔
682
                        opid,
683
                        otr,
684
                        string_fragment::invalid(),
685
                    });
686
                active_iter = active_emp_res.first;
1,004✔
687
            } else {
UNCOV
688
                active_iter->second.or_value |= otr;
×
689
            }
690

691
            auto& row = active_iter->second;
1,004✔
692
            for (auto& sub : active_iter->second.or_value.otr_sub_ops) {
1,004✔
UNCOV
693
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
694

UNCOV
695
                if (subid_iter == this->gs_subid_map.end()) {
×
696
                    subid_iter = this->gs_subid_map
×
UNCOV
697
                                     .emplace(sub.ostr_subid.to_owned(
×
698
                                                  this->gs_allocator),
×
699
                                              true)
×
700
                                     .first;
701
                }
702
                sub.ostr_subid = subid_iter->first;
×
UNCOV
703
                if (sub.ostr_subid.length()
×
UNCOV
704
                    > active_iter->second.or_max_subid_width)
×
705
                {
706
                    active_iter->second.or_max_subid_width
×
707
                        = sub.ostr_subid.length();
×
708
                }
709
            }
710

711
            if (otr.otr_description.lod_id) {
1,004✔
712
                auto desc_id = otr.otr_description.lod_id.value();
996✔
713
                auto desc_def_iter
714
                    = format->lf_opid_description_def->find(desc_id);
996✔
715

716
                if (desc_def_iter == format->lf_opid_description_def->end()) {
996✔
UNCOV
717
                    log_error("cannot find description: %s",
×
718
                              active_iter->first.data());
719
                } else {
720
                    auto desc_key
721
                        = opid_description_def_key{format->get_name(), desc_id};
996✔
722
                    auto desc_defs_iter
723
                        = row.or_description_defs.odd_defs.find(desc_key);
996✔
724
                    if (desc_defs_iter
996✔
725
                        == row.or_description_defs.odd_defs.end())
996✔
726
                    {
727
                        row.or_description_defs.odd_defs.insert(
1,992✔
728
                            desc_key, desc_def_iter->second);
996✔
729
                    }
730

731
                    auto& all_descs = active_iter->second.or_descriptions;
996✔
732
                    auto& curr_desc_m = all_descs[desc_key];
996✔
733
                    const auto& new_desc_v = otr.otr_description.lod_elements;
996✔
734

735
                    for (const auto& desc_pair : new_desc_v) {
1,992✔
736
                        curr_desc_m[desc_pair.first] = desc_pair.second;
996✔
737
                    }
738
                }
739
            } else {
740
                ensure(otr.otr_description.lod_elements.empty());
8✔
741
            }
742
            active_iter->second.or_value.otr_description.lod_elements.clear();
1,004✔
743
        }
744

745
        if (this->gs_index_progress) {
23✔
UNCOV
746
            switch (this->gs_index_progress(
×
UNCOV
747
                progress_t{index, this->gs_lss.file_count()}))
×
748
            {
UNCOV
749
                case lnav::progress_result_t::ok:
×
UNCOV
750
                    break;
×
UNCOV
751
                case lnav::progress_result_t::interrupt:
×
752
                    return false;
×
753
            }
754
        }
755
    }
23✔
756
    if (this->gs_index_progress) {
23✔
UNCOV
757
        this->gs_index_progress(std::nullopt);
×
758
    }
759

760
    size_t filtered_in_count = 0;
23✔
761
    for (const auto& filt : this->tss_filters) {
25✔
762
        if (!filt->is_enabled()) {
2✔
UNCOV
763
            continue;
×
764
        }
765
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
766
            filtered_in_count += 1;
1✔
767
        }
768
    }
769
    this->gs_filter_hits = {};
23✔
770
    this->gs_time_order.clear();
23✔
771
    this->gs_time_order.reserve(this->gs_active_opids.size());
23✔
772
    for (auto& pair : this->gs_active_opids) {
1,027✔
773
        auto& otr = pair.second.or_value;
1,004✔
774
        std::string full_desc;
1,004✔
775
        const auto& desc_defs = pair.second.or_description_defs.odd_defs;
1,004✔
776
        for (auto& desc : pair.second.or_descriptions) {
2,000✔
777
            auto desc_def_iter = desc_defs.find(desc.first);
996✔
778
            if (desc_def_iter == desc_defs.end()) {
996✔
UNCOV
779
                continue;
×
780
            }
781
            const auto& desc_def = desc_def_iter->second;
996✔
782
            full_desc = desc_def.to_string(desc.second);
996✔
783
        }
784
        pair.second.or_descriptions.clear();
1,004✔
785
        auto full_desc_sf = string_fragment::from_str(full_desc);
1,004✔
786
        auto desc_sf_iter = this->gs_descriptions.find(full_desc_sf);
1,004✔
787
        if (desc_sf_iter == this->gs_descriptions.end()) {
1,004✔
788
            full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,008✔
789
                this->gs_allocator);
1,004✔
790
        }
791
        pair.second.or_description = full_desc_sf;
1,004✔
792

793
        shared_buffer sb_opid;
1,004✔
794
        shared_buffer_ref sbr_opid;
1,004✔
795
        sbr_opid.share(
1,004✔
796
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,004✔
797
        shared_buffer sb_desc;
1,004✔
798
        shared_buffer_ref sbr_desc;
1,004✔
799
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,004✔
800
        if (this->tss_apply_filters) {
1,004✔
801
            auto filtered_in = false;
1,004✔
802
            auto filtered_out = false;
1,004✔
803
            for (const auto& filt : this->tss_filters) {
1,170✔
804
                if (!filt->is_enabled()) {
166✔
UNCOV
805
                    continue;
×
806
                }
807
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
498✔
808
                    if (filt->matches(std::nullopt, *sbr)) {
332✔
809
                        this->gs_filter_hits[filt->get_index()] += 1;
2✔
810
                        switch (filt->get_type()) {
2✔
811
                            case text_filter::INCLUDE:
1✔
812
                                filtered_in = true;
1✔
813
                                break;
1✔
814
                            case text_filter::EXCLUDE:
1✔
815
                                filtered_out = true;
1✔
816
                                break;
1✔
UNCOV
817
                            default:
×
UNCOV
818
                                break;
×
819
                        }
820
                    }
821
                }
822
            }
823

824
            if (min_log_time_opt
1,004✔
825
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,004✔
826
            {
827
                filtered_out = true;
16✔
828
            }
829
            if (max_log_time_opt
1,004✔
830
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,004✔
831
            {
832
                filtered_out = true;
16✔
833
            }
834

835
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,004✔
836
                this->gs_filtered_count += 1;
115✔
837
                continue;
115✔
838
            }
839
        }
840

841
        if (pair.second.or_name.length() > this->gs_opid_width) {
889✔
842
            this->gs_opid_width = pair.second.or_name.length();
20✔
843
        }
844
        if (full_desc.size() > max_desc_width) {
889✔
845
            max_desc_width = full_desc.size();
12✔
846
        }
847

848
        if (this->gs_lower_bound.tv_sec == 0
1,778✔
849
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
889✔
850
        {
851
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
86✔
852
        }
853
        if (this->gs_upper_bound.tv_sec == 0
1,778✔
854
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
889✔
855
        {
856
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
66✔
857
        }
858
        this->gs_time_order.emplace_back(pair.second);
889✔
859
    }
1,464✔
860
    std::stable_sort(this->gs_time_order.begin(),
23✔
861
                     this->gs_time_order.end(),
862
                     std::less<const opid_row>{});
863
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
912✔
864
        const auto& row = this->gs_time_order[lpc].get();
889✔
865
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
889✔
866
            bm_errs.insert_once(vis_line_t(lpc));
19✔
867
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
870✔
UNCOV
868
            bm_warns.insert_once(vis_line_t(lpc));
×
869
        }
870
    }
871

872
    this->gs_opid_width = std::min(this->gs_opid_width, MAX_OPID_WIDTH);
23✔
873
    this->gs_total_width
874
        = std::max<size_t>(22 + this->gs_opid_width + max_desc_width,
46✔
875
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
23✔
876

877
    this->tss_view->set_needs_update();
23✔
878

879
    return true;
23✔
880
}
881

882
std::optional<vis_line_t>
883
timeline_source::row_for_time(struct timeval time_bucket)
11✔
884
{
885
    auto iter = this->gs_time_order.begin();
11✔
886
    while (true) {
887
        if (iter == this->gs_time_order.end()) {
79✔
888
            return std::nullopt;
2✔
889
        }
890

891
        if (iter->get().or_value.otr_range.contains_inclusive(time_bucket)) {
77✔
892
            break;
9✔
893
        }
894
        ++iter;
68✔
895
    }
896

897
    auto closest_iter = iter;
9✔
898
    auto closest_diff = time_bucket - iter->get().or_value.otr_range.tr_begin;
9✔
899
    for (; iter != this->gs_time_order.end(); ++iter) {
18✔
900
        if (time_bucket < iter->get().or_value.otr_range.tr_begin) {
14✔
901
            break;
5✔
902
        }
903
        if (!iter->get().or_value.otr_range.contains_inclusive(time_bucket)) {
9✔
UNCOV
904
            continue;
×
905
        }
906

907
        auto diff = time_bucket - iter->get().or_value.otr_range.tr_begin;
9✔
908
        if (diff < closest_diff) {
9✔
UNCOV
909
            closest_iter = iter;
×
UNCOV
910
            closest_diff = diff;
×
911
        }
912

913
        for (const auto& sub : iter->get().or_value.otr_sub_ops) {
9✔
UNCOV
914
            if (!sub.ostr_range.contains_inclusive(time_bucket)) {
×
UNCOV
915
                continue;
×
916
            }
917

UNCOV
918
            diff = time_bucket - sub.ostr_range.tr_begin;
×
UNCOV
919
            if (diff < closest_diff) {
×
920
                closest_iter = iter;
×
UNCOV
921
                closest_diff = diff;
×
922
            }
923
        }
924
    }
925

926
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
18✔
927
}
928

929
std::optional<text_time_translator::row_info>
930
timeline_source::time_for_row(vis_line_t row)
26✔
931
{
932
    if (row >= this->gs_time_order.size()) {
26✔
UNCOV
933
        return std::nullopt;
×
934
    }
935

936
    const auto& otr = this->gs_time_order[row].get().or_value;
26✔
937

938
    if (this->tss_view->get_selection() == row) {
26✔
939
        auto ov_sel = this->tss_view->get_overlay_selection();
26✔
940

941
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
26✔
UNCOV
942
            return row_info{
×
UNCOV
943
                otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin,
×
944
                row,
945
            };
946
        }
947
    }
948

949
    auto preview_selection = this->gs_preview_view.get_selection();
26✔
950
    if (!preview_selection) {
26✔
UNCOV
951
        return std::nullopt;
×
952
    }
953
    if (preview_selection < this->gs_preview_rows.size()) {
26✔
UNCOV
954
        return this->gs_preview_rows[preview_selection.value()];
×
955
    }
956

957
    return row_info{
52✔
958
        otr.otr_range.tr_begin,
959
        row,
960
    };
52✔
961
}
962

963
size_t
964
timeline_source::text_line_width(textview_curses& curses)
5,396✔
965
{
966
    return this->gs_total_width;
5,396✔
967
}
968

969
void
970
timeline_source::text_selection_changed(textview_curses& tc)
5✔
971
{
972
    static const size_t MAX_PREVIEW_LINES = 200;
973

974
    auto sel = tc.get_selection();
5✔
975

976
    this->gs_preview_source.clear();
5✔
977
    this->gs_preview_rows.clear();
5✔
978
    if (!sel || sel.value() >= this->gs_time_order.size()) {
5✔
979
        return;
5✔
980
    }
981

UNCOV
982
    const auto& row = this->gs_time_order[sel.value()].get();
×
UNCOV
983
    auto low_tv = row.or_value.otr_range.tr_begin;
×
UNCOV
984
    auto high_tv = row.or_value.otr_range.tr_end;
×
985
    auto id_sf = row.or_name;
×
UNCOV
986
    auto level_stats = row.or_value.otr_level_stats;
×
UNCOV
987
    auto ov_sel = tc.get_overlay_selection();
×
UNCOV
988
    if (ov_sel) {
×
UNCOV
989
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
UNCOV
990
        id_sf = sub.ostr_subid;
×
UNCOV
991
        low_tv = sub.ostr_range.tr_begin;
×
UNCOV
992
        high_tv = sub.ostr_range.tr_end;
×
UNCOV
993
        level_stats = sub.ostr_level_stats;
×
994
    }
UNCOV
995
    high_tv.tv_sec += 1;
×
UNCOV
996
    auto low_vl = this->gs_lss.row_for_time(low_tv);
×
UNCOV
997
    auto high_vl = this->gs_lss.row_for_time(high_tv).value_or(
×
UNCOV
998
        vis_line_t(this->gs_lss.text_line_count()));
×
999

UNCOV
1000
    if (!low_vl) {
×
UNCOV
1001
        return;
×
1002
    }
1003

UNCOV
1004
    auto preview_content = attr_line_t();
×
UNCOV
1005
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
×
UNCOV
1006
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
×
UNCOV
1007
    auto id_hash = row.or_name.hash();
×
UNCOV
1008
    for (const auto& msg_line : win) {
×
UNCOV
1009
        if (!msg_line.get_logline().match_opid_hash(id_hash)) {
×
UNCOV
1010
            continue;
×
1011
        }
1012

UNCOV
1013
        const auto& lvv = msg_line.get_values();
×
UNCOV
1014
        if (!lvv.lvv_opid_value) {
×
UNCOV
1015
            continue;
×
1016
        }
UNCOV
1017
        auto opid_sf = lvv.lvv_opid_value.value();
×
1018

UNCOV
1019
        if (opid_sf == row.or_name) {
×
UNCOV
1020
            std::vector<attr_line_t> rows_al(msg_line.get_line_count());
×
1021

UNCOV
1022
            auto cl = this->gs_lss.at(msg_line.get_vis_line());
×
UNCOV
1023
            this->gs_log_view.listview_value_for_rows(
×
UNCOV
1024
                this->gs_log_view, msg_line.get_vis_line(), rows_al);
×
1025

UNCOV
1026
            for (const auto& row_al : rows_al) {
×
UNCOV
1027
                this->gs_preview_rows.emplace_back(
×
UNCOV
1028
                    msg_line.get_logline().get_timeval(), cl);
×
UNCOV
1029
                ++cl;
×
UNCOV
1030
                preview_content.append(row_al).append("\n");
×
1031
            }
UNCOV
1032
            msgs_remaining -= 1;
×
1033
            if (msgs_remaining == 0) {
×
1034
                break;
×
1035
            }
1036
        }
1037
    }
1038

UNCOV
1039
    this->gs_preview_source.replace_with(preview_content);
×
UNCOV
1040
    this->gs_preview_view.set_selection(0_vl);
×
UNCOV
1041
    this->gs_preview_status_source.get_description().set_value(
×
1042
        " ID %.*s", id_sf.length(), id_sf.data());
UNCOV
1043
    auto err_count = level_stats.lls_error_count;
×
UNCOV
1044
    if (err_count == 0) {
×
UNCOV
1045
        this->gs_preview_status_source
×
UNCOV
1046
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
UNCOV
1047
            .set_value("");
×
UNCOV
1048
    } else if (err_count > 1) {
×
UNCOV
1049
        this->gs_preview_status_source
×
UNCOV
1050
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
UNCOV
1051
            .set_value("%'d errors", err_count);
×
1052
    } else {
UNCOV
1053
        this->gs_preview_status_source
×
UNCOV
1054
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
UNCOV
1055
            .set_value("%'d error", err_count);
×
1056
    }
UNCOV
1057
    this->gs_preview_status_source
×
UNCOV
1058
        .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
×
UNCOV
1059
        .set_value("%'d messages ", level_stats.lls_total_count);
×
1060
}
1061

1062
void
1063
timeline_source::text_filters_changed()
12✔
1064
{
1065
    this->rebuild_indexes();
12✔
1066
    this->tss_view->reload_data();
12✔
1067
    this->tss_view->redo_search();
12✔
1068
}
12✔
1069

1070
int
1071
timeline_source::get_filtered_count() const
45✔
1072
{
1073
    return this->gs_filtered_count;
45✔
1074
}
1075

1076
int
UNCOV
1077
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1078
{
UNCOV
1079
    return this->gs_filter_hits[filter_index];
×
1080
}
1081

1082
static const std::vector<breadcrumb::possibility>&
UNCOV
1083
timestamp_poss()
×
1084
{
1085
    const static std::vector<breadcrumb::possibility> retval = {
1086
        breadcrumb::possibility{"-1 day"},
1087
        breadcrumb::possibility{"-1h"},
1088
        breadcrumb::possibility{"-30m"},
1089
        breadcrumb::possibility{"-15m"},
1090
        breadcrumb::possibility{"-5m"},
1091
        breadcrumb::possibility{"-1m"},
1092
        breadcrumb::possibility{"+1m"},
1093
        breadcrumb::possibility{"+5m"},
1094
        breadcrumb::possibility{"+15m"},
1095
        breadcrumb::possibility{"+30m"},
1096
        breadcrumb::possibility{"+1h"},
1097
        breadcrumb::possibility{"+1 day"},
1098
    };
1099

1100
    return retval;
×
1101
}
1102

1103
void
UNCOV
1104
timeline_source::text_crumbs_for_line(int line,
×
1105
                                      std::vector<breadcrumb::crumb>& crumbs)
1106
{
1107
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1108

UNCOV
1109
    if (line >= this->gs_time_order.size()) {
×
1110
        return;
×
1111
    }
1112

1113
    const auto& row = this->gs_time_order[line].get();
×
1114
    char ts[64];
1115

UNCOV
1116
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1117

UNCOV
1118
    crumbs.emplace_back(std::string(ts),
×
1119
                        timestamp_poss,
UNCOV
1120
                        [ec = this->gs_exec_context](const auto& ts) {
×
UNCOV
1121
                            auto cmd
×
UNCOV
1122
                                = fmt::format(FMT_STRING(":goto {}"),
×
1123
                                              ts.template get<std::string>());
UNCOV
1124
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
UNCOV
1125
                        });
×
UNCOV
1126
    crumbs.back().c_expected_input
×
UNCOV
1127
        = breadcrumb::crumb::expected_input_t::anything;
×
UNCOV
1128
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1129
}
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