1 /**
2 * Copyright (c) 2018, 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 ON
25 * 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 "config.h"
31
32 #include "base/enum_util.hh"
33 #include "base/func_util.hh"
34
35 #include "lnav.hh"
36
37 #include "filter_sub_source.hh"
38 #include "readline_possibilities.hh"
39 #include "readline_highlighters.hh"
40
41 using namespace std;
42
filter_sub_source()43 filter_sub_source::filter_sub_source()
44 {
45 this->fss_regex_context.set_highlighter(readline_regex_highlighter)
46 .set_append_character(0);
47 this->fss_editor.add_context(lnav::enums::to_underlying(filter_lang_t::REGEX),
48 this->fss_regex_context);
49 this->fss_sql_context.set_highlighter(readline_sqlite_highlighter)
50 .set_append_character(0);
51 this->fss_editor.add_context(lnav::enums::to_underlying(filter_lang_t::SQL),
52 this->fss_sql_context);
53 this->fss_editor.set_change_action(bind_mem(
54 &filter_sub_source::rl_change, this));
55 this->fss_editor.set_perform_action(bind_mem(
56 &filter_sub_source::rl_perform, this));
57 this->fss_editor.set_abort_action(bind_mem(
58 &filter_sub_source::rl_abort, this));
59 this->fss_editor.set_display_match_action(
60 bind_mem(&filter_sub_source::rl_display_matches, this));
61 this->fss_editor.set_display_next_action(
62 bind_mem(&filter_sub_source::rl_display_next, this));
63 this->fss_match_view.set_sub_source(&this->fss_match_source);
64 this->fss_match_view.set_height(0_vl);
65 this->fss_match_view.set_show_scrollbar(true);
66 this->fss_match_view.set_default_role(view_colors::VCR_POPUP);
67 }
68
list_input_handle_key(listview_curses & lv,int ch)69 bool filter_sub_source::list_input_handle_key(listview_curses &lv, int ch)
70 {
71 if (this->fss_editing) {
72 switch (ch) {
73 case KEY_CTRL_RBRACKET:
74 this->fss_editor.abort();
75 return true;
76 default:
77 this->fss_editor.handle_key(ch);
78 return true;
79 }
80 }
81
82 switch (ch) {
83 case 'f': {
84 auto top_view = *lnav_data.ld_view_stack.top();
85 auto tss = top_view->get_sub_source();
86
87 tss->toggle_apply_filters();
88 break;
89 }
90 case ' ': {
91 textview_curses *top_view = *lnav_data.ld_view_stack.top();
92 text_sub_source *tss = top_view->get_sub_source();
93 filter_stack &fs = tss->get_filters();
94
95 if (fs.empty()) {
96 return true;
97 }
98
99 shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());
100
101 fs.set_filter_enabled(tf, !tf->is_enabled());
102 tss->text_filters_changed();
103 lv.reload_data();
104 return true;
105 }
106 case 't': {
107 textview_curses *top_view = *lnav_data.ld_view_stack.top();
108 text_sub_source *tss = top_view->get_sub_source();
109 filter_stack &fs = tss->get_filters();
110
111 if (fs.empty()) {
112 return true;
113 }
114
115 shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());
116
117 if (tf->get_type() == text_filter::INCLUDE) {
118 tf->set_type(text_filter::EXCLUDE);
119 } else {
120 tf->set_type(text_filter::INCLUDE);
121 }
122
123 tss->text_filters_changed();
124 lv.reload_data();
125 return true;
126 }
127 case 'D': {
128 textview_curses *top_view = *lnav_data.ld_view_stack.top();
129 text_sub_source *tss = top_view->get_sub_source();
130 filter_stack &fs = tss->get_filters();
131
132 if (fs.empty()) {
133 return true;
134 }
135
136 shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());
137
138 fs.delete_filter(tf->get_id());
139 lv.reload_data();
140 tss->text_filters_changed();
141 return true;
142 }
143 case 'i': {
144 textview_curses *top_view = *lnav_data.ld_view_stack.top();
145 text_sub_source *tss = top_view->get_sub_source();
146 filter_stack &fs = tss->get_filters();
147 auto filter_index = fs.next_index();
148
149 if (!filter_index) {
150 lnav_data.ld_filter_help_status_source.fss_error
151 .set_value("error: too many filters");
152 return true;
153 }
154
155 auto ef = make_shared<empty_filter>(text_filter::type_t::INCLUDE,
156 *filter_index);
157 fs.add_filter(ef);
158 lv.set_selection(vis_line_t(fs.size() - 1));
159 lv.reload_data();
160
161 this->fss_editing = true;
162
163 add_view_text_possibilities(&this->fss_editor,
164 lnav::enums::to_underlying(filter_lang_t::REGEX),
165 "*",
166 top_view);
167 this->fss_editor.set_window(lv.get_window());
168 this->fss_editor.set_visible(true);
169 this->fss_editor.set_y(
170 lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
171 this->fss_editor.set_left(25);
172 this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
173 this->fss_editor.focus(lnav::enums::to_underlying(filter_lang_t::REGEX), "", "");
174 this->fss_filter_state = true;
175 ef->disable();
176 return true;
177 }
178 case 'o': {
179 textview_curses *top_view = *lnav_data.ld_view_stack.top();
180 text_sub_source *tss = top_view->get_sub_source();
181 filter_stack &fs = tss->get_filters();
182 auto filter_index = fs.next_index();
183
184 if (!filter_index) {
185 lnav_data.ld_filter_help_status_source.fss_error
186 .set_value("error: too many filters");
187 return true;
188 }
189
190 auto ef = make_shared<empty_filter>(text_filter::type_t::EXCLUDE,
191 *filter_index);
192 fs.add_filter(ef);
193 lv.set_selection(vis_line_t(fs.size() - 1));
194 lv.reload_data();
195
196 this->fss_editing = true;
197
198 add_view_text_possibilities(&this->fss_editor,
199 lnav::enums::to_underlying(filter_lang_t::REGEX),
200 "*",
201 top_view);
202 this->fss_editor.set_window(lv.get_window());
203 this->fss_editor.set_visible(true);
204 this->fss_editor.set_y(
205 lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
206 this->fss_editor.set_left(25);
207 this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
208 this->fss_editor.focus(lnav::enums::to_underlying(filter_lang_t::REGEX), "", "");
209 this->fss_filter_state = true;
210 ef->disable();
211 return true;
212 }
213 case '\r':
214 case KEY_ENTER: {
215 textview_curses *top_view = *lnav_data.ld_view_stack.top();
216 text_sub_source *tss = top_view->get_sub_source();
217 filter_stack &fs = tss->get_filters();
218
219 if (fs.empty()) {
220 return true;
221 }
222
223 shared_ptr<text_filter> tf = *(fs.begin() + lv.get_selection());
224
225 this->fss_editing = true;
226
227 add_view_text_possibilities(&this->fss_editor,
228 lnav::enums::to_underlying(filter_lang_t::REGEX),
229 "*",
230 top_view);
231 add_filter_expr_possibilities(&this->fss_editor,
232 lnav::enums::to_underlying(filter_lang_t::SQL),
233 "*");
234 this->fss_editor.set_window(lv.get_window());
235 this->fss_editor.set_visible(true);
236 this->fss_editor.set_y(
237 lv.get_y() + (int) (lv.get_selection() - lv.get_top()));
238 this->fss_editor.set_left(25);
239 this->fss_editor.set_width(this->tss_view->get_width() - 8 - 1);
240 this->fss_editor.focus(lnav::enums::to_underlying(tf->get_lang()), "");
241 this->fss_editor.rewrite_line(0, tf->get_id().c_str());
242 this->fss_filter_state = tf->is_enabled();
243 tf->disable();
244 tss->text_filters_changed();
245 return true;
246 }
247 case 'n': {
248 execute_command(lnav_data.ld_exec_context, "next-mark search");
249 return true;
250 }
251 case 'N': {
252 execute_command(lnav_data.ld_exec_context, "prev-mark search");
253 return true;
254 }
255 case '/': {
256 execute_command(lnav_data.ld_exec_context,
257 "prompt search-filters");
258 return true;
259 }
260 default:
261 log_debug("unhandled %x", ch);
262 break;
263 }
264
265 return false;
266 }
267
text_line_count()268 size_t filter_sub_source::text_line_count()
269 {
270 return (lnav_data.ld_view_stack.top() | [](auto tc) {
271 text_sub_source *tss = tc->get_sub_source();
272 filter_stack &fs = tss->get_filters();
273
274 return nonstd::make_optional(fs.size());
275 }).value_or(0);
276 }
277
text_line_width(textview_curses & curses)278 size_t filter_sub_source::text_line_width(textview_curses &curses)
279 {
280 textview_curses *top_view = *lnav_data.ld_view_stack.top();
281 text_sub_source *tss = top_view->get_sub_source();
282 filter_stack &fs = tss->get_filters();
283 size_t retval = 0;
284
285 for (auto &filter : fs) {
286 retval = std::max(filter->get_id().size() + 8, retval);
287 }
288
289 return retval;
290 }
291
text_value_for_line(textview_curses & tc,int line,std::string & value_out,text_sub_source::line_flags_t flags)292 void filter_sub_source::text_value_for_line(textview_curses &tc, int line,
293 std::string &value_out,
294 text_sub_source::line_flags_t flags)
295 {
296 textview_curses *top_view = *lnav_data.ld_view_stack.top();
297 text_sub_source *tss = top_view->get_sub_source();
298 filter_stack &fs = tss->get_filters();
299 shared_ptr<text_filter> tf = *(fs.begin() + line);
300 char hits[32];
301
302 value_out = " ";
303 switch (tf->get_type()) {
304 case text_filter::INCLUDE:
305 value_out.append(" IN ");
306 break;
307 case text_filter::EXCLUDE:
308 if (tf->get_lang() == filter_lang_t::REGEX) {
309 value_out.append("OUT ");
310 } else {
311 value_out.append(" ");
312 }
313 break;
314 default:
315 ensure(0);
316 break;
317 }
318
319 if (this->fss_editing && line == tc.get_selection()) {
320 snprintf(hits, sizeof(hits), "%9s hits | ", "-");
321 } else {
322 snprintf(hits, sizeof(hits), "%'9d hits | ",
323 tss->get_filtered_count_for(tf->get_index()));
324 }
325 value_out.append(hits);
326
327 value_out.append(tf->get_id());
328 }
329
text_attrs_for_line(textview_curses & tc,int line,string_attrs_t & value_out)330 void filter_sub_source::text_attrs_for_line(textview_curses &tc, int line,
331 string_attrs_t &value_out)
332 {
333 auto &vcolors = view_colors::singleton();
334 textview_curses *top_view = *lnav_data.ld_view_stack.top();
335 text_sub_source *tss = top_view->get_sub_source();
336 filter_stack &fs = tss->get_filters();
337 shared_ptr<text_filter> tf = *(fs.begin() + line);
338 bool selected =
339 lnav_data.ld_mode == LNM_FILTER && line == tc.get_selection();
340
341 if (selected) {
342 value_out.emplace_back(line_range{0, 1}, &view_curses::VC_GRAPHIC,
343 ACS_RARROW);
344 }
345
346 chtype enabled = tf->is_enabled() ? ACS_DIAMOND : ' ';
347
348 line_range lr{2, 3};
349 value_out.emplace_back(lr, &view_curses::VC_GRAPHIC, enabled);
350 if (tf->is_enabled()) {
351 value_out.emplace_back(lr, &view_curses::VC_FOREGROUND,
352 vcolors.ansi_to_theme_color(COLOR_GREEN));
353 }
354
355 int fg_role = tf->get_type() == text_filter::INCLUDE ?
356 view_colors::VCR_OK : view_colors::VCR_ERROR;
357 value_out.emplace_back(line_range{4, 7},
358 &view_curses::VC_ROLE_FG,
359 fg_role);
360 value_out.emplace_back(line_range{4, 7}, &view_curses::VC_STYLE, A_BOLD);
361
362 value_out.emplace_back(line_range{8, 17}, &view_curses::VC_STYLE, A_BOLD);
363 value_out.emplace_back(line_range{23, 24}, &view_curses::VC_GRAPHIC,
364 ACS_VLINE);
365
366 if (selected) {
367 value_out.emplace_back(line_range{0, -1},
368 &view_curses::VC_ROLE,
369 view_colors::VCR_FOCUSED);
370 }
371 }
372
text_size_for_line(textview_curses & tc,int line,text_sub_source::line_flags_t raw)373 size_t filter_sub_source::text_size_for_line(textview_curses &tc, int line,
374 text_sub_source::line_flags_t raw)
375 {
376 textview_curses *top_view = *lnav_data.ld_view_stack.top();
377 text_sub_source *tss = top_view->get_sub_source();
378 filter_stack &fs = tss->get_filters();
379 shared_ptr<text_filter> tf = *(fs.begin() + line);
380
381 return 8 + tf->get_id().size();
382 }
383
rl_change(readline_curses * rc)384 void filter_sub_source::rl_change(readline_curses *rc)
385 {
386 textview_curses *top_view = *lnav_data.ld_view_stack.top();
387 text_sub_source *tss = top_view->get_sub_source();
388 filter_stack &fs = tss->get_filters();
389 if (fs.empty()) {
390 return;
391 }
392
393 auto iter = fs.begin() + this->tss_view->get_selection();
394 shared_ptr<text_filter> tf = *iter;
395 string new_value = rc->get_line_buffer();
396
397 switch (tf->get_lang()) {
398 case filter_lang_t::NONE:
399 break;
400 case filter_lang_t::REGEX: {
401 auto_mem<pcre> code;
402 const char *errptr;
403 int eoff;
404
405 if ((code = pcre_compile(new_value.c_str(),
406 PCRE_CASELESS,
407 &errptr,
408 &eoff,
409 nullptr)) == nullptr) {
410 lnav_data.ld_filter_help_status_source.fss_error
411 .set_value("error: %s", errptr);
412 } else {
413 auto &hm = top_view->get_highlights();
414 highlighter hl(code.release());
415 int color;
416
417 if (tf->get_type() == text_filter::EXCLUDE) {
418 color = COLOR_RED;
419 } else {
420 color = COLOR_GREEN;
421 }
422 hl.with_attrs(
423 view_colors::ansi_color_pair(COLOR_BLACK, color) | A_BLINK);
424
425 hm[{highlight_source_t::PREVIEW, "preview"}] = hl;
426 top_view->set_needs_update();
427 lnav_data.ld_filter_help_status_source.fss_error.clear();
428 }
429 break;
430 }
431 case filter_lang_t::SQL: {
432 auto full_sql = fmt::format("SELECT 1 WHERE {}", new_value);
433 auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
434 #ifdef SQLITE_PREPARE_PERSISTENT
435 auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
436 full_sql.c_str(),
437 full_sql.size(),
438 SQLITE_PREPARE_PERSISTENT,
439 stmt.out(),
440 nullptr);
441 #else
442 auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
443 full_sql.c_str(),
444 full_sql.size(),
445 stmt.out(),
446 nullptr);
447 #endif
448 if (retcode != SQLITE_OK) {
449 lnav_data.ld_filter_help_status_source.fss_error
450 .set_value("error: %s", sqlite3_errmsg(lnav_data.ld_db));
451 } else {
452 auto set_res = lnav_data.ld_log_source
453 .set_preview_sql_filter(stmt.release());
454
455 if (set_res.isErr()) {
456 lnav_data.ld_filter_help_status_source.fss_error
457 .set_value("error: %s", set_res.unwrapErr().c_str());
458 } else {
459 top_view->set_needs_update();
460 lnav_data.ld_filter_help_status_source.fss_error.clear();
461 }
462 }
463 break;
464 }
465 }
466 }
467
rl_perform(readline_curses * rc)468 void filter_sub_source::rl_perform(readline_curses *rc)
469 {
470 textview_curses *top_view = *lnav_data.ld_view_stack.top();
471 text_sub_source *tss = top_view->get_sub_source();
472 filter_stack &fs = tss->get_filters();
473 auto iter = fs.begin() + this->tss_view->get_selection();
474 shared_ptr<text_filter> tf = *iter;
475 string new_value = rc->get_value();
476
477 if (new_value.empty()) {
478 this->rl_abort(rc);
479 } else {
480 top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
481 switch (tf->get_lang()) {
482 case filter_lang_t::NONE:
483 case filter_lang_t::REGEX: {
484 auto_mem<pcre> code;
485 const char *errptr;
486 int eoff;
487
488 if ((code = pcre_compile(new_value.c_str(),
489 PCRE_CASELESS,
490 &errptr,
491 &eoff,
492 nullptr)) == nullptr) {
493 this->rl_abort(rc);
494 } else {
495 tf->lf_deleted = true;
496 tss->text_filters_changed();
497
498 auto pf = make_shared<pcre_filter>(tf->get_type(),
499 new_value, tf->get_index(),
500 code.release());
501
502 *iter = pf;
503 tss->text_filters_changed();
504 }
505 break;
506 }
507 case filter_lang_t::SQL: {
508 auto full_sql = fmt::format("SELECT 1 WHERE {}", new_value);
509 auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
510 #ifdef SQLITE_PREPARE_PERSISTENT
511 auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(),
512 full_sql.c_str(),
513 full_sql.size(),
514 SQLITE_PREPARE_PERSISTENT,
515 stmt.out(),
516 nullptr);
517 #else
518 auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(),
519 full_sql.c_str(),
520 full_sql.size(),
521 stmt.out(),
522 nullptr);
523 #endif
524 if (retcode != SQLITE_OK) {
525 this->rl_abort(rc);
526 } else {
527 lnav_data.ld_log_source.set_sql_filter(
528 new_value, stmt.release());
529 tss->text_filters_changed();
530 }
531 break;
532 }
533 }
534 }
535
536 lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
537 lnav_data.ld_filter_help_status_source.fss_prompt.clear();
538 this->fss_editing = false;
539 this->fss_editor.set_visible(false);
540 this->tss_view->reload_data();
541 }
542
rl_abort(readline_curses * rc)543 void filter_sub_source::rl_abort(readline_curses *rc)
544 {
545 textview_curses *top_view = *lnav_data.ld_view_stack.top();
546 text_sub_source *tss = top_view->get_sub_source();
547 filter_stack &fs = tss->get_filters();
548 auto iter = fs.begin() + this->tss_view->get_selection();
549 shared_ptr<text_filter> tf = *iter;
550
551 lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
552 lnav_data.ld_filter_help_status_source.fss_prompt.clear();
553 lnav_data.ld_filter_help_status_source.fss_error.clear();
554 top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
555 top_view->reload_data();
556 fs.delete_filter("");
557 this->tss_view->reload_data();
558 this->fss_editor.set_visible(false);
559 this->fss_editing = false;
560 this->tss_view->set_needs_update();
561 tf->set_enabled(this->fss_filter_state);
562 tss->text_filters_changed();
563 this->tss_view->reload_data();
564 }
565
rl_display_matches(readline_curses * rc)566 void filter_sub_source::rl_display_matches(readline_curses *rc)
567 {
568 const std::vector<std::string> &matches = rc->get_matches();
569 unsigned long width = 0;
570
571 if (matches.empty()) {
572 this->fss_match_source.clear();
573 this->fss_match_view.set_height(0_vl);
574 this->tss_view->set_needs_update();
575 } else {
576 string current_match = rc->get_match_string();
577 attr_line_t al;
578 vis_line_t line, selected_line;
579
580 for (auto &match : matches) {
581 if (match == current_match) {
582 al.append(match, &view_curses::VC_STYLE, A_REVERSE);
583 selected_line = line;
584 } else {
585 al.append(match);
586 }
587 al.append(1, '\n');
588 width = std::max(width, (unsigned long) match.size());
589 line += 1_vl;
590 }
591
592 this->fss_match_view.set_selection(selected_line);
593 this->fss_match_source.replace_with(al);
594 this->fss_match_view.set_height(
595 std::min(vis_line_t(matches.size()), 3_vl));
596 }
597
598 this->fss_match_view.set_window(this->tss_view->get_window());
599 this->fss_match_view.set_y(rc->get_y() + 1);
600 this->fss_match_view.set_x(rc->get_left() + rc->get_match_start());
601 this->fss_match_view.set_width(width + 3);
602 this->fss_match_view.set_needs_update();
603 this->fss_match_view.scroll_selection_into_view();
604 this->fss_match_view.reload_data();
605 }
606
rl_display_next(readline_curses * rc)607 void filter_sub_source::rl_display_next(readline_curses *rc)
608 {
609 textview_curses &tc = this->fss_match_view;
610
611 if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) {
612 tc.set_top(0_vl);
613 } else {
614 tc.shift_top(tc.get_height());
615 }
616 }
617
list_input_handle_scroll_out(listview_curses & lv)618 void filter_sub_source::list_input_handle_scroll_out(listview_curses &lv)
619 {
620 lnav_data.ld_mode = LNM_PAGING;
621 lnav_data.ld_filter_view.reload_data();
622 }
623