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

tstack / lnav / 18684526628-2593

21 Oct 2025 12:52PM UTC coverage: 69.895% (-0.02%) from 69.914%
18684526628-2593

push

github

tstack
[log-view] fix header when horiz-scrolling

25 of 34 new or added lines in 3 files covered. (73.53%)

423 existing lines in 3 files now uncovered.

50767 of 72633 relevant lines covered (69.9%)

422081.35 hits per line

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

65.72
/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
#include <utility>
32
#include <vector>
33

34
#include "timeline_source.hh"
35

36
#include <time.h>
37

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

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

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

61
static constexpr size_t MAX_OPID_WIDTH = 80;
62
static constexpr size_t MAX_DESC_WIDTH = 256;
63

64
size_t
65
abbrev_ftime(char* datebuf, size_t db_size, const tm& lb_tm, const tm& dt)
16✔
66
{
67
    char lb_fmt[32] = " ";
16✔
68
    bool same = true;
16✔
69

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

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

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

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

128
    const auto& sti = tc->tc_selected_text.value();
×
129

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

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

142
    this->los_menu_items.clear();
×
143

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

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

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

169
    return retval;
×
170
}
×
171

172
timeline_header_overlay::timeline_header_overlay(
604✔
173
    const std::shared_ptr<timeline_source>& src)
604✔
174
    : gho_src(src)
604✔
175
{
176
}
604✔
177

178
bool
179
timeline_header_overlay::list_static_overlay(const listview_curses& lv,
35✔
180
                                             media_t media,
181
                                             int y,
182
                                             int bottom,
183
                                             attr_line_t& value_out)
184
{
185
    if (y >= 3) {
35✔
186
        return false;
8✔
187
    }
188

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

195
        return false;
×
196
    }
197

198
    auto lb = this->gho_src->gs_lower_bound;
27✔
199
    tm lb_tm;
200
    auto ub = this->gho_src->gs_upper_bound;
27✔
201
    tm ub_tm;
202
    auto bounds
203
        = this->gho_src->get_time_bounds_for(lv.get_selection().value_or(0_vl));
27✔
204

205
    if (bounds.first < lb) {
27✔
206
        lb = bounds.first;
27✔
207
    }
208
    if (ub < bounds.second) {
27✔
209
        ub = bounds.second;
11✔
210
    }
211
    auto whole_span = to_us(ub - lb);
27✔
212
    auto use_small_span = whole_span < 5min;
27✔
213

214
    secs2tm(lb.tv_sec, &lb_tm);
27✔
215
    secs2tm(ub.tv_sec, &ub_tm);
27✔
216

217
    tm sel_lb_tm;
218
    secs2tm(bounds.first.tv_sec, &sel_lb_tm);
27✔
219
    tm sel_ub_tm;
220
    secs2tm(bounds.second.tv_sec, &sel_ub_tm);
27✔
221

222
    auto width = lv.get_dimensions().second - 1;
27✔
223

224
    char datebuf[64];
225

226
    if (y == 0) {
27✔
227
        double span = ub.tv_sec - lb.tv_sec;
11✔
228
        double per_ch = span / (double) width;
11✔
229
        strftime(datebuf, sizeof(datebuf), " %Y-%m-%dT%H:%M", &lb_tm);
11✔
230
        value_out.append(datebuf);
11✔
231

232
        auto duration_str = humanize::time::duration::from_tv(ub - lb)
11✔
233
                                .with_resolution(1min)
11✔
234
                                .to_string();
11✔
235
        auto duration_pos = width / 2 - duration_str.size() / 2;
11✔
236
        value_out.pad_to(duration_pos).append(duration_str);
11✔
237

238
        auto upper_size
239
            = strftime(datebuf, sizeof(datebuf), "%Y-%m-%dT%H:%M ", &ub_tm);
11✔
240
        auto upper_pos = width - upper_size;
11✔
241

242
        value_out.pad_to(upper_pos).append(datebuf);
11✔
243

244
        auto lr = line_range{};
11✔
245
        if (lb.tv_sec < bounds.first.tv_sec) {
11✔
246
            auto start_diff = bounds.first.tv_sec - lb.tv_sec;
×
247
            lr.lr_start = start_diff / per_ch;
×
248
        } else {
249
            lr.lr_start = 0;
11✔
250
        }
251
        if (lb.tv_sec < bounds.second.tv_sec) {
11✔
252
            auto start_diff = bounds.second.tv_sec - lb.tv_sec;
8✔
253
            lr.lr_end = start_diff / per_ch;
8✔
254
        } else {
255
            lr.lr_end = 1;
3✔
256
        }
257
        if (lr.lr_start == lr.lr_end) {
11✔
258
            lr.lr_end += 1;
4✔
259
        }
260

261
        value_out.get_attrs().emplace_back(
22✔
262
            lr, VC_ROLE.value(role_t::VCR_CURSOR_LINE));
22✔
263
        value_out.with_attr_for_all(VC_ROLE.value(role_t::VCR_STATUS_INFO));
11✔
264
    } else if (y == 1) {
27✔
265
        abbrev_ftime(datebuf, sizeof(datebuf), lb_tm, sel_lb_tm);
8✔
266
        value_out.appendf(FMT_STRING(" {}"), datebuf);
32✔
267

268
        auto bound_dur
269
            = humanize::time::duration::from_tv(bounds.second - bounds.first);
8✔
270
        if (!use_small_span) {
8✔
271
            bound_dur.with_resolution(1min);
×
272
        }
273
        auto duration_str = bound_dur.to_string();
8✔
274
        auto duration_pos = width / 2 - duration_str.size() / 2;
8✔
275
        value_out.pad_to(duration_pos).append(duration_str);
8✔
276

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

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

309
    if (lv.get_selection() != line) {
×
310
        return;
×
311
    }
312

313
    if (line >= this->gho_src->gs_time_order.size()) {
×
314
        return;
×
315
    }
316

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

319
    if (row.or_value.otr_sub_ops.size() <= 1) {
×
320
        return;
×
321
    }
322

323
    auto width = lv.get_dimensions().second;
×
324

325
    if (width < 37) {
×
326
        return;
×
327
    }
328

329
    width -= 37;
×
330
    double span = row.or_value.otr_range.duration().count();
×
331
    double per_ch = span / (double) width;
×
332

333
    for (const auto& sub : row.or_value.otr_sub_ops) {
×
334
        value_out.resize(value_out.size() + 1);
×
335

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

359
        auto start_diff = (double) to_mstime(sub.ostr_range.tr_begin
×
360
                                             - row.or_value.otr_range.tr_begin);
×
361
        auto end_diff = (double) to_mstime(sub.ostr_range.tr_end
×
362
                                           - row.or_value.otr_range.tr_begin);
×
363

364
        auto lr = line_range{
365
            (int) (32 + (start_diff / per_ch)),
×
366
            (int) (32 + (end_diff / per_ch)),
×
367
            line_range::unit::codepoint,
368
        };
369

370
        if (lr.lr_start == lr.lr_end) {
×
371
            lr.lr_end += 1;
×
372
        }
373

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

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

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

436
                if (height > 5) {
×
437
                    this->gs_preview_view.set_height(height / 2_vl);
×
438
                }
439
            } else {
440
                this->gs_preview_view.set_height(5_vl);
×
441
            }
442
            return true;
×
443
        }
444
    }
445
    if (this->gs_preview_focused) {
×
446
        return this->gs_preview_view.handle_key(ch);
×
447
    }
448

449
    return false;
×
450
}
451

452
bool
453
timeline_source::text_handle_mouse(
×
454
    textview_curses& tc,
455
    const listview_curses::display_line_content_t&,
456
    mouse_event& me)
457
{
458
    auto nci = ncinput{};
×
459
    if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{0, -1})) {
×
460
        nci.id = '\r';
×
461
        nci.eff_text[0] = '\r';
×
462
        this->list_input_handle_key(tc, nci);
×
463
    }
464

465
    return false;
×
466
}
467

468
std::pair<timeval, timeval>
469
timeline_source::get_time_bounds_for(int line)
385✔
470
{
471
    const auto low_index = this->tss_view->get_top();
385✔
472
    auto high_index
473
        = std::min(this->tss_view->get_bottom(),
385✔
474
                   vis_line_t((int) this->gs_time_order.size() - 1));
385✔
475
    const auto& low_row = this->gs_time_order[low_index].get();
385✔
476
    const auto& high_row = this->gs_time_order[high_index].get();
385✔
477
    auto low_tv = low_row.or_value.otr_range.tr_begin;
385✔
478
    auto high_tv = high_row.or_value.otr_range.tr_begin;
385✔
479

480
    for (auto index = low_index; index <= high_index; index += 1_vl) {
770✔
481
        const auto& row = this->gs_time_order[index].get();
385✔
482

483
        if (high_tv < row.or_value.otr_range.tr_end) {
385✔
484
            high_tv = row.or_value.otr_range.tr_end;
381✔
485
        }
486
    }
487
    auto duration = to_us(high_tv - low_tv);
385✔
488
    auto span_iter
489
        = std::upper_bound(TIME_SPANS.begin(), TIME_SPANS.end(), duration);
385✔
490
    if (span_iter == TIME_SPANS.end()) {
385✔
491
        --span_iter;
×
492
    }
493
    auto span_portion = *span_iter / 8;
385✔
494
    auto lower_dur = to_us(low_tv);
385✔
495
    lower_dur = rounddown(lower_dur, span_portion);
385✔
496
    auto upper_dur = to_us(high_tv);
385✔
497
    upper_dur = roundup(upper_dur, span_portion);
385✔
498
    auto lower_tv = to_timeval(lower_dur);
385✔
499
    auto upper_tv = to_timeval(upper_dur);
385✔
500

501
    return {lower_tv, upper_tv};
770✔
502
}
503

504
size_t
505
timeline_source::text_line_count()
7,285✔
506
{
507
    return this->gs_time_order.size();
7,285✔
508
}
509

510
line_info
511
timeline_source::text_value_for_line(textview_curses& tc,
358✔
512
                                     int line,
513
                                     std::string& value_out,
514
                                     text_sub_source::line_flags_t flags)
515
{
516
    if (line < (ssize_t) this->gs_time_order.size()) {
358✔
517
        const auto& row = this->gs_time_order[line].get();
358✔
518
        auto duration
519
            = row.or_value.otr_range.tr_end - row.or_value.otr_range.tr_begin;
358✔
520
        auto duration_str = fmt::format(
521
            FMT_STRING(" {: >13}"),
1,074✔
522
            humanize::time::duration::from_tv(duration).to_string());
716✔
523

524
        this->gs_rendered_line.clear();
358✔
525

526
        auto total_msgs = row.or_value.otr_level_stats.lls_total_count;
358✔
527
        auto truncated_name
528
            = attr_line_t::from_table_cell_content(row.or_name, MAX_OPID_WIDTH);
358✔
529
        auto truncated_desc = attr_line_t::from_table_cell_content(
530
            row.or_description, MAX_DESC_WIDTH);
358✔
531
        this->gs_rendered_line
532
            .append(duration_str, VC_ROLE.value(role_t::VCR_OFFSET_TIME))
716✔
533
            .append("  ")
358✔
534
            .append(lnav::roles::error(humanize::sparkline(
1,074✔
535
                row.or_value.otr_level_stats.lls_error_count, total_msgs)))
358✔
536
            .append(lnav::roles::warning(humanize::sparkline(
1,074✔
537
                row.or_value.otr_level_stats.lls_warning_count, total_msgs)))
358✔
538
            .append("  ")
358✔
539
            .append(lnav::roles::identifier(truncated_name))
716✔
540
            .append(
716✔
541
                this->gs_opid_width - truncated_name.utf8_length_or_length(),
358✔
542
                ' ')
543
            .append(truncated_desc);
358✔
544
        this->gs_rendered_line.with_attr_for_all(
358✔
545
            VC_ROLE.value(role_t::VCR_COMMENT));
716✔
546

547
        value_out = this->gs_rendered_line.get_string();
358✔
548
    }
358✔
549

550
    return {};
358✔
551
}
552

553
void
554
timeline_source::text_attrs_for_line(textview_curses& tc,
358✔
555
                                     int line,
556
                                     string_attrs_t& value_out)
557
{
558
    if (line < (ssize_t) this->gs_time_order.size()) {
358✔
559
        const auto& row = this->gs_time_order[line].get();
358✔
560

561
        value_out = this->gs_rendered_line.get_attrs();
358✔
562

563
        auto lr = line_range{-1, -1, line_range::unit::codepoint};
358✔
564
        auto sel_bounds
565
            = this->get_time_bounds_for(tc.get_selection().value_or(0_vl));
358✔
566

567
        if (row.or_value.otr_range.tr_begin <= sel_bounds.second
358✔
568
            && sel_bounds.first <= row.or_value.otr_range.tr_end)
358✔
569
        {
570
            static const int INDENT = 22;
571

572
            auto width = tc.get_dimensions().second;
65✔
573

574
            if (width > INDENT) {
65✔
575
                width -= INDENT;
65✔
576
                double span
577
                    = (to_us(sel_bounds.second) - to_us(sel_bounds.first))
65✔
578
                          .count();
65✔
579
                double per_ch = span / (double) width;
65✔
580

581
                if (row.or_value.otr_range.tr_begin <= sel_bounds.first) {
65✔
582
                    lr.lr_start = INDENT;
×
583
                } else {
584
                    double start_diff = (to_us(row.or_value.otr_range.tr_begin)
65✔
585
                                         - to_us(sel_bounds.first))
130✔
586
                                            .count();
65✔
587

588
                    lr.lr_start = INDENT + floor(start_diff / per_ch);
65✔
589
                }
590

591
                if (sel_bounds.second < row.or_value.otr_range.tr_end) {
65✔
592
                    lr.lr_end = -1;
4✔
593
                } else {
594
                    double end_diff = (to_us(row.or_value.otr_range.tr_end)
61✔
595
                                       - to_us(sel_bounds.first))
122✔
596
                                          .count();
61✔
597

598
                    lr.lr_end = INDENT + ceil(end_diff / per_ch);
61✔
599
                    if (lr.lr_start == lr.lr_end) {
61✔
600
                        lr.lr_end += 1;
×
601
                    }
602
                }
603

604
                auto block_attrs = text_attrs::with_reverse();
65✔
605
                value_out.emplace_back(lr, VC_STYLE.value(block_attrs));
65✔
606
            }
607
        }
608
        auto alt_row_index = line % 4;
358✔
609
        if (alt_row_index == 2 || alt_row_index == 3) {
358✔
610
            value_out.emplace_back(line_range{0, -1},
174✔
611
                                   VC_ROLE.value(role_t::VCR_ALT_ROW));
348✔
612
        }
613
    }
614
}
358✔
615

616
size_t
617
timeline_source::text_size_for_line(textview_curses& tc,
×
618
                                    int line,
619
                                    text_sub_source::line_flags_t raw)
620
{
621
    return this->gs_total_width;
×
622
}
623

624
bool
625
timeline_source::rebuild_indexes()
25✔
626
{
627
    static auto op = lnav_operation{"timeline_rebuild"};
25✔
628

629
    auto op_guard = lnav_opid_guard::internal(op);
25✔
630
    auto& bm = this->tss_view->get_bookmarks();
25✔
631
    auto& bm_errs = bm[&textview_curses::BM_ERRORS];
25✔
632
    auto& bm_warns = bm[&textview_curses::BM_WARNINGS];
25✔
633

634
    bm_errs.clear();
25✔
635
    bm_warns.clear();
25✔
636

637
    this->gs_lower_bound = {};
25✔
638
    this->gs_upper_bound = {};
25✔
639
    this->gs_opid_width = 0;
25✔
640
    this->gs_total_width = 0;
25✔
641
    this->gs_filtered_count = 0;
25✔
642
    this->gs_active_opids.clear();
25✔
643
    this->gs_descriptions.clear();
25✔
644
    this->gs_subid_map.clear();
25✔
645
    this->gs_allocator.reset();
25✔
646
    this->gs_preview_source.clear();
25✔
647
    this->gs_preview_rows.clear();
25✔
648
    this->gs_preview_status_source.get_description().clear();
25✔
649

650
    auto min_log_time_opt = this->get_min_row_time();
25✔
651
    auto max_log_time_opt = this->get_max_row_time();
25✔
652
    auto max_desc_width = size_t{0};
25✔
653

654
    log_info("building opid table");
25✔
655
    for (const auto& [index, ld] : lnav::itertools::enumerate(this->gs_lss)) {
51✔
656
        if (ld->get_file_ptr() == nullptr) {
26✔
657
            continue;
1✔
658
        }
659
        if (!ld->is_visible()) {
26✔
660
            continue;
1✔
661
        }
662

663
        ld->get_file_ptr()->enable_cache();
25✔
664
        auto format = ld->get_file_ptr()->get_format();
25✔
665
        safe::ReadAccess<logfile::safe_opid_state> r_opid_map(
666
            ld->get_file_ptr()->get_opids());
25✔
667
        for (const auto& pair : r_opid_map->los_opid_ranges) {
1,086✔
668
            auto& otr = pair.second;
1,061✔
669
            auto active_iter = this->gs_active_opids.find(pair.first);
1,061✔
670
            if (active_iter == this->gs_active_opids.end()) {
1,061✔
671
                auto opid = pair.first.to_owned(this->gs_allocator);
1,061✔
672
                auto active_emp_res = this->gs_active_opids.emplace(
2,122✔
673
                    opid,
674
                    opid_row{
1,061✔
675
                        opid,
676
                        otr,
677
                        string_fragment::invalid(),
678
                    });
679
                active_iter = active_emp_res.first;
1,061✔
680
            } else {
681
                active_iter->second.or_value |= otr;
×
682
            }
683

684
            auto& row = active_iter->second;
1,061✔
685
            for (auto& sub : active_iter->second.or_value.otr_sub_ops) {
1,061✔
686
                auto subid_iter = this->gs_subid_map.find(sub.ostr_subid);
×
687

688
                if (subid_iter == this->gs_subid_map.end()) {
×
689
                    subid_iter = this->gs_subid_map
×
690
                                     .emplace(sub.ostr_subid.to_owned(
×
691
                                                  this->gs_allocator),
×
692
                                              true)
×
693
                                     .first;
694
                }
695
                sub.ostr_subid = subid_iter->first;
×
696
                if (sub.ostr_subid.length()
×
697
                    > active_iter->second.or_max_subid_width)
×
698
                {
699
                    active_iter->second.or_max_subid_width
×
700
                        = sub.ostr_subid.length();
×
701
                }
702
            }
703

704
            if (otr.otr_description.lod_id) {
1,061✔
705
                auto desc_id = otr.otr_description.lod_id.value();
1,051✔
706
                auto desc_def_iter
707
                    = format->lf_opid_description_def->find(desc_id);
1,051✔
708

709
                if (desc_def_iter == format->lf_opid_description_def->end()) {
1,051✔
710
                    log_error("cannot find description: %s",
×
711
                              active_iter->first.data());
712
                } else {
713
                    auto desc_key
714
                        = opid_description_def_key{format->get_name(), desc_id};
1,051✔
715
                    auto desc_defs_iter
716
                        = row.or_description_defs.odd_defs.find(desc_key);
1,051✔
717
                    if (desc_defs_iter
1,051✔
718
                        == row.or_description_defs.odd_defs.end())
1,051✔
719
                    {
720
                        row.or_description_defs.odd_defs.insert(
2,102✔
721
                            desc_key, desc_def_iter->second);
1,051✔
722
                    }
723

724
                    auto& all_descs = active_iter->second.or_descriptions;
1,051✔
725
                    auto& curr_desc_m = all_descs[desc_key];
1,051✔
726
                    const auto& new_desc_v = otr.otr_description.lod_elements;
1,051✔
727

728
                    for (const auto& desc_pair : new_desc_v) {
2,102✔
729
                        curr_desc_m[desc_pair.first] = desc_pair.second;
1,051✔
730
                    }
731
                }
732
            } else if (!otr.otr_description.lod_elements.empty()) {
10✔
733
                auto desc_sf = string_fragment::from_str(
2✔
734
                    otr.otr_description.lod_elements.front().second);
2✔
735
                active_iter->second.or_description
2✔
736
                    = desc_sf.to_owned(this->gs_allocator);
4✔
737
            }
738
            active_iter->second.or_value.otr_description.lod_elements.clear();
1,061✔
739
        }
740

741
        if (this->gs_index_progress) {
25✔
742
            switch (this->gs_index_progress(
×
743
                progress_t{index, this->gs_lss.file_count()}))
×
744
            {
745
                case lnav::progress_result_t::ok:
×
746
                    break;
×
747
                case lnav::progress_result_t::interrupt:
×
748
                    return false;
×
749
            }
750
        }
751
    }
25✔
752
    if (this->gs_index_progress) {
25✔
753
        this->gs_index_progress(std::nullopt);
×
754
    }
755
    log_info("active opids: %zu", this->gs_active_opids.size());
25✔
756

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

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

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

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

842
        if (pair.second.or_name.length() > this->gs_opid_width) {
946✔
843
            this->gs_opid_width = pair.second.or_name.length();
22✔
844
        }
845
        if (full_desc.size() > max_desc_width) {
946✔
846
            max_desc_width = full_desc.size();
15✔
847
        }
848

849
        if (this->gs_lower_bound.tv_sec == 0
1,892✔
850
            || pair.second.or_value.otr_range.tr_begin < this->gs_lower_bound)
946✔
851
        {
852
            this->gs_lower_bound = pair.second.or_value.otr_range.tr_begin;
91✔
853
        }
854
        if (this->gs_upper_bound.tv_sec == 0
1,892✔
855
            || this->gs_upper_bound < pair.second.or_value.otr_range.tr_end)
946✔
856
        {
857
            this->gs_upper_bound = pair.second.or_value.otr_range.tr_end;
76✔
858
        }
859
        this->gs_time_order.emplace_back(pair.second);
946✔
860
    }
1,521✔
861
    std::stable_sort(this->gs_time_order.begin(),
25✔
862
                     this->gs_time_order.end(),
863
                     std::less<const opid_row>{});
864
    for (size_t lpc = 0; lpc < this->gs_time_order.size(); lpc++) {
971✔
865
        const auto& row = this->gs_time_order[lpc].get();
946✔
866
        if (row.or_value.otr_level_stats.lls_error_count > 0) {
946✔
867
            bm_errs.insert_once(vis_line_t(lpc));
22✔
868
        } else if (row.or_value.otr_level_stats.lls_warning_count > 0) {
924✔
869
            bm_warns.insert_once(vis_line_t(lpc));
×
870
        }
871
    }
872

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

878
    this->tss_view->set_needs_update();
25✔
879

880
    return true;
25✔
881
}
25✔
882

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

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

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

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

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

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

927
    return vis_line_t(std::distance(this->gs_time_order.begin(), closest_iter));
×
928
}
929

930
std::optional<vis_line_t>
931
timeline_source::row_for(const row_info& ri)
12✔
932
{
933
    auto vl_opt = this->gs_lss.row_for(ri);
12✔
934
    if (!vl_opt) {
12✔
935
        return this->row_for_time(ri.ri_time);
×
936
    }
937

938
    auto vl = vl_opt.value();
12✔
939
    auto win = this->gs_lss.window_at(vl);
12✔
940
    for (const auto& msg_line : *win) {
18✔
941
        const auto& lvv = msg_line.get_values();
12✔
942

943
        if (lvv.lvv_opid_value) {
12✔
944
            auto opid_iter
945
                = this->gs_active_opids.find(lvv.lvv_opid_value.value());
11✔
946
            if (opid_iter != this->gs_active_opids.end()) {
11✔
947
                for (const auto& [index, oprow] :
88✔
948
                     lnav::itertools::enumerate(this->gs_time_order))
90✔
949
                {
950
                    if (&oprow.get() == &opid_iter->second) {
77✔
951
                        return vis_line_t(index);
9✔
952
                    }
953
                }
954
            }
955
        }
956
    }
21✔
957

958
    return this->row_for_time(ri.ri_time);
3✔
959
}
12✔
960

961
std::optional<text_time_translator::row_info>
962
timeline_source::time_for_row(vis_line_t row)
28✔
963
{
964
    if (row >= this->gs_time_order.size()) {
28✔
965
        return std::nullopt;
×
966
    }
967

968
    const auto& otr = this->gs_time_order[row].get().or_value;
28✔
969

970
    if (this->tss_view->get_selection() == row) {
28✔
971
        auto ov_sel = this->tss_view->get_overlay_selection();
28✔
972

973
        if (ov_sel && ov_sel.value() < otr.otr_sub_ops.size()) {
28✔
974
            return row_info{
×
975
                otr.otr_sub_ops[ov_sel.value()].ostr_range.tr_begin,
×
976
                row,
977
            };
978
        }
979
    }
980

981
    auto preview_selection = this->gs_preview_view.get_selection();
28✔
982
    if (!preview_selection) {
28✔
983
        return std::nullopt;
×
984
    }
985
    if (preview_selection < this->gs_preview_rows.size()) {
28✔
986
        return this->gs_preview_rows[preview_selection.value()];
28✔
987
    }
988

989
    return row_info{
×
990
        otr.otr_range.tr_begin,
991
        row,
992
    };
993
}
994

995
size_t
996
timeline_source::text_line_width(textview_curses& curses)
5,440✔
997
{
998
    return this->gs_total_width;
5,440✔
999
}
1000

1001
void
1002
timeline_source::text_selection_changed(textview_curses& tc)
42✔
1003
{
1004
    static const size_t MAX_PREVIEW_LINES = 200;
1005

1006
    auto sel = tc.get_selection();
42✔
1007

1008
    this->gs_preview_source.clear();
42✔
1009
    this->gs_preview_rows.clear();
42✔
1010
    if (!sel || sel.value() >= this->gs_time_order.size()) {
42✔
1011
        return;
14✔
1012
    }
1013

1014
    const auto& row = this->gs_time_order[sel.value()].get();
28✔
1015
    auto low_tv = row.or_value.otr_range.tr_begin;
28✔
1016
    auto high_tv = row.or_value.otr_range.tr_end;
28✔
1017
    auto id_sf = row.or_name;
28✔
1018
    auto level_stats = row.or_value.otr_level_stats;
28✔
1019
    auto ov_sel = tc.get_overlay_selection();
28✔
1020
    if (ov_sel) {
28✔
1021
        const auto& sub = row.or_value.otr_sub_ops[ov_sel.value()];
×
1022
        id_sf = sub.ostr_subid;
×
1023
        low_tv = sub.ostr_range.tr_begin;
×
1024
        high_tv = sub.ostr_range.tr_end;
×
1025
        level_stats = sub.ostr_level_stats;
×
1026
    }
1027
    high_tv.tv_sec += 1;
28✔
1028
    auto low_vl = this->gs_lss.row_for_time(low_tv);
28✔
1029
    auto high_vl = this->gs_lss.row_for_time(high_tv).value_or(
56✔
1030
        vis_line_t(this->gs_lss.text_line_count()));
28✔
1031

1032
    if (!low_vl) {
28✔
1033
        return;
×
1034
    }
1035

1036
    auto preview_content = attr_line_t();
28✔
1037
    auto msgs_remaining = size_t{MAX_PREVIEW_LINES};
28✔
1038
    auto win = this->gs_lss.window_at(low_vl.value(), high_vl);
28✔
1039
    auto id_hash = row.or_name.hash();
28✔
1040
    auto msg_count = 0;
28✔
1041
    for (const auto& msg_line : *win) {
1,112✔
1042
        if (!msg_line.get_logline().match_opid_hash(id_hash)) {
542✔
1043
            continue;
463✔
1044
        }
1045

1046
        const auto& lvv = msg_line.get_values();
79✔
1047
        if (!lvv.lvv_opid_value) {
79✔
1048
            continue;
×
1049
        }
1050
        auto opid_sf = lvv.lvv_opid_value.value();
79✔
1051

1052
        if (opid_sf == row.or_name) {
79✔
1053
            for (size_t lpc = 0; lpc < msg_line.get_line_count(); lpc++) {
158✔
1054
                auto vl = msg_line.get_vis_line() + vis_line_t(lpc);
79✔
1055
                auto cl = this->gs_lss.at(vl);
79✔
1056
                auto row_al = attr_line_t();
79✔
1057
                this->gs_log_view.textview_value_for_row(vl, row_al);
79✔
1058
                preview_content.append(row_al).append("\n");
79✔
1059
                this->gs_preview_rows.emplace_back(
79✔
1060
                    msg_line.get_logline().get_timeval(), cl);
79✔
1061
                ++cl;
79✔
1062
            }
79✔
1063
            msg_count += 1;
79✔
1064
            msgs_remaining -= 1;
79✔
1065
            if (msgs_remaining == 0) {
79✔
1066
                break;
×
1067
            }
1068
        }
1069
    }
107✔
1070

1071
    this->gs_preview_source.replace_with(preview_content);
28✔
1072
    this->gs_preview_view.set_selection(0_vl);
28✔
1073
    this->gs_preview_status_source.get_description().set_value(
28✔
1074
        " ID %.*s", id_sf.length(), id_sf.data());
1075
    auto err_count = level_stats.lls_error_count;
28✔
1076
    if (err_count == 0) {
28✔
1077
        this->gs_preview_status_source
15✔
1078
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
15✔
1079
            .set_value("");
15✔
1080
    } else if (err_count > 1) {
13✔
1081
        this->gs_preview_status_source
×
1082
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
×
1083
            .set_value("%'d errors", err_count);
×
1084
    } else {
1085
        this->gs_preview_status_source
13✔
1086
            .statusview_value_for_field(timeline_status_source::TSF_ERRORS)
13✔
1087
            .set_value("%'d error", err_count);
13✔
1088
    }
1089
    if (msg_count < level_stats.lls_total_count) {
28✔
1090
        this->gs_preview_status_source
23✔
1091
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
23✔
1092
            .set_value(
23✔
1093
                "%'d of %'d messages ", msg_count, level_stats.lls_total_count);
1094
    } else {
1095
        this->gs_preview_status_source
5✔
1096
            .statusview_value_for_field(timeline_status_source::TSF_TOTAL)
5✔
1097
            .set_value("%'d messages ", level_stats.lls_total_count);
5✔
1098
    }
1099
    this->gs_preview_status_view.set_needs_update();
28✔
1100
}
28✔
1101

1102
void
1103
timeline_source::text_filters_changed()
12✔
1104
{
1105
    this->rebuild_indexes();
12✔
1106
    this->tss_view->reload_data();
12✔
1107
    this->tss_view->redo_search();
12✔
1108
}
12✔
1109

1110
int
1111
timeline_source::get_filtered_count() const
45✔
1112
{
1113
    return this->gs_filtered_count;
45✔
1114
}
1115

1116
int
1117
timeline_source::get_filtered_count_for(size_t filter_index) const
×
1118
{
1119
    return this->gs_filter_hits[filter_index];
×
1120
}
1121

1122
static const std::vector<breadcrumb::possibility>&
1123
timestamp_poss()
×
1124
{
1125
    const static std::vector<breadcrumb::possibility> retval = {
1126
        breadcrumb::possibility{"-1 day"},
1127
        breadcrumb::possibility{"-1h"},
1128
        breadcrumb::possibility{"-30m"},
1129
        breadcrumb::possibility{"-15m"},
1130
        breadcrumb::possibility{"-5m"},
1131
        breadcrumb::possibility{"-1m"},
1132
        breadcrumb::possibility{"+1m"},
1133
        breadcrumb::possibility{"+5m"},
1134
        breadcrumb::possibility{"+15m"},
1135
        breadcrumb::possibility{"+30m"},
1136
        breadcrumb::possibility{"+1h"},
1137
        breadcrumb::possibility{"+1 day"},
1138
    };
1139

1140
    return retval;
×
1141
}
1142

1143
void
1144
timeline_source::text_crumbs_for_line(int line,
×
1145
                                      std::vector<breadcrumb::crumb>& crumbs)
1146
{
1147
    text_sub_source::text_crumbs_for_line(line, crumbs);
×
1148

1149
    if (line >= this->gs_time_order.size()) {
×
1150
        return;
×
1151
    }
1152

1153
    const auto& row = this->gs_time_order[line].get();
×
1154
    char ts[64];
1155

1156
    sql_strftime(ts, sizeof(ts), row.or_value.otr_range.tr_begin, 'T');
×
1157

1158
    crumbs.emplace_back(std::string(ts),
×
1159
                        timestamp_poss,
1160
                        [ec = this->gs_exec_context](const auto& ts) {
×
1161
                            auto cmd
×
1162
                                = fmt::format(FMT_STRING(":goto {}"),
×
1163
                                              ts.template get<std::string>());
1164
                            ec->execute(INTERNAL_SRC_LOC, cmd);
×
1165
                        });
×
1166
    crumbs.back().c_expected_input
×
1167
        = breadcrumb::crumb::expected_input_t::anything;
×
1168
    crumbs.back().c_search_placeholder = "(Enter an absolute or relative time)";
×
1169
}
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