/** * Copyright (c) 2018, Timothy Stack * * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are met: * * * Redistributions of source code must retain the above copyright notice, this * list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above copyright notice, * this list of conditions and the following disclaimer in the documentation * and/or other materials provided with the distribution. * * Neither the name of Timothy Stack nor the names of its contributors * may be used to endorse or promote products derived from this software * without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE REGENTS AND CONTRIBUTORS ''AS IS'' AND ANY * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE * DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR CONTRIBUTORS BE LIABLE FOR ANY * DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES * (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND * ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "filter_sub_source.hh" #include "base/attr_line.builder.hh" #include "base/enum_util.hh" #include "base/func_util.hh" #include "base/opt_util.hh" #include "bound_tags.hh" #include "config.h" #include "lnav.hh" #include "readline_highlighters.hh" #include "readline_possibilities.hh" #include "sql_util.hh" using namespace lnav::roles::literals; filter_sub_source::filter_sub_source(std::shared_ptr editor) : fss_editor(editor) { this->fss_editor->set_x(25); this->fss_editor->set_width(-1); this->fss_editor->set_save_history(!(lnav_data.ld_flags & LNF_SECURE_MODE)); this->fss_regex_context.set_highlighter(readline_regex_highlighter) .set_append_character(0); this->fss_editor->add_context(filter_lang_t::REGEX, this->fss_regex_context); this->fss_sql_context.set_highlighter(readline_sqlite_highlighter) .set_append_character(0); this->fss_editor->add_context(filter_lang_t::SQL, this->fss_sql_context); this->fss_editor->set_change_action( bind_mem(&filter_sub_source::rl_change, this)); this->fss_editor->set_perform_action( bind_mem(&filter_sub_source::rl_perform, this)); this->fss_editor->set_abort_action( bind_mem(&filter_sub_source::rl_abort, this)); this->fss_editor->set_display_match_action( bind_mem(&filter_sub_source::rl_display_matches, this)); this->fss_editor->set_display_next_action( bind_mem(&filter_sub_source::rl_display_next, this)); this->fss_match_view.set_sub_source(&this->fss_match_source); this->fss_match_view.set_height(0_vl); this->fss_match_view.set_show_scrollbar(true); this->fss_match_view.set_default_role(role_t::VCR_POPUP); } void filter_sub_source::register_view(textview_curses* tc) { text_sub_source::register_view(tc); tc->add_child_view(this->fss_editor.get()); tc->add_child_view(&this->fss_match_view); } bool filter_sub_source::list_input_handle_key(listview_curses& lv, int ch) { if (this->fss_editing) { switch (ch) { case KEY_ESCAPE: case KEY_CTRL(']'): this->fss_editor->abort(); return true; default: this->fss_editor->handle_key(ch); return true; } } switch (ch) { case 'f': { auto* top_view = *lnav_data.ld_view_stack.top(); auto* tss = top_view->get_sub_source(); tss->toggle_apply_filters(); top_view->reload_data(); break; } case ' ': { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); if (fs.empty()) { return true; } auto tf = *(fs.begin() + lv.get_selection()); fs.set_filter_enabled(tf, !tf->is_enabled()); tss->text_filters_changed(); lv.reload_data(); top_view->reload_data(); return true; } case 't': { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); if (fs.empty()) { return true; } auto tf = *(fs.begin() + lv.get_selection()); if (tf->get_type() == text_filter::INCLUDE) { tf->set_type(text_filter::EXCLUDE); } else { tf->set_type(text_filter::INCLUDE); } tss->text_filters_changed(); lv.reload_data(); top_view->reload_data(); return true; } case 'D': { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); if (fs.empty()) { return true; } auto tf = *(fs.begin() + lv.get_selection()); fs.delete_filter(tf->get_id()); lv.reload_data(); tss->text_filters_changed(); top_view->reload_data(); return true; } case 'i': { auto* top_view = *lnav_data.ld_view_stack.top(); auto* tss = top_view->get_sub_source(); auto& fs = tss->get_filters(); auto filter_index = fs.next_index(); if (!filter_index) { lnav_data.ld_filter_help_status_source.fss_error.set_value( "error: too many filters"); return true; } auto ef = std::make_shared( text_filter::type_t::INCLUDE, *filter_index); fs.add_filter(ef); lv.set_selection(vis_line_t(fs.size() - 1)); lv.reload_data(); this->fss_editing = true; this->tss_view->vc_enabled = false; add_view_text_possibilities(this->fss_editor.get(), filter_lang_t::REGEX, "*", top_view, text_quoting::regex); this->fss_editor->set_window(lv.get_window()); this->fss_editor->set_visible(true); this->fss_editor->set_y( lv.get_y() + (int) (lv.get_selection() - lv.get_top())); this->fss_editor->window_change(); this->fss_editor->focus(filter_lang_t::REGEX, "", ""); this->fss_filter_state = true; ef->disable(); return true; } case 'o': { auto* top_view = *lnav_data.ld_view_stack.top(); auto* tss = top_view->get_sub_source(); auto& fs = tss->get_filters(); auto filter_index = fs.next_index(); if (!filter_index) { lnav_data.ld_filter_help_status_source.fss_error.set_value( "error: too many filters"); return true; } auto ef = std::make_shared( text_filter::type_t::EXCLUDE, *filter_index); fs.add_filter(ef); lv.set_selection(vis_line_t(fs.size() - 1)); lv.reload_data(); this->fss_editing = true; this->tss_view->vc_enabled = false; add_view_text_possibilities(this->fss_editor.get(), filter_lang_t::REGEX, "*", top_view, text_quoting::regex); this->fss_editor->set_window(lv.get_window()); this->fss_editor->set_visible(true); this->fss_editor->set_y( lv.get_y() + (int) (lv.get_selection() - lv.get_top())); this->fss_editor->window_change(); this->fss_editor->focus(filter_lang_t::REGEX, "", ""); this->fss_filter_state = true; ef->disable(); return true; } case '\r': case KEY_ENTER: { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); if (fs.empty()) { return true; } auto tf = *(fs.begin() + lv.get_selection()); this->fss_editing = true; this->tss_view->vc_enabled = false; auto tq = tf->get_lang() == filter_lang_t::SQL ? text_quoting::sql : text_quoting::regex; add_view_text_possibilities( this->fss_editor.get(), tf->get_lang(), "*", top_view, tq); if (top_view == &lnav_data.ld_views[LNV_LOG]) { add_filter_expr_possibilities( this->fss_editor.get(), filter_lang_t::SQL, "*"); } this->fss_editor->set_window(lv.get_window()); this->fss_editor->set_visible(true); this->fss_editor->set_y( lv.get_y() + (int) (lv.get_selection() - lv.get_top())); this->fss_editor->focus(tf->get_lang(), ""); this->fss_editor->rewrite_line(0, tf->get_id().c_str()); this->fss_filter_state = tf->is_enabled(); tf->disable(); tss->text_filters_changed(); return true; } case 'n': { execute_command(lnav_data.ld_exec_context, "next-mark search"); return true; } case 'N': { execute_command(lnav_data.ld_exec_context, "prev-mark search"); return true; } case '/': { execute_command(lnav_data.ld_exec_context, "prompt search-filters"); return true; } default: log_debug("unhandled %x", ch); break; } return false; } size_t filter_sub_source::text_line_count() { return (lnav_data.ld_view_stack.top() | [](auto tc) -> nonstd::optional { text_sub_source* tss = tc->get_sub_source(); if (tss == nullptr) { return nonstd::nullopt; } auto& fs = tss->get_filters(); return nonstd::make_optional(fs.size()); }) .value_or(0); } size_t filter_sub_source::text_line_width(textview_curses& curses) { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); size_t retval = 0; for (auto& filter : fs) { retval = std::max(filter->get_id().size() + 8, retval); } return retval; } void filter_sub_source::text_value_for_line(textview_curses& tc, int line, std::string& value_out, text_sub_source::line_flags_t flags) { auto* top_view = *lnav_data.ld_view_stack.top(); auto* tss = top_view->get_sub_source(); auto& fs = tss->get_filters(); auto tf = *(fs.begin() + line); bool selected = lnav_data.ld_mode == ln_mode_t::FILTER && line == tc.get_selection(); this->fss_curr_line.clear(); auto& al = this->fss_curr_line; attr_line_builder alb(al); if (selected) { al.append(" ", VC_GRAPHIC.value(ACS_RARROW)); } else { al.append(" "); } al.append(" "); if (tf->is_enabled()) { al.append("\u25c6"_ok); } else { al.append("\u25c7"_comment); } al.append(" "); switch (tf->get_type()) { case text_filter::INCLUDE: al.append(" ").append(lnav::roles::ok("IN")).append(" "); break; case text_filter::EXCLUDE: if (tf->get_lang() == filter_lang_t::REGEX) { al.append(lnav::roles::error("OUT")).append(" "); } else { al.append(" "); } break; default: ensure(0); break; } { auto ag = alb.with_attr(VC_ROLE.value(role_t::VCR_NUMBER)); if (this->fss_editing && line == tc.get_selection()) { alb.appendf(FMT_STRING("{:>9}"), "-"); } else { alb.appendf(FMT_STRING("{:>9}"), tss->get_filtered_count_for(tf->get_index())); } } al.append(" hits ").append("|", VC_GRAPHIC.value(ACS_VLINE)).append(" "); attr_line_t content{tf->get_id()}; switch (tf->get_lang()) { case filter_lang_t::REGEX: readline_regex_highlighter(content, content.length()); break; case filter_lang_t::SQL: readline_sqlite_highlighter(content, content.length()); break; case filter_lang_t::NONE: break; } al.append(content); if (selected) { al.with_attr_for_all(VC_ROLE.value(role_t::VCR_FOCUSED)); } value_out = al.get_string(); } void filter_sub_source::text_attrs_for_line(textview_curses& tc, int line, string_attrs_t& value_out) { value_out = this->fss_curr_line.get_attrs(); } size_t filter_sub_source::text_size_for_line(textview_curses& tc, int line, text_sub_source::line_flags_t raw) { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); auto tf = *(fs.begin() + line); return 8 + tf->get_id().size(); } void filter_sub_source::rl_change(readline_curses* rc) { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); if (fs.empty()) { return; } auto iter = fs.begin() + this->tss_view->get_selection(); auto tf = *iter; auto new_value = rc->get_line_buffer(); switch (tf->get_lang()) { case filter_lang_t::NONE: break; case filter_lang_t::REGEX: { if (new_value.empty()) { if (fs.get_filter(top_view->get_current_search()) == nullptr) { this->fss_editor->set_suggestion( top_view->get_current_search()); } } else { auto regex_res = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS); if (regex_res.isErr()) { auto pe = regex_res.unwrapErr(); lnav_data.ld_filter_help_status_source.fss_error.set_value( "error: %s", pe.get_message().c_str()); } else { auto& hm = top_view->get_highlights(); highlighter hl(regex_res.unwrap().to_shared()); auto role = tf->get_type() == text_filter::EXCLUDE ? role_t::VCR_DIFF_DELETE : role_t::VCR_DIFF_ADD; hl.with_role(role); hl.with_attrs(text_attrs{A_BLINK | A_REVERSE}); hm[{highlight_source_t::PREVIEW, "preview"}] = hl; top_view->set_needs_update(); lnav_data.ld_filter_help_status_source.fss_error.clear(); } } break; } case filter_lang_t::SQL: { auto full_sql = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value); auto_mem stmt(sqlite3_finalize); #ifdef SQLITE_PREPARE_PERSISTENT auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(), full_sql.c_str(), full_sql.size(), SQLITE_PREPARE_PERSISTENT, stmt.out(), nullptr); #else auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(), full_sql.c_str(), full_sql.size(), stmt.out(), nullptr); #endif if (retcode != SQLITE_OK) { lnav_data.ld_filter_help_status_source.fss_error.set_value( "error: %s", sqlite3_errmsg(lnav_data.ld_db)); } else { auto set_res = lnav_data.ld_log_source.set_preview_sql_filter( stmt.release()); if (set_res.isErr()) { lnav_data.ld_filter_help_status_source.fss_error.set_value( "error: %s", set_res.unwrapErr() .to_attr_line() .get_string() .c_str()); } else { top_view->set_needs_update(); lnav_data.ld_filter_help_status_source.fss_error.clear(); } } break; } } } void filter_sub_source::rl_perform(readline_curses* rc) { static const intern_string_t INPUT_SRC = intern_string::lookup("input"); textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); auto iter = fs.begin() + this->tss_view->get_selection(); auto tf = *iter; auto new_value = rc->get_value().get_string(); if (new_value.empty()) { this->rl_abort(rc); } else { top_view->get_highlights().erase( {highlight_source_t::PREVIEW, "preview"}); switch (tf->get_lang()) { case filter_lang_t::NONE: case filter_lang_t::REGEX: { auto compile_res = lnav::pcre2pp::code::from(new_value, PCRE2_CASELESS); if (compile_res.isErr()) { auto ce = compile_res.unwrapErr(); auto um = lnav::console::to_user_message(INPUT_SRC, ce); lnav_data.ld_exec_context.ec_error_callback_stack.back()( um); this->rl_abort(rc); } else { tf->lf_deleted = true; tss->text_filters_changed(); auto pf = std::make_shared( tf->get_type(), new_value, tf->get_index(), compile_res.unwrap().to_shared()); *iter = pf; tss->text_filters_changed(); } break; } case filter_lang_t::SQL: { auto full_sql = fmt::format(FMT_STRING("SELECT 1 WHERE {}"), new_value); auto_mem stmt(sqlite3_finalize); #ifdef SQLITE_PREPARE_PERSISTENT auto retcode = sqlite3_prepare_v3(lnav_data.ld_db.in(), full_sql.c_str(), full_sql.size(), SQLITE_PREPARE_PERSISTENT, stmt.out(), nullptr); #else auto retcode = sqlite3_prepare_v2(lnav_data.ld_db.in(), full_sql.c_str(), full_sql.size(), stmt.out(), nullptr); #endif if (retcode != SQLITE_OK) { auto sqlerr = annotate_sql_with_error( lnav_data.ld_db.in(), full_sql.c_str(), nullptr); auto um = lnav::console::user_message::error( "invalid SQL expression") .with_reason(sqlite3_errmsg(lnav_data.ld_db.in())) .with_snippet(lnav::console::snippet::from( INPUT_SRC, sqlerr)); lnav_data.ld_exec_context.ec_error_callback_stack.back()( um); this->rl_abort(rc); } else { lnav_data.ld_log_source.set_sql_filter(new_value, stmt.release()); tss->text_filters_changed(); } break; } } } lnav_data.ld_log_source.set_preview_sql_filter(nullptr); lnav_data.ld_filter_help_status_source.fss_prompt.clear(); this->fss_editing = false; this->tss_view->vc_enabled = true; this->fss_editor->set_visible(false); top_view->reload_data(); this->tss_view->reload_data(); } void filter_sub_source::rl_abort(readline_curses* rc) { textview_curses* top_view = *lnav_data.ld_view_stack.top(); text_sub_source* tss = top_view->get_sub_source(); filter_stack& fs = tss->get_filters(); auto iter = fs.begin() + this->tss_view->get_selection(); auto tf = *iter; lnav_data.ld_log_source.set_preview_sql_filter(nullptr); lnav_data.ld_filter_help_status_source.fss_prompt.clear(); lnav_data.ld_filter_help_status_source.fss_error.clear(); top_view->get_highlights().erase({highlight_source_t::PREVIEW, "preview"}); top_view->reload_data(); fs.delete_filter(""); this->tss_view->reload_data(); this->fss_editor->set_visible(false); this->fss_editing = false; this->tss_view->vc_enabled = true; this->tss_view->set_needs_update(); tf->set_enabled(this->fss_filter_state); tss->text_filters_changed(); this->tss_view->reload_data(); } void filter_sub_source::rl_display_matches(readline_curses* rc) { const std::vector& matches = rc->get_matches(); unsigned long width = 0; if (matches.empty()) { this->fss_match_source.clear(); this->fss_match_view.set_height(0_vl); this->tss_view->set_needs_update(); } else { auto current_match = rc->get_match_string(); attr_line_t al; vis_line_t line, selected_line; for (const auto& match : matches) { if (match == current_match) { al.append(match, VC_STYLE.value(text_attrs{A_REVERSE})); selected_line = line; } else { al.append(match); } al.append(1, '\n'); width = std::max(width, (unsigned long) match.size()); line += 1_vl; } this->fss_match_view.set_selection(selected_line); this->fss_match_source.replace_with(al); this->fss_match_view.set_height( std::min(vis_line_t(matches.size()), 3_vl)); } this->fss_match_view.set_window(this->tss_view->get_window()); this->fss_match_view.set_y(rc->get_y() + 1); this->fss_match_view.set_x(rc->get_x() + rc->get_match_start()); this->fss_match_view.set_width(width + 3); this->fss_match_view.set_needs_update(); this->fss_match_view.reload_data(); } void filter_sub_source::rl_display_next(readline_curses* rc) { textview_curses& tc = this->fss_match_view; if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) { tc.set_top(0_vl); } else { tc.shift_top(tc.get_height()); } } void filter_sub_source::list_input_handle_scroll_out(listview_curses& lv) { lnav_data.ld_mode = ln_mode_t::PAGING; lnav_data.ld_filter_view.reload_data(); } bool filter_sub_source::text_handle_mouse(textview_curses& tc, mouse_event& me) { if (this->fss_editing) { return true; } if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 1, 3)) { this->list_input_handle_key(tc, ' '); } if (me.is_click_in(mouse_button_t::BUTTON_LEFT, 4, 7)) { this->list_input_handle_key(tc, 't'); } if (me.is_double_click_in(mouse_button_t::BUTTON_LEFT, line_range{25, -1})) { this->list_input_handle_key(tc, '\r'); } return true; }