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