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

tstack / lnav / 18696676448-2596

21 Oct 2025 08:10PM UTC coverage: 69.94% (-0.08%) from 70.021%
18696676448-2596

push

github

tstack
[timeline] try a different approach with header

91 of 102 new or added lines in 6 files covered. (89.22%)

63 existing lines in 6 files now uncovered.

50804 of 72639 relevant lines covered (69.94%)

423532.08 hits per line

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

62.61
/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 <algorithm>
31
#include <chrono>
32
#include <utility>
33
#include <vector>
34

35
#include "timeline_source.hh"
36

37
#include <time.h>
38

39
#include "base/humanize.hh"
40
#include "base/humanize.time.hh"
41
#include "base/itertools.enumerate.hh"
42
#include "base/itertools.hh"
43
#include "base/keycodes.hh"
44
#include "base/math_util.hh"
45
#include "command_executor.hh"
46
#include "crashd.client.hh"
47
#include "lnav_util.hh"
48
#include "logline_window.hh"
49
#include "md4cpp.hh"
50
#include "sql_util.hh"
51
#include "sysclip.hh"
52

53
using namespace std::chrono_literals;
54
using namespace lnav::roles::literals;
55
using namespace md4cpp::literals;
56

57
static const std::vector<std::chrono::microseconds> TIME_SPANS = {
58
    500us, 1ms,   100ms, 500ms, 1s, 5s, 10s, 15s,     30s,      1min,
59
    5min,  15min, 1h,    2h,    4h, 8h, 24h, 7 * 24h, 30 * 24h, 365 * 24h,
60
};
61

62
static constexpr size_t MAX_OPID_WIDTH = 80;
63
static constexpr size_t MAX_DESC_WIDTH = 256;
64
static constexpr int CHART_INDENT = 22;
65

66
size_t
UNCOV
67
abbrev_ftime(char* datebuf, size_t db_size, const tm& lb_tm, const tm& dt)
×
68
{
UNCOV
69
    char lb_fmt[32] = " ";
×
UNCOV
70
    bool same = true;
×
71

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

117
std::vector<attr_line_t>
118
timeline_preview_overlay::list_overlay_menu(const listview_curses& lv,
×
119
                                            vis_line_t line)
120
{
121
    static constexpr auto MENU_WIDTH = 25;
122

123
    const auto* tc = dynamic_cast<const textview_curses*>(&lv);
×
124
    std::vector<attr_line_t> retval;
×
125

126
    if (tc->tc_text_selection_active || !tc->tc_selected_text) {
×
127
        return retval;
×
128
    }
129

130
    const auto& sti = tc->tc_selected_text.value();
×
131

132
    if (sti.sti_line != line) {
×
133
        return retval;
×
134
    }
135
    auto title = " Actions "_status_title;
×
136
    auto left = std::max(0, sti.sti_x - 2);
×
137
    auto dim = lv.get_dimensions();
×
138
    auto menu_line = vis_line_t{1};
×
139

140
    if (left + MENU_WIDTH >= dim.second) {
×
141
        left = dim.second - MENU_WIDTH;
×
142
    }
143

144
    this->los_menu_items.clear();
×
145

146
    retval.emplace_back(attr_line_t().pad_to(left).append(title));
×
147
    {
148
        auto start = left;
×
149
        attr_line_t al;
×
150

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

165
                auto clip_pipe = clip_res.unwrap();
×
166
                fwrite(value.c_str(), 1, value.length(), clip_pipe.in());
×
167
            });
×
168
        retval.emplace_back(attr_line_t().pad_to(left).append(al));
×
169
    }
170

171
    return retval;
×
172
}
×
173

174
timeline_header_overlay::timeline_header_overlay(
606✔
175
    const std::shared_ptr<timeline_source>& src)
606✔
176
    : gho_src(src)
606✔
177
{
178
}
606✔
179

180
bool
181
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
21✔
182
                                             media_t media,
183
                                             int y,
184
                                             int bottom,
185
                                             attr_line_t& value_out)
186
{
187
    if (y >= 1) {
21✔
188
        return false;
9✔
189
    }
190

191
    if (this->gho_src->gs_time_order.empty()) {
12✔
192
        if (y == 0) {
×
193
            value_out.append("No operations found"_error);
×
194
            return true;
×
195
        }
196

197
        return false;
×
198
    }
199

200
    auto sel = lv.get_selection().value_or(0_vl);
12✔
201
    const auto& row = this->gho_src->gs_time_order[sel].get();
12✔
202
    auto tr = row.or_value.otr_range;
12✔
203
    auto [lb, ub] = this->gho_src->get_time_bounds_for(sel);
12✔
204
    auto sel_begin_us = to_us(tr.tr_begin - lb);
12✔
205
    auto sel_end_us = to_us(tr.tr_end - lb);
12✔
206

207
    tm sel_lb_tm;
208
    secs2tm(lb.tv_sec, &sel_lb_tm);
12✔
209
    tm sel_ub_tm;
210
    secs2tm(ub.tv_sec, &sel_ub_tm);
12✔
211

212
    auto [height, width] = lv.get_dimensions();
12✔
213
    if (width <= CHART_INDENT) {
12✔
NEW
214
        return true;
×
215
    }
216

217
    value_out.append("   Duration   "_h1)
12✔
218
        .append("|", VC_GRAPHIC.value(NCACS_VLINE))
12✔
219
        .append(" ")
12✔
220
        .append("\u2718"_error)
12✔
221
        .append("\u25b2"_warning)
12✔
222
        .append(" ")
12✔
223
        .append("|", VC_GRAPHIC.value(NCACS_VLINE))
24✔
224
        .append(" Operation"_h1);
12✔
225
    auto line_width = CHART_INDENT;
12✔
226
    auto mark_width = (double) (width - line_width);
12✔
227
    double span = to_us(ub - lb).count();
12✔
228
    auto us_per_ch = std::chrono::microseconds{(int64_t) (span / mark_width)};
12✔
229
    auto us_per_inc = us_per_ch * 10;
12✔
230
    auto lr = line_range{
231
        static_cast<int>(CHART_INDENT + floor(sel_begin_us / us_per_ch)),
12✔
232
        static_cast<int>(CHART_INDENT + ceil(sel_end_us / us_per_ch)),
24✔
233
        line_range::unit::codepoint,
234
    };
12✔
235
    if (lr.lr_start == lr.lr_end) {
12✔
236
        lr.lr_end += 1;
1✔
237
    }
238
    value_out.get_attrs().emplace_back(lr,
24✔
239
                                       VC_ROLE.value(role_t::VCR_CURSOR_LINE));
24✔
240
    auto total_us = std::chrono::microseconds{0};
12✔
241
    std::vector<std::string> durations;
12✔
242
    auto remaining_width = mark_width - 10;
12✔
243
    auto max_width = size_t{0};
12✔
244
    while (remaining_width > 0) {
45✔
245
        total_us += us_per_inc;
33✔
246
        auto dur = humanize::time::duration::from_tv(to_timeval(total_us));
33✔
247
        if (us_per_inc > 24 * 1h) {
33✔
NEW
248
            dur.with_resolution(24 * 1h);
×
249
        } else if (us_per_inc > 1h) {
33✔
NEW
250
            dur.with_resolution(1h);
×
251
        } else if (us_per_inc > 1min) {
33✔
NEW
252
            dur.with_resolution(1min);
×
253
        } else if (us_per_inc > 2s) {
33✔
254
            dur.with_resolution(1s);
3✔
255
        }
256
        durations.emplace_back(dur.to_string());
33✔
257
        max_width = std::max(durations.back().size(), max_width);
33✔
258
        remaining_width -= 10;
33✔
259
    }
260
    for (auto& label : durations) {
45✔
261
        line_width += 10;
33✔
262
        value_out.pad_to(line_width)
33✔
263
            .append("|", VC_GRAPHIC.value(NCACS_VLINE))
66✔
264
            .append(max_width - label.size(), ' ')
33✔
265
            .append(label);
33✔
266
    }
267

268
    auto hdr_attrs = text_attrs::with_underline();
12✔
269
    value_out.with_attr_for_all(VC_STYLE.value(hdr_attrs))
12✔
270
        .with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
12✔
271

272
    return true;
12✔
273
}
12✔
274
void
275
timeline_header_overlay::list_value_for_overlay(
365✔
276
    const listview_curses& lv,
277
    vis_line_t line,
278
    std::vector<attr_line_t>& value_out)
279
{
280
    if (!this->gho_show_details) {
365✔
281
        return;
365✔
282
    }
283

284
    if (lv.get_selection() != line) {
×
285
        return;
×
286
    }
287

288
    if (line >= this->gho_src->gs_time_order.size()) {
×
289
        return;
×
290
    }
291

292
    const auto& row = this->gho_src->gs_time_order[line].get();
×
293

294
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
295
        return;
×
296
    }
297

298
    auto width = lv.get_dimensions().second;
×
299

300
    if (width < 37) {
×
301
        return;
×
302
    }
303

304
    width -= 37;
×
305
    double span = row.or_value.otr_range.duration().count();
×
306
    double per_ch = span / (double) width;
×
307

308
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
309
        value_out.resize(value_out.size() + 1);
×
310

311
        auto& al = value_out.back();
×
312
        auto& attrs = al.get_attrs();
×
313
        auto total_msgs = sub.ostr_level_stats.lls_total_count;
×
314
        auto duration = sub.ostr_range.tr_end - sub.ostr_range.tr_begin;
×
315
        auto duration_str = fmt::format(
316
            FMT_STRING(" {: >13}"),
×
317
            humanize::time::duration::from_tv(duration).to_string());
×
318
        al.pad_to(14)
×
319
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
×
320
            .append(" ")
×
321
            .append(lnav::roles::error(humanize::sparkline(
×
322
                sub.ostr_level_stats.lls_error_count, total_msgs)))
×
323
            .append(lnav::roles::warning(humanize::sparkline(
×
324
                sub.ostr_level_stats.lls_warning_count, total_msgs)))
×
325
            .append(" ")
×
326
            .append(lnav::roles::identifier(sub.ostr_subid.to_string()))
×
327
            .append(row.or_max_subid_width
×
328
                        - sub.ostr_subid.utf8_length().unwrapOr(
×
329
                            row.or_max_subid_width),
×
330
                    ' ')
331
            .append(sub.ostr_description);
×
332
        al.with_attr_for_all(VC_ROLE.value(role_t::VCR_COMMENT));
×
333

334
        auto start_diff = (double) to_mstime(sub.ostr_range.tr_begin
×
335
                                             - row.or_value.otr_range.tr_begin);
×
336
        auto end_diff = (double) to_mstime(sub.ostr_range.tr_end
×
337
                                           - row.or_value.otr_range.tr_begin);
×
338

339
        auto lr = line_range{
340
            (int) (32 + (start_diff / per_ch)),
×
341
            (int) (32 + (end_diff / per_ch)),
×
342
            line_range::unit::codepoint,
343
        };
344

345
        if (lr.lr_start == lr.lr_end) {
×
346
            lr.lr_end += 1;
×
347
        }
348

349
        auto block_attrs = text_attrs::with_reverse();
×
350
        attrs.emplace_back(lr, VC_STYLE.value(block_attrs));
×
351
    }
352
    if (!value_out.empty()) {
×
353
        value_out.back().get_attrs().emplace_back(
×
354
            line_range{0, -1}, VC_STYLE.value(text_attrs::with_underline()));
×
355
    }
356
}
357
std::optional<attr_line_t>
358
timeline_header_overlay::list_header_for_overlay(const listview_curses& lv,
×
359
                                                 vis_line_t line)
360
{
361
    if (lv.get_overlay_selection()) {
×
362
        return attr_line_t("\u258C Sub-operations: Press ")
×
363
            .append("Esc"_hotkey)
×
364
            .append(" to exit this panel");
×
365
    }
366
    return attr_line_t("\u258C Sub-operations: Press ")
×
367
        .append("CTRL-]"_hotkey)
×
368
        .append(" to focus on this panel");
×
369
}
370

371
timeline_source::timeline_source(textview_curses& log_view,
606✔
372
                                 logfile_sub_source& lss,
373
                                 textview_curses& preview_view,
374
                                 plain_text_source& preview_source,
375
                                 statusview_curses& preview_status_view,
376
                                 timeline_status_source& preview_status_source)
606✔
377
    : gs_log_view(log_view), gs_lss(lss), gs_preview_view(preview_view),
606✔
378
      gs_preview_source(preview_source),
606✔
379
      gs_preview_status_view(preview_status_view),
606✔
380
      gs_preview_status_source(preview_status_source)
606✔
381
{
382
    this->tss_supports_filtering = true;
606✔
383
    this->gs_preview_view.set_overlay_source(&this->gs_preview_overlay);
606✔
384
}
606✔
385

386
bool
387
timeline_source::list_input_handle_key(listview_curses& lv, const ncinput& ch)
×
388
{
389
    switch (ch.eff_text[0]) {
×
390
        case 'q':
×
391
        case KEY_ESCAPE: {
392
            if (this->gs_preview_focused) {
×
393
                this->gs_preview_focused = false;
×
394
                this->gs_preview_view.set_height(5_vl);
×
395
                this->gs_preview_status_view.set_enabled(
×
396
                    this->gs_preview_focused);
×
397
                this->tss_view->set_enabled(!this->gs_preview_focused);
×
398
                return true;
×
399
            }
400
            break;
×
401
        }
402
        case '\n':
×
403
        case '\r':
404
        case NCKEY_ENTER: {
405
            this->gs_preview_focused = !this->gs_preview_focused;
×
406
            this->gs_preview_status_view.set_enabled(this->gs_preview_focused);
×
407
            this->tss_view->set_enabled(!this->gs_preview_focused);
×
408
            if (this->gs_preview_focused) {
×
409
                auto height = this->tss_view->get_dimensions().first;
×
410

411
                if (height > 5) {
×
412
                    this->gs_preview_view.set_height(height / 2_vl);
×
413
                }
414
            } else {
415
                this->gs_preview_view.set_height(5_vl);
×
416
            }
417
            return true;
×
418
        }
419
    }
420
    if (this->gs_preview_focused) {
×
421
        return this->gs_preview_view.handle_key(ch);
×
422
    }
423

424
    return false;
×
425
}
426

427
bool
428
timeline_source::text_handle_mouse(
×
429
    textview_curses& tc,
430
    const listview_curses::display_line_content_t&,
431
    mouse_event& me)
432
{
433
    auto nci = ncinput{};
×
434
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
435
        nci.id = '\r';
×
436
        nci.eff_text[0] = '\r';
×
437
        this->list_input_handle_key(tc, nci);
×
438
    }
439

440
    return false;
×
441
}
442

443
std::pair<timeval, timeval>
444
timeline_source::get_time_bounds_for(int line)
374✔
445
{
446
    const auto low_index = this->tss_view->get_top();
374✔
447
    auto high_index
448
        = std::min(this->tss_view->get_bottom(),
374✔
449
                   vis_line_t((int) this->gs_time_order.size() - 1));
374✔
450
    const auto& low_row = this->gs_time_order[low_index].get();
374✔
451
    const auto& high_row = this->gs_time_order[high_index].get();
374✔
452
    auto low_tv = low_row.or_value.otr_range.tr_begin;
374✔
453
    auto high_tv = high_row.or_value.otr_range.tr_begin;
374✔
454

455
    for (auto index = low_index; index <= high_index; index += 1_vl) {
748✔
456
        const auto& row = this->gs_time_order[index].get();
374✔
457

458
        if (high_tv < row.or_value.otr_range.tr_end) {
374✔
459
            high_tv = row.or_value.otr_range.tr_end;
372✔
460
        }
461
    }
462
    auto duration = to_us(high_tv - low_tv);
374✔
463
    auto span_iter
464
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
374✔
465
    if (span_iter == TIME_SPANS.end()) {
374✔
466
        --span_iter;
×
467
    }
468
    auto span_portion = *span_iter / 8;
374✔
469
    auto lower_dur = to_us(low_tv);
374✔
470
    lower_dur = rounddown(lower_dur, span_portion);
374✔
471
    auto upper_dur = to_us(high_tv);
374✔
472
    upper_dur = roundup(upper_dur, span_portion);
374✔
473
    auto lower_tv = to_timeval(lower_dur);
374✔
474
    auto upper_tv = to_timeval(upper_dur);
374✔
475

476
    return {lower_tv, upper_tv};
748✔
477
}
478

479
size_t
480
timeline_source::text_line_count()
7,278✔
481
{
482
    return this->gs_time_order.size();
7,278✔
483
}
484

485
line_info
486
timeline_source::text_value_for_line(textview_curses& tc,
362✔
487
                                     int line,
488
                                     std::string& value_out,
489
                                     line_flags_t flags)
490
{
491
    if (!this->ts_rebuild_in_progress
724✔
492
        && line < (ssize_t) this->gs_time_order.size())
362✔
493
    {
494
        const auto& row = this->gs_time_order[line].get();
362✔
495
        auto duration
496
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
362✔
497
        auto duration_str = fmt::format(
498
            FMT_STRING(" {: >13}"),
1,086✔
499
            humanize::time::duration::from_tv(duration).to_string());
724✔
500

501
        this->gs_rendered_line.clear();
362✔
502

503
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
362✔
504
        auto truncated_name
505
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
362✔
506
        auto truncated_desc = attr_line_t::from_table_cell_content(
507
            row.or_description, MAX_DESC_WIDTH);
362✔
508
        this->gs_rendered_line
509
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
724✔
510
            .append("  ")
362✔
511
            .append(lnav::roles::error(humanize::sparkline(
1,086✔
512
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
362✔
513
            .append(lnav::roles::warning(humanize::sparkline(
1,086✔
514
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
362✔
515
            .append("  ")
362✔
516
            .append(lnav::roles::identifier(truncated_name))
724✔
517
            .append(
724✔
518
                this->gs_opid_width - truncated_name.utf8_length_or_length(),
362✔
519
                ' ')
520
            .append(truncated_desc);
362✔
521
        this->gs_rendered_line.with_attr_for_all(
362✔
522
            VC_ROLE.value(role_t::VCR_COMMENT));
724✔
523

524
        value_out = this->gs_rendered_line.get_string();
362✔
525
    }
362✔
526

527
    return {};
362✔
528
}
529

530
void
531
timeline_source::text_attrs_for_line(textview_curses& tc,
362✔
532
                                     int line,
533
                                     string_attrs_t& value_out)
534
{
535
    if (!this->ts_rebuild_in_progress
724✔
536
        && line < (ssize_t) this->gs_time_order.size())
362✔
537
    {
538
        const auto& row = this->gs_time_order[line].get();
362✔
539

540
        value_out = this->gs_rendered_line.get_attrs();
362✔
541

542
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
362✔
543
        auto [sel_lb, sel_ub]
362✔
544
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
362✔
545

546
        if (row.or_value.otr_range.tr_begin <= sel_ub
362✔
547
            && sel_lb <= row.or_value.otr_range.tr_end)
362✔
548
        {
549
            auto width = tc.get_dimensions().second;
66✔
550

551
            if (width > CHART_INDENT) {
66✔
552
                width -= CHART_INDENT;
66✔
553
                double span = to_us(sel_ub - sel_lb).count();
66✔
554
                auto us_per_ch = std::chrono::microseconds{
555
                    static_cast<int64_t>(span / (double) width)};
66✔
556

557
                if (row.or_value.otr_range.tr_begin <= sel_lb) {
66✔
NEW
558
                    lr.lr_start = CHART_INDENT;
×
559
                } else {
560
                    auto start_diff
561
                        = to_us(row.or_value.otr_range.tr_begin - sel_lb);
66✔
562

563
                    lr.lr_start = CHART_INDENT + floor(start_diff / us_per_ch);
66✔
564
                }
565

566
                if (sel_ub < row.or_value.otr_range.tr_end) {
66✔
567
                    lr.lr_end = -1;
4✔
568
                } else {
569
                    auto end_diff
570
                        = to_us(row.or_value.otr_range.tr_end - sel_lb);
62✔
571

572
                    lr.lr_end = CHART_INDENT + ceil(end_diff / us_per_ch);
62✔
573
                    if (lr.lr_start == lr.lr_end) {
62✔
574
                        lr.lr_end += 1;
41✔
575
                    }
576
                }
577

578
                auto block_attrs = text_attrs::with_reverse();
66✔
579
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
66✔
580
            }
581
        }
582
        auto alt_row_index = line % 4;
362✔
583
        if (alt_row_index == 2 || alt_row_index == 3) {
362✔
584
            value_out.emplace_back(line_range{0, -1},
176✔
585
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
352✔
586
        }
587
    }
588
}
362✔
589

590
size_t
591
timeline_source::text_size_for_line(textview_curses& tc,
×
592
                                    int line,
593
                                    text_sub_source::line_flags_t raw)
594
{
595
    return this->gs_total_width;
×
596
}
597

598
bool
599
timeline_source::rebuild_indexes()
26✔
600
{
601
    static auto op = lnav_operation{"timeline_rebuild"};
26✔
602

603
    auto op_guard = lnav_opid_guard::internal(op);
26✔
604
    auto& bm = this->tss_view->get_bookmarks();
26✔
605
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
26✔
606
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
26✔
607

608
    this->ts_rebuild_in_progress = true;
26✔
609
    bm_errs.clear();
26✔
610
    bm_warns.clear();
26✔
611

612
    this->gs_lower_bound = {};
26✔
613
    this->gs_upper_bound = {};
26✔
614
    this->gs_opid_width = 0;
26✔
615
    this->gs_total_width = 0;
26✔
616
    this->gs_filtered_count = 0;
26✔
617
    this->gs_active_opids.clear();
26✔
618
    this->gs_descriptions.clear();
26✔
619
    this->gs_subid_map.clear();
26✔
620
    this->gs_allocator.reset();
26✔
621
    this->gs_preview_source.clear();
26✔
622
    this->gs_preview_rows.clear();
26✔
623
    this->gs_preview_status_source.get_description().clear();
26✔
624

625
    auto min_log_time_opt = this->get_min_row_time();
26✔
626
    auto max_log_time_opt = this->get_max_row_time();
26✔
627
    auto max_desc_width = size_t{0};
26✔
628

629
    log_info("building opid table");
26✔
630
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
53✔
631
        if (ld->get_file_ptr() == nullptr) {
27✔
632
            continue;
1✔
633
        }
634
        if (!ld->is_visible()) {
27✔
635
            continue;
1✔
636
        }
637

638
        ld->get_file_ptr()->enable_cache();
26✔
639
        auto format = ld->get_file_ptr()->get_format();
26✔
640
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
641
            ld->get_file_ptr()->get_opids());
26✔
642
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,091✔
643
            auto& otr = pair.second;
1,065✔
644
            auto active_iter = this->gs_active_opids.find(pair.first);
1,065✔
645
            if (active_iter == this->gs_active_opids.end()) {
1,065✔
646
                auto opid = pair.first.to_owned(this->gs_allocator);
1,065✔
647
                auto active_emp_res = this->gs_active_opids.emplace(
2,130✔
648
                    opid,
649
                    opid_row{
1,065✔
650
                        opid,
651
                        otr,
652
                        string_fragment::invalid(),
653
                    });
654
                active_iter = active_emp_res.first;
1,065✔
655
            } else {
656
                active_iter->second.or_value |= otr;
×
657
            }
658

659
            auto& row = active_iter->second;
1,065✔
660
            for (auto& sub : active_iter->second.or_value.otr_sub_ops) {
1,065✔
661
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
662

663
                if (subid_iter == this->gs_subid_map.end()) {
×
664
                    subid_iter = this->gs_subid_map
×
665
                                     .emplace(sub.ostr_subid.to_owned(
×
666
                                                  this->gs_allocator),
×
667
                                              true)
×
668
                                     .first;
669
                }
670
                sub.ostr_subid = subid_iter->first;
×
671
                if (sub.ostr_subid.length()
×
672
                    > active_iter->second.or_max_subid_width)
×
673
                {
674
                    active_iter->second.or_max_subid_width
×
675
                        = sub.ostr_subid.length();
×
676
                }
677
            }
678

679
            if (otr.otr_description.lod_id) {
1,065✔
680
                auto desc_id = otr.otr_description.lod_id.value();
1,055✔
681
                auto desc_def_iter
682
                    = format->lf_opid_description_def->find(desc_id);
1,055✔
683

684
                if (desc_def_iter == format->lf_opid_description_def->end()) {
1,055✔
685
                    log_error("cannot find description: %s",
×
686
                              active_iter->first.data());
687
                } else {
688
                    auto desc_key
689
                        = opid_description_def_key{format->get_name(), desc_id};
1,055✔
690
                    auto desc_defs_iter
691
                        = row.or_description_defs.odd_defs.find(desc_key);
1,055✔
692
                    if (desc_defs_iter
1,055✔
693
                        == row.or_description_defs.odd_defs.end())
1,055✔
694
                    {
695
                        row.or_description_defs.odd_defs.insert(
2,110✔
696
                            desc_key, desc_def_iter->second);
1,055✔
697
                    }
698

699
                    auto& all_descs = active_iter->second.or_descriptions;
1,055✔
700
                    auto& curr_desc_m = all_descs[desc_key];
1,055✔
701
                    const auto& new_desc_v = otr.otr_description.lod_elements;
1,055✔
702

703
                    for (const auto& desc_pair : new_desc_v) {
2,110✔
704
                        curr_desc_m[desc_pair.first] = desc_pair.second;
1,055✔
705
                    }
706
                }
707
            } else if (!otr.otr_description.lod_elements.empty()) {
10✔
708
                auto desc_sf = string_fragment::from_str(
2✔
709
                    otr.otr_description.lod_elements.front().second);
2✔
710
                active_iter->second.or_description
2✔
711
                    = desc_sf.to_owned(this->gs_allocator);
4✔
712
            }
713
            active_iter->second.or_value.otr_description.lod_elements.clear();
1,065✔
714
        }
715

716
        if (this->gs_index_progress) {
26✔
717
            switch (this->gs_index_progress(
×
718
                progress_t{index, this->gs_lss.file_count()}))
×
719
            {
720
                case lnav::progress_result_t::ok:
×
721
                    break;
×
722
                case lnav::progress_result_t::interrupt:
×
NEW
723
                    log_debug("timeline rebuild interrupted");
×
NEW
724
                    this->ts_rebuild_in_progress = false;
×
UNCOV
725
                    return false;
×
726
            }
727
        }
728
    }
26✔
729
    if (this->gs_index_progress) {
26✔
730
        this->gs_index_progress(std::nullopt);
×
731
    }
732
    log_info("active opids: %zu", this->gs_active_opids.size());
26✔
733

734
    size_t filtered_in_count = 0;
26✔
735
    for (const auto& filt : this->tss_filters) {
28✔
736
        if (!filt->is_enabled()) {
2✔
737
            continue;
×
738
        }
739
        if (filt->get_type() == text_filter::INCLUDE) {
2✔
740
            filtered_in_count += 1;
1✔
741
        }
742
    }
743
    this->gs_filter_hits = {};
26✔
744
    this->gs_time_order.clear();
26✔
745
    this->gs_time_order.reserve(this->gs_active_opids.size());
26✔
746
    for (auto& pair : this->gs_active_opids) {
1,091✔
747
        auto& otr = pair.second.or_value;
1,065✔
748
        std::string full_desc;
1,065✔
749
        if (pair.second.or_description.empty()) {
1,065✔
750
            const auto& desc_defs = pair.second.or_description_defs.odd_defs;
1,063✔
751
            for (auto& desc : pair.second.or_descriptions) {
2,118✔
752
                auto desc_def_iter = desc_defs.find(desc.first);
1,055✔
753
                if (desc_def_iter == desc_defs.end()) {
1,055✔
754
                    continue;
×
755
                }
756
                const auto& desc_def = desc_def_iter->second;
1,055✔
757
                full_desc = desc_def.to_string(desc.second);
1,055✔
758
            }
759
            pair.second.or_descriptions.clear();
1,063✔
760
            auto full_desc_sf = string_fragment::from_str(full_desc);
1,063✔
761
            auto desc_sf_iter = this->gs_descriptions.find(full_desc_sf);
1,063✔
762
            if (desc_sf_iter == this->gs_descriptions.end()) {
1,063✔
763
                full_desc_sf = string_fragment::from_str(full_desc).to_owned(
2,126✔
764
                    this->gs_allocator);
1,063✔
765
            }
766
            pair.second.or_description = full_desc_sf;
1,063✔
767
        } else {
768
            full_desc += pair.second.or_description;
2✔
769
        }
770

771
        shared_buffer sb_opid;
1,065✔
772
        shared_buffer_ref sbr_opid;
1,065✔
773
        sbr_opid.share(
1,065✔
774
            sb_opid, pair.second.or_name.data(), pair.second.or_name.length());
1,065✔
775
        shared_buffer sb_desc;
1,065✔
776
        shared_buffer_ref sbr_desc;
1,065✔
777
        sbr_desc.share(sb_desc, full_desc.c_str(), full_desc.length());
1,065✔
778
        if (this->tss_apply_filters) {
1,065✔
779
            auto filtered_in = false;
1,065✔
780
            auto filtered_out = false;
1,065✔
781
            for (const auto& filt : this->tss_filters) {
1,231✔
782
                if (!filt->is_enabled()) {
166✔
783
                    continue;
×
784
                }
785
                for (const auto sbr : {&sbr_opid, &sbr_desc}) {
498✔
786
                    if (filt->matches(std::nullopt, *sbr)) {
332✔
787
                        this->gs_filter_hits[filt->get_index()] += 1;
2✔
788
                        switch (filt->get_type()) {
2✔
789
                            case text_filter::INCLUDE:
1✔
790
                                filtered_in = true;
1✔
791
                                break;
1✔
792
                            case text_filter::EXCLUDE:
1✔
793
                                filtered_out = true;
1✔
794
                                break;
1✔
795
                            default:
×
796
                                break;
×
797
                        }
798
                    }
799
                }
800
            }
801

802
            if (min_log_time_opt
1,065✔
803
                && otr.otr_range.tr_end < min_log_time_opt.value())
1,065✔
804
            {
805
                filtered_out = true;
16✔
806
            }
807
            if (max_log_time_opt
1,065✔
808
                && max_log_time_opt.value() < otr.otr_range.tr_begin)
1,065✔
809
            {
810
                filtered_out = true;
16✔
811
            }
812

813
            if ((filtered_in_count > 0 && !filtered_in) || filtered_out) {
1,065✔
814
                this->gs_filtered_count += 1;
115✔
815
                continue;
115✔
816
            }
817
        }
818

819
        if (pair.second.or_name.length() > this->gs_opid_width) {
950✔
820
            this->gs_opid_width = pair.second.or_name.length();
23✔
821
        }
822
        if (full_desc.size() > max_desc_width) {
950✔
823
            max_desc_width = full_desc.size();
17✔
824
        }
825

826
        if (this->gs_lower_bound.tv_sec == 0
1,900✔
827
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
950✔
828
        {
829
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
92✔
830
        }
831
        if (this->gs_upper_bound.tv_sec == 0
1,900✔
832
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
950✔
833
        {
834
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
79✔
835
        }
836
        this->gs_time_order.emplace_back(pair.second);
950✔
837
    }
1,525✔
838
    std::stable_sort(this->gs_time_order.begin(),
26✔
839
                     this->gs_time_order.end(),
840
                     std::less<const opid_row>{});
841
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
976✔
842
        const auto& row = this->gs_time_order[lpc].get();
950✔
843
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
950✔
844
            bm_errs.insert_once(vis_line_t(lpc));
22✔
845
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
928✔
846
            bm_warns.insert_once(vis_line_t(lpc));
×
847
        }
848
    }
849

850
    this->gs_opid_width = std::min(this->gs_opid_width, MAX_OPID_WIDTH);
26✔
851
    this->gs_total_width
852
        = std::max<size_t>(22 + this->gs_opid_width + max_desc_width,
52✔
853
                           1 + 16 + 5 + 8 + 5 + 16 + 1 /* header */);
26✔
854

855
    this->tss_view->set_needs_update();
26✔
856
    this->ts_rebuild_in_progress = false;
26✔
857

858
    ensure(this->gs_time_order.empty() || this->gs_opid_width > 0);
26✔
859

860
    return true;
26✔
861
}
26✔
862

863
std::optional<vis_line_t>
864
timeline_source::row_for_time(timeval time_bucket)
3✔
865
{
866
    auto iter = this->gs_time_order.begin();
3✔
867
    while (true) {
868
        if (iter == this->gs_time_order.end()) {
71✔
869
            return std::nullopt;
3✔
870
        }
871

872
        if (iter->get().or_value.otr_range.contains_inclusive(time_bucket)) {
68✔
873
            break;
×
874
        }
875
        ++iter;
68✔
876
    }
877

878
    auto closest_iter = iter;
×
879
    auto closest_diff = time_bucket - iter->get().or_value.otr_range.tr_begin;
×
880
    for (; iter != this->gs_time_order.end(); ++iter) {
×
881
        if (time_bucket < iter->get().or_value.otr_range.tr_begin) {
×
882
            break;
×
883
        }
884
        if (!iter->get().or_value.otr_range.contains_inclusive(time_bucket)) {
×
885
            continue;
×
886
        }
887

888
        auto diff = time_bucket - iter->get().or_value.otr_range.tr_begin;
×
889
        if (diff < closest_diff) {
×
890
            closest_iter = iter;
×
891
            closest_diff = diff;
×
892
        }
893

894
        for (const auto& sub : iter->get().or_value.otr_sub_ops) {
×
895
            if (!sub.ostr_range.contains_inclusive(time_bucket)) {
×
896
                continue;
×
897
            }
898

899
            diff = time_bucket - sub.ostr_range.tr_begin;
×
900
            if (diff < closest_diff) {
×
901
                closest_iter = iter;
×
902
                closest_diff = diff;
×
903
            }
904
        }
905
    }
906

907
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
×
908
}
909

910
std::optional<vis_line_t>
911
timeline_source::row_for(const row_info& ri)
12✔
912
{
913
    auto vl_opt = this->gs_lss.row_for(ri);
12✔
914
    if (!vl_opt) {
12✔
915
        return this->row_for_time(ri.ri_time);
×
916
    }
917

918
    auto vl = vl_opt.value();
12✔
919
    auto win = this->gs_lss.window_at(vl);
12✔
920
    for (const auto& msg_line : *win) {
18✔
921
        const auto& lvv = msg_line.get_values();
12✔
922

923
        if (lvv.lvv_opid_value) {
12✔
924
            auto opid_iter
925
                = this->gs_active_opids.find(lvv.lvv_opid_value.value());
11✔
926
            if (opid_iter != this->gs_active_opids.end()) {
11✔
927
                for (const auto& [index, oprow] :
88✔
928
                     lnav::itertools::enumerate(this->gs_time_order))
90✔
929
                {
930
                    if (&oprow.get() == &opid_iter->second) {
77✔
931
                        return vis_line_t(index);
9✔
932
                    }
933
                }
934
            }
935
        }
936
    }
21✔
937

938
    return this->row_for_time(ri.ri_time);
3✔
939
}
12✔
940

941
std::optional<text_time_translator::row_info>
942
timeline_source::time_for_row(vis_line_t row)
29✔
943
{
944
    if (row >= this->gs_time_order.size()) {
29✔
945
        return std::nullopt;
×
946
    }
947

948
    const auto& otr = this->gs_time_order[row].get().or_value;
29✔
949

950
    if (this->tss_view->get_selection() == row) {
29✔
951
        auto ov_sel = this->tss_view->get_overlay_selection();
29✔
952

953
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
29✔
954
            return row_info{
×
955
                otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin,
×
956
                row,
957
            };
958
        }
959
    }
960

961
    auto preview_selection = this->gs_preview_view.get_selection();
29✔
962
    if (!preview_selection) {
29✔
963
        return std::nullopt;
×
964
    }
965
    if (preview_selection < this->gs_preview_rows.size()) {
29✔
966
        return this->gs_preview_rows[preview_selection.value()];
29✔
967
    }
968

969
    return row_info{
×
970
        otr.otr_range.tr_begin,
971
        row,
972
    };
973
}
974

975
size_t
976
timeline_source::text_line_width(textview_curses& curses)
5,432✔
977
{
978
    return this->gs_total_width;
5,432✔
979
}
980

981
void
982
timeline_source::text_selection_changed(textview_curses& tc)
43✔
983
{
984
    static const size_t MAX_PREVIEW_LINES = 200;
985

986
    auto sel = tc.get_selection();
43✔
987

988
    this->gs_preview_source.clear();
43✔
989
    this->gs_preview_rows.clear();
43✔
990
    if (!sel || sel.value() >= this->gs_time_order.size()) {
43✔
991
        return;
14✔
992
    }
993

994
    const auto& row = this->gs_time_order[sel.value()].get();
29✔
995
    auto low_tv = row.or_value.otr_range.tr_begin;
29✔
996
    auto high_tv = row.or_value.otr_range.tr_end;
29✔
997
    auto id_sf = row.or_name;
29✔
998
    auto level_stats = row.or_value.otr_level_stats;
29✔
999
    auto ov_sel = tc.get_overlay_selection();
29✔
1000
    if (ov_sel) {
29✔
1001
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1002
        id_sf = sub.ostr_subid;
×
1003
        low_tv = sub.ostr_range.tr_begin;
×
1004
        high_tv = sub.ostr_range.tr_end;
×
1005
        level_stats = sub.ostr_level_stats;
×
1006
    }
1007
    high_tv.tv_sec += 1;
29✔
1008
    auto low_vl = this->gs_lss.row_for_time(low_tv);
29✔
1009
    auto high_vl = this->gs_lss.row_for_time(high_tv).value_or(
58✔
1010
        vis_line_t(this->gs_lss.text_line_count()));
29✔
1011

1012
    if (!low_vl) {
29✔
1013
        return;
×
1014
    }
1015

1016
    auto preview_content = attr_line_t();
29✔
1017
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
29✔
1018
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
29✔
1019
    auto id_hash = row.or_name.hash();
29✔
1020
    auto msg_count = 0;
29✔
1021
    for (const auto& msg_line : *win) {
1,115✔
1022
        if (!msg_line.get_logline().match_opid_hash(id_hash)) {
543✔
1023
            continue;
463✔
1024
        }
1025

1026
        const auto& lvv = msg_line.get_values();
80✔
1027
        if (!lvv.lvv_opid_value) {
80✔
1028
            continue;
×
1029
        }
1030
        auto opid_sf = lvv.lvv_opid_value.value();
80✔
1031

1032
        if (opid_sf == row.or_name) {
80✔
1033
            for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
162✔
1034
                auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
82✔
1035
                auto cl = this->gs_lss.at(vl);
82✔
1036
                auto row_al = attr_line_t();
82✔
1037
                this->gs_log_view.textview_value_for_row(vl, row_al);
82✔
1038
                preview_content.append(row_al).append("\n");
82✔
1039
                this->gs_preview_rows.emplace_back(
82✔
1040
                    msg_line.get_logline().get_timeval(), cl);
82✔
1041
                ++cl;
82✔
1042
            }
82✔
1043
            msg_count += 1;
80✔
1044
            msgs_remaining -= 1;
80✔
1045
            if (msgs_remaining == 0) {
80✔
1046
                break;
×
1047
            }
1048
        }
1049
    }
109✔
1050

1051
    this->gs_preview_source.replace_with(preview_content);
29✔
1052
    this->gs_preview_view.set_selection(0_vl);
29✔
1053
    this->gs_preview_status_source.get_description().set_value(
29✔
1054
        " ID %.*s", id_sf.length(), id_sf.data());
1055
    auto err_count = level_stats.lls_error_count;
29✔
1056
    if (err_count == 0) {
29✔
1057
        this->gs_preview_status_source
16✔
1058
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
16✔
1059
            .set_value("");
16✔
1060
    } else if (err_count > 1) {
13✔
1061
        this->gs_preview_status_source
×
1062
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
1063
            .set_value("%'d errors", err_count);
×
1064
    } else {
1065
        this->gs_preview_status_source
13✔
1066
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
13✔
1067
            .set_value("%'d error", err_count);
13✔
1068
    }
1069
    if (msg_count < level_stats.lls_total_count) {
29✔
1070
        this->gs_preview_status_source
23✔
1071
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
23✔
1072
            .set_value(
23✔
1073
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1074
    } else {
1075
        this->gs_preview_status_source
6✔
1076
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
6✔
1077
            .set_value("%'d messages ", level_stats.lls_total_count);
6✔
1078
    }
1079
    this->gs_preview_status_view.set_needs_update();
29✔
1080
}
29✔
1081

1082
void
1083
timeline_source::text_filters_changed()
12✔
1084
{
1085
    this->rebuild_indexes();
12✔
1086
    this->tss_view->reload_data();
12✔
1087
    this->tss_view->redo_search();
12✔
1088
}
12✔
1089

1090
int
1091
timeline_source::get_filtered_count() const
47✔
1092
{
1093
    return this->gs_filtered_count;
47✔
1094
}
1095

1096
int
1097
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1098
{
1099
    return this->gs_filter_hits[filter_index];
×
1100
}
1101

1102
static const std::vector<breadcrumb::possibility>&
1103
timestamp_poss()
×
1104
{
1105
    const static std::vector<breadcrumb::possibility> retval = {
1106
        breadcrumb::possibility{"-1 day"},
1107
        breadcrumb::possibility{"-1h"},
1108
        breadcrumb::possibility{"-30m"},
1109
        breadcrumb::possibility{"-15m"},
1110
        breadcrumb::possibility{"-5m"},
1111
        breadcrumb::possibility{"-1m"},
1112
        breadcrumb::possibility{"+1m"},
1113
        breadcrumb::possibility{"+5m"},
1114
        breadcrumb::possibility{"+15m"},
1115
        breadcrumb::possibility{"+30m"},
1116
        breadcrumb::possibility{"+1h"},
1117
        breadcrumb::possibility{"+1 day"},
1118
    };
1119

1120
    return retval;
×
1121
}
1122

1123
void
1124
timeline_source::text_crumbs_for_line(int line,
×
1125
                                      std::vector<breadcrumb::crumb>& crumbs)
1126
{
1127
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1128

1129
    if (line >= this->gs_time_order.size()) {
×
1130
        return;
×
1131
    }
1132

1133
    const auto& row = this->gs_time_order[line].get();
×
1134
    char ts[64];
1135

1136
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1137

1138
    crumbs.emplace_back(std::string(ts),
×
1139
                        timestamp_poss,
1140
                        [ec = this->gs_exec_context](const auto& ts) {
×
1141
                            auto cmd
×
1142
                                = fmt::format(FMT_STRING(":goto {}"),
×
1143
                                              ts.template get<std::string>());
1144
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1145
                        });
×
1146
    crumbs.back().c_expected_input
×
1147
        = breadcrumb::crumb::expected_input_t::anything;
×
1148
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1149
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc