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

tstack / lnav / 18954735303-2618

30 Oct 2025 08:49PM UTC coverage: 68.778% (+0.7%) from 68.076%
18954735303-2618

push

github

tstack
[filters] add min/max time filters to UI

Related to #1275
Related to #1576

175 of 525 new or added lines in 12 files covered. (33.33%)

582 existing lines in 3 files now uncovered.

50384 of 73256 relevant lines covered (68.78%)

426743.35 hits per line

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

75.0
/src/cmds.filtering.cc
1
/**
2
 * Copyright (c) 2025, 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 "lnav.hh"
31
#include "lnav_commands.hh"
32
#include "sql_util.hh"
33

34
static Result<std::string, lnav::console::user_message>
35
com_hide_line(exec_context& ec,
19✔
36
              std::string cmdline,
37
              std::vector<std::string>& args)
38
{
39
    auto* tc = *lnav_data.ld_view_stack.top();
19✔
40
    auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source());
19✔
41
    std::string retval;
19✔
42

43
    if (ttt == nullptr) {
19✔
44
        return ec.make_error("this view does not support time filtering");
×
45
    }
46

47
    if (args.size() == 1) {
19✔
48
        auto min_time_opt = ttt->get_min_row_time();
3✔
49
        auto max_time_opt = ttt->get_max_row_time();
3✔
50
        char min_time_str[32], max_time_str[32];
51

52
        if (min_time_opt) {
3✔
53
            sql_strftime(
2✔
54
                min_time_str, sizeof(min_time_str), min_time_opt.value());
2✔
55
        }
56
        if (max_time_opt) {
3✔
57
            sql_strftime(
2✔
58
                max_time_str, sizeof(max_time_str), max_time_opt.value());
2✔
59
        }
60
        if (min_time_opt && max_time_opt) {
3✔
61
            retval = fmt::format(FMT_STRING("info: hiding lines before {} and "
4✔
62
                                            "after {}"),
63
                                 min_time_str,
64
                                 max_time_str);
1✔
65
        } else if (min_time_opt) {
2✔
66
            retval = fmt::format(FMT_STRING("info: hiding lines before {}"),
4✔
67
                                 min_time_str);
1✔
68
        } else if (max_time_opt) {
1✔
69
            retval = fmt::format(FMT_STRING("info: hiding lines after {}"),
4✔
70
                                 max_time_str);
1✔
71
        } else {
72
            retval
73
                = "info: no lines hidden by time, pass an "
74
                  "absolute or "
75
                  "relative time";
×
76
        }
77
    } else if (args.size() >= 2) {
16✔
78
        std::string all_args = remaining_args(cmdline, args);
16✔
79
        date_time_scanner dts;
16✔
80
        timeval tv_abs;
81
        std::optional<timeval> tv_opt;
16✔
82
        auto parse_res = relative_time::from_str(all_args);
16✔
83

84
        if (parse_res.isOk()) {
16✔
85
            if (tc->get_inner_height() > 0) {
2✔
86
                exttm tm;
2✔
87

88
                auto vl = tc->get_selection();
2✔
89
                if (vl) {
2✔
90
                    auto log_vl_ri = ttt->time_for_row(vl.value());
2✔
91
                    if (log_vl_ri) {
2✔
92
                        tm = exttm::from_tv(log_vl_ri.value().ri_time);
2✔
93
                        tv_opt = parse_res.unwrap().adjust(tm).to_timeval();
2✔
94
                    }
95
                }
96
            }
97
        } else if (dts.convert_to_timeval(all_args, tv_abs)) {
14✔
98
            tv_opt = tv_abs;
13✔
99
        } else {
100
            auto pe = parse_res.unwrapErr();
1✔
101
            if (!pe.pe_msg.empty()) {
1✔
102
                auto msg
103
                    = lnav::console::user_message::error(
×
104
                          attr_line_t("invalid time value: ").append(all_args))
2✔
105
                          .with_reason(pe.pe_msg);
1✔
106
                return Err(msg);
1✔
107
            }
1✔
108

109
            return ec.make_error(FMT_STRING("invalid time value: {}"),
×
110
                                 all_args);
×
111
        }
1✔
112

113
        if (tv_opt) {
15✔
114
            if (ec.ec_dry_run) {
15✔
NEW
115
                if (args[0] == "hide-lines-before") {
×
NEW
116
                    ttt->ttt_preview_min_time = tv_opt.value();
×
117
                } else {
NEW
118
                    ttt->ttt_preview_max_time = tv_opt.value();
×
119
                }
120
            } else {
121
                char time_text[256];
122
                std::string relation;
15✔
123

124
                sql_strftime(time_text, sizeof(time_text), tv_opt.value());
15✔
125
                if (args[0] == "hide-lines-before") {
15✔
126
                    ttt->set_min_row_time(tv_opt.value());
8✔
127
                    relation = "before";
8✔
128
                } else {
129
                    ttt->set_max_row_time(tv_opt.value());
7✔
130
                    relation = "after";
7✔
131
                }
132

133
                tc->get_sub_source()->text_filters_changed();
15✔
134
                tc->reload_data();
15✔
135

136
                retval = fmt::format(FMT_STRING("info: hiding lines {} {}"),
60✔
137
                                     relation,
138
                                     time_text);
15✔
139
            }
15✔
140
        }
141
    }
17✔
142

143
    return Ok(retval);
18✔
144
}
19✔
145

146
static Result<std::string, lnav::console::user_message>
147
com_show_lines(exec_context& ec,
1✔
148
               std::string cmdline,
149
               std::vector<std::string>& args)
150
{
151
    auto* tc = *lnav_data.ld_view_stack.top();
1✔
152
    auto* ttt = dynamic_cast<text_time_translator*>(tc->get_sub_source());
1✔
153
    std::string retval = "info: showing lines";
1✔
154

155
    if (ttt == nullptr) {
1✔
156
        return ec.make_error("this view does not support time filtering");
×
157
    }
158

159
    if (ec.ec_dry_run) {
1✔
160
        retval = "";
×
161
    } else if (!args.empty()) {
1✔
162
        ttt->clear_min_max_row_times();
1✔
163
        tc->get_sub_source()->text_filters_changed();
1✔
164
    }
165

166
    return Ok(retval);
1✔
167
}
1✔
168

169
static Result<std::string, lnav::console::user_message> com_enable_filter(
170
    exec_context& ec, std::string cmdline, std::vector<std::string>& args);
171

172
static Result<std::string, lnav::console::user_message>
173
com_filter(exec_context& ec,
32✔
174
           std::string cmdline,
175
           std::vector<std::string>& args)
176
{
177
    std::string retval;
32✔
178

179
    auto tc = *lnav_data.ld_view_stack.top();
32✔
180
    auto tss = tc->get_sub_source();
32✔
181

182
    if (!tss->tss_supports_filtering) {
32✔
183
        return ec.make_error("{} view does not support filtering",
184
                             lnav_view_strings[tc - lnav_data.ld_views]);
×
185
    }
186
    if (args.size() > 1) {
32✔
187
        const static intern_string_t PATTERN_SRC
188
            = intern_string::lookup("pattern");
90✔
189

190
        auto* tss = tc->get_sub_source();
32✔
191
        auto& fs = tss->get_filters();
32✔
192
        auto re_frag = remaining_args_frag(cmdline, args);
32✔
193
        args[1] = re_frag.to_string();
32✔
194
        if (fs.get_filter(args[1]) != nullptr) {
32✔
195
            return com_enable_filter(ec, cmdline, args);
1✔
196
        }
197

198
        if (fs.full()) {
31✔
199
            return ec.make_error(
200
                "filter limit reached, try combining "
201
                "filters with a pipe symbol (e.g. foo|bar)");
×
202
        }
203

204
        auto compile_res = lnav::pcre2pp::code::from(args[1], PCRE2_CASELESS);
31✔
205

206
        if (compile_res.isErr()) {
31✔
207
            auto ce = compile_res.unwrapErr();
×
208
            auto um = lnav::console::to_user_message(PATTERN_SRC, ce);
×
209
            return Err(um);
×
210
        }
211
        if (ec.ec_dry_run) {
31✔
212
            if (args[0] == "filter-in" && !fs.empty()) {
×
213
                lnav_data.ld_preview_status_source[0]
214
                    .get_description()
×
215
                    .set_value(
×
216
                        "Match preview for :filter-in only works if there are "
217
                        "no "
218
                        "other filters");
219
                retval = "";
×
220
            } else {
221
                auto& hm = tc->get_highlights();
×
222
                highlighter hl(compile_res.unwrap().to_shared());
×
223
                auto role = (args[0] == "filter-out") ? role_t::VCR_DIFF_DELETE
×
224
                                                      : role_t::VCR_DIFF_ADD;
×
225

226
                hl.with_role(role);
×
227
                hl.with_attrs(text_attrs::with_styles(
×
228
                    text_attrs::style::blink, text_attrs::style::reverse));
229

230
                hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
×
231
                tc->reload_data();
×
232

233
                lnav_data.ld_preview_status_source[0]
234
                    .get_description()
×
235
                    .set_value(
×
236
                        "Matches are highlighted in %s in the text view",
237
                        role == role_t::VCR_DIFF_DELETE ? "red" : "green");
238

239
                retval = "";
×
240
            }
241
            lnav_data.ld_status[LNS_PREVIEW0].set_needs_update();
×
242
        } else {
243
            auto lt = (args[0] == "filter-out") ? text_filter::EXCLUDE
31✔
244
                                                : text_filter::INCLUDE;
31✔
245
            auto filter_index = fs.next_index();
31✔
246
            if (!filter_index) {
31✔
247
                return ec.make_error("too many filters");
×
248
            }
249
            auto pf = std::make_shared<pcre_filter>(
250
                lt, args[1], *filter_index, compile_res.unwrap().to_shared());
31✔
251

252
            log_debug("%s [%d] %s",
31✔
253
                      args[0].c_str(),
254
                      pf->get_index(),
255
                      args[1].c_str());
256
            fs.add_filter(pf);
31✔
257
            const auto start_time = std::chrono::steady_clock::now();
31✔
258
            tss->text_filters_changed();
31✔
259
            const auto end_time = std::chrono::steady_clock::now();
31✔
260
            const double duration
261
                = std::chrono::duration_cast<std::chrono::milliseconds>(
31✔
262
                      end_time - start_time)
31✔
263
                      .count()
31✔
264
                / 1000.0;
31✔
265

266
            retval = fmt::format(FMT_STRING("info: filter activated in {:.3}s"),
124✔
267
                                 duration);
31✔
268
        }
31✔
269
    } else {
31✔
270
        return ec.make_error("expecting a regular expression to filter");
×
271
    }
272

273
    return Ok(retval);
31✔
274
}
32✔
275

276
static readline_context::prompt_result_t
277
com_filter_prompt(exec_context& ec, const std::string& cmdline)
×
278
{
279
    const auto* tc = lnav_data.ld_view_stack.top().value();
×
280
    std::vector<std::string> args;
×
281

282
    split_ws(cmdline, args);
×
283
    if (args.size() > 1) {
×
284
        return {};
×
285
    }
286

287
    if (tc->tc_selected_text) {
×
288
        return {"", tc->tc_selected_text->sti_value};
×
289
    }
290

291
    return {"", tc->get_current_search()};
×
292
}
293

294
static Result<std::string, lnav::console::user_message>
295
com_enable_filter(exec_context& ec,
1✔
296
                  std::string cmdline,
297
                  std::vector<std::string>& args)
298
{
299
    std::string retval;
1✔
300

301
    if (args.empty()) {
1✔
302
        args.emplace_back("disabled-filter");
×
303
    } else if (args.size() > 1) {
1✔
304
        auto* tc = *lnav_data.ld_view_stack.top();
1✔
305
        auto* tss = tc->get_sub_source();
1✔
306
        auto& fs = tss->get_filters();
1✔
307
        std::shared_ptr<text_filter> lf;
1✔
308

309
        args[1] = remaining_args(cmdline, args);
1✔
310
        lf = fs.get_filter(args[1]);
1✔
311
        if (lf == nullptr) {
1✔
312
            return ec.make_error("no such filter -- {}", args[1]);
×
313
        }
314
        if (lf->is_enabled()) {
1✔
315
            retval = "info: filter already enabled";
×
316
        } else if (ec.ec_dry_run) {
1✔
317
            retval = "";
×
318
        } else {
319
            fs.set_filter_enabled(lf, true);
1✔
320
            tss->text_filters_changed();
1✔
321
            retval = "info: filter enabled";
1✔
322
        }
323
    } else {
1✔
324
        return ec.make_error("expecting disabled filter to enable");
×
325
    }
326

327
    return Ok(retval);
1✔
328
}
1✔
329

330
static Result<std::string, lnav::console::user_message>
331
com_disable_filter(exec_context& ec,
2✔
332
                   std::string cmdline,
333
                   std::vector<std::string>& args)
334
{
335
    std::string retval;
2✔
336

337
    if (args.empty()) {
2✔
338
        args.emplace_back("enabled-filter");
×
339
    } else if (args.size() > 1) {
2✔
340
        auto* tc = *lnav_data.ld_view_stack.top();
2✔
341
        auto* tss = tc->get_sub_source();
2✔
342
        auto& fs = tss->get_filters();
2✔
343
        std::shared_ptr<text_filter> lf;
2✔
344

345
        args[1] = remaining_args(cmdline, args);
2✔
346
        lf = fs.get_filter(args[1]);
2✔
347
        if (lf == nullptr) {
2✔
348
            return ec.make_error("no such filter -- {}", args[1]);
×
349
        }
350
        if (!lf->is_enabled()) {
2✔
351
            retval = "info: filter already disabled";
×
352
        } else if (ec.ec_dry_run) {
2✔
353
            retval = "";
×
354
        } else {
355
            fs.set_filter_enabled(lf, false);
2✔
356
            tss->text_filters_changed();
2✔
357
            retval = "info: filter disabled";
2✔
358
        }
359
    } else {
2✔
360
        return ec.make_error("expecting enabled filter to disable");
×
361
    }
362

363
    return Ok(retval);
2✔
364
}
2✔
365

366
static Result<std::string, lnav::console::user_message>
367
com_delete_filter(exec_context& ec,
1✔
368
                  std::string cmdline,
369
                  std::vector<std::string>& args)
370
{
371
    std::string retval;
1✔
372

373
    if (args.empty()) {
1✔
374
        args.emplace_back("all-filters");
×
375
    } else if (args.size() > 1) {
1✔
376
        auto* tc = *lnav_data.ld_view_stack.top();
1✔
377
        auto* tss = tc->get_sub_source();
1✔
378
        auto& fs = tss->get_filters();
1✔
379

380
        args[1] = remaining_args(cmdline, args);
1✔
381
        if (ec.ec_dry_run) {
1✔
382
            retval = "";
×
383
        } else if (fs.delete_filter(args[1])) {
1✔
384
            retval = "info: deleted filter";
1✔
385
            tss->text_filters_changed();
1✔
386
        } else {
387
            return ec.make_error("unknown filter -- {}", args[1]);
×
388
        }
389
    } else {
390
        return ec.make_error("expecting a filter to delete");
×
391
    }
392

393
    return Ok(retval);
1✔
394
}
1✔
395

396
static readline_context::command_t FILTERING_COMMANDS[] = {
397
    {
398
        "hide-lines-before",
399
        com_hide_line,
400

401
        help_text(":hide-lines-before")
402
            .with_summary("Hide lines that come before the given date")
403
            .with_parameter(
404
                help_text("date", "An absolute or relative date")
405
                    .with_format(
406
                        help_parameter_format_t::HPF_TIME_FILTER_POINT))
407
            .with_examples({
408
                {"To hide the lines before the focused line in the view",
409
                 "here"},
410
                {"To hide the log messages before 6 AM today", "6am"},
411
            })
412
            .with_tags({"filtering"}),
413
    },
414
    {
415
        "hide-lines-after",
416
        com_hide_line,
417

418
        help_text(":hide-lines-after")
419
            .with_summary("Hide lines that come after the given date")
420
            .with_parameter(
421
                help_text("date", "An absolute or relative date")
422
                    .with_format(
423
                        help_parameter_format_t::HPF_TIME_FILTER_POINT))
424
            .with_examples({
425
                {"To hide the lines after the focused line in the view",
426
                 "here"},
427
                {"To hide the lines after 6 AM today", "6am"},
428
            })
429
            .with_tags({"filtering"}),
430
    },
431
    {
432
        "show-lines-before-and-after",
433
        com_show_lines,
434

435
        help_text(":show-lines-before-and-after")
436
            .with_summary("Show lines that were hidden by the "
437
                          "'hide-lines' commands")
438
            .with_opposites({"hide-lines-before", "hide-lines-after"})
439
            .with_tags({"filtering"}),
440
    },
441
    {
442
        "filter-in",
443
        com_filter,
444

445
        help_text(":filter-in")
446
            .with_summary("Only show lines that match the given regular "
447
                          "expression in the current view")
448
            .with_parameter(
449
                help_text("pattern", "The regular expression to match")
450
                    .with_format(help_parameter_format_t::HPF_REGEX))
451
            .with_tags({"filtering"})
452
            .with_example({"To filter out log messages that do not have the "
453
                           "string 'dhclient'",
454
                           "dhclient"}),
455
        com_filter_prompt,
456
    },
457
    {
458
        "filter-out",
459
        com_filter,
460

461
        help_text(":filter-out")
462
            .with_summary("Remove lines that match the given "
463
                          "regular expression "
464
                          "in the current view")
465
            .with_parameter(
466
                help_text("pattern", "The regular expression to match")
467
                    .with_format(help_parameter_format_t::HPF_REGEX))
468
            .with_tags({"filtering"})
469
            .with_example({"To filter out log messages that "
470
                           "contain the string "
471
                           "'last message repeated'",
472
                           "last message repeated"}),
473
        com_filter_prompt,
474
    },
475
    {
476
        "enable-filter",
477
        com_enable_filter,
478

479
        help_text(":enable-filter")
480
            .with_summary("Enable a previously created and disabled filter")
481
            .with_parameter(
482
                help_text("pattern",
483
                          "The regular expression used in the filter command")
484
                    .with_format(help_parameter_format_t::HPF_DISABLED_FILTERS))
485
            .with_tags({"filtering"})
486
            .with_opposites({"disable-filter"})
487
            .with_example({"To enable the disabled filter with the "
488
                           "pattern 'last "
489
                           "message repeated'",
490
                           "last message repeated"}),
491
    },
492
    {
493
        "disable-filter",
494
        com_disable_filter,
495

496
        help_text(":disable-filter")
497
            .with_summary("Disable a filter created with filter-in/filter-out")
498
            .with_parameter(
499
                help_text("pattern",
500
                          "The regular expression used in the filter command")
501
                    .with_format(help_parameter_format_t::HPF_ENABLED_FILTERS))
502
            .with_tags({"filtering"})
503
            .with_opposites({"filter-out", "filter-in"})
504
            .with_example({"To disable the filter with the pattern 'last "
505
                           "message repeated'",
506
                           "last message repeated"}),
507
    },
508
    {
509
        "delete-filter",
510
        com_delete_filter,
511

512
        help_text(":delete-filter")
513
            .with_summary(
514
                "Delete the filter created with ':filter-in' or ':filter-out'")
515
            .with_parameter(
516
                help_text("pattern", "The regular expression to match")
517
                    .with_format(help_parameter_format_t::HPF_ALL_FILTERS))
518
            .with_opposites({"filter-in", "filter-out"})
519
            .with_tags({"filtering"})
520
            .with_example({"To delete the filter with the pattern 'last "
521
                           "message repeated'",
522
                           "last message repeated"}),
523
    },
524
};
525

526
void
527
init_lnav_filtering_commands(readline_context::command_map_t& cmd_map)
598✔
528
{
529
    for (auto& cmd : FILTERING_COMMANDS) {
5,382✔
530
        cmd.c_help.index_tags();
4,784✔
531
        cmd_map[cmd.c_name] = &cmd;
14,352✔
532
    }
533
}
598✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc