1 /**
2  * Copyright (c) 2015, 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/injector.hh"
33 #include "base/humanize.network.hh"
34 #include "lnav.hh"
35 #include "lnav_util.hh"
36 #include "lnav_config.hh"
37 #include "sysclip.hh"
38 #include "vtab_module.hh"
39 #include "plain_text_source.hh"
40 #include "command_executor.hh"
41 #include "readline_curses.hh"
42 #include "readline_highlighters.hh"
43 #include "log_format_loader.hh"
44 #include "help_text_formatter.hh"
45 #include "sqlite-extension-func.hh"
46 #include "field_overlay_source.hh"
47 #include "yajlpp/yajlpp.hh"
48 #include "tailer/tailer.looper.hh"
49 #include "service_tags.hh"
50 
51 using namespace std;
52 
53 #define ABORT_MSG "(Press " ANSI_BOLD("CTRL+]") " to abort)"
54 
55 #define STR_HELPER(x) #x
56 #define STR(x) STR_HELPER(x)
57 
58 #define ANSI_RE(msg) ANSI_CSI "1;3" STR(COLOR_CYAN) "m" msg ANSI_NORM
59 #define ANSI_CLS(msg) ANSI_CSI "1;3" STR(COLOR_MAGENTA) "m" msg ANSI_NORM
60 #define ANSI_KW(msg) ANSI_CSI "3" STR(COLOR_BLUE) "m" msg ANSI_NORM
61 #define ANSI_REV(msg) ANSI_CSI "7m" msg ANSI_NORM
62 #define ANSI_STR(msg) ANSI_CSI "32m" msg ANSI_NORM
63 
64 const char *RE_HELP =
65     " "  ANSI_RE(".") "   Any character    "
66     " "     "a" ANSI_RE("|") "b   a or b        "
67     " " ANSI_RE("(?-i)") "   Case-sensitive search\n"
68 
69     " " ANSI_CLS("\\w") "  Word character   "
70     " "     "a" ANSI_RE("?") "    0 or 1 a's    "
71     " "                 ANSI_RE("$") "       End of string\n"
72 
73     " " ANSI_CLS("\\d") "  Digit            "
74     " "     "a" ANSI_RE("*") "    0 or more a's "
75     " " ANSI_RE("(") "..." ANSI_RE(")") "   Capture\n"
76 
77     " " ANSI_CLS("\\s") "  White space      "
78     " "     "a" ANSI_RE("+") "    1 or more a's "
79     " "                 ANSI_RE("^") "       Start of string\n"
80 
81     " " ANSI_RE("\\") "   Escape character "
82     " " ANSI_RE("[^") "ab" ANSI_RE("]") " " ANSI_BOLD("Not") " a or b    "
83     " " ANSI_RE("[") "ab" ANSI_RE("-") "d" ANSI_RE("]") "  Any of a, b, c, or d"
84 ;
85 
86 const char *RE_EXAMPLE =
87     ANSI_UNDERLINE("Examples") "\n"
88     "  abc" ANSI_RE("*") "       matches  "
89     ANSI_STR("'ab'") ", " ANSI_STR("'abc'") ", " ANSI_STR("'abccc'") "\n"
90 
91     "  key=" ANSI_RE("(\\w+)")
92     "  matches  key=" ANSI_REV("123") ", key=" ANSI_REV("abc") " and captures 123 and abc\n"
93 
94     "  " ANSI_RE("\\") "[abc" ANSI_RE("\\") "]    matches  " ANSI_STR("'[abc]'") "\n"
95 
96     "  " ANSI_RE("(?-i)") "ABC   matches  " ANSI_STR("'ABC'") " and " ANSI_UNDERLINE("not") " " ANSI_STR("'abc'")
97 ;
98 
99 const char *SQL_HELP =
100     " " ANSI_KW("SELECT") "  Select rows from a table      "
101     " " ANSI_KW("DELETE") "  Delete rows from a table\n"
102     " " ANSI_KW("INSERT") "  Insert rows into a table      "
103     " " ANSI_KW("UPDATE") "  Update rows in a table\n"
104     " " ANSI_KW("CREATE") "  Create a table/index          "
105     " " ANSI_KW("DROP") "    Drop a table/index\n"
106     " " ANSI_KW("ATTACH") "  Attach a SQLite database file "
107     " " ANSI_KW("DETACH") "  Detach a SQLite database"
108 ;
109 
110 const char *SQL_EXAMPLE =
111     ANSI_UNDERLINE("Examples") "\n"
112     "  SELECT * FROM %s WHERE log_level >= 'warning' LIMIT 10\n"
113     "  UPDATE %s SET log_mark = 1 WHERE log_line = log_top_line()\n"
114     "  SELECT * FROM logline LIMIT 10"
115 ;
116 
117 static const char *LNAV_CMD_PROMPT = "Enter an lnav command: " ABORT_MSG;
118 
rl_set_help()119 void rl_set_help()
120 {
121     switch (lnav_data.ld_mode) {
122         case LNM_SEARCH: {
123             lnav_data.ld_doc_source.replace_with(RE_HELP);
124             lnav_data.ld_example_source.replace_with(RE_EXAMPLE);
125             break;
126         }
127         case LNM_SQL: {
128             textview_curses &log_view = lnav_data.ld_views[LNV_LOG];
129             auto lss = (logfile_sub_source *) log_view.get_sub_source();
130             char example_txt[1024];
131             attr_line_t example_al;
132 
133             if (log_view.get_inner_height() > 0) {
134                 auto cl = lss->at(log_view.get_top());
135                 auto lf = lss->find(cl);
136                 auto format_name = lf->get_format()->get_name().get();
137 
138                 snprintf(example_txt, sizeof(example_txt),
139                          SQL_EXAMPLE,
140                          format_name,
141                          format_name);
142                 example_al.with_ansi_string(example_txt);
143                 readline_sqlite_highlighter(example_al, 0);
144             }
145 
146             lnav_data.ld_doc_source.replace_with(SQL_HELP);
147             lnav_data.ld_example_source.replace_with(example_al);
148             break;
149         }
150         default:
151             break;
152     }
153 }
154 
155 static
rl_sql_help(readline_curses * rc)156 bool rl_sql_help(readline_curses *rc)
157 {
158     attr_line_t al(rc->get_line_buffer());
159     const string_attrs_t &sa = al.get_attrs();
160     size_t x = rc->get_x();
161     bool has_doc = false;
162 
163     if (x > 0) {
164         x -= 1;
165     }
166 
167     annotate_sql_statement(al);
168 
169     auto avail_help = find_sql_help_for_line(al, x);
170 
171     if (!avail_help.empty()) {
172         size_t help_count = avail_help.size();
173         textview_curses &dtc = lnav_data.ld_doc_view;
174         textview_curses &etc = lnav_data.ld_example_view;
175         unsigned long doc_width, ex_width;
176         vis_line_t doc_height, ex_height;
177         attr_line_t doc_al, ex_al;
178 
179         dtc.get_dimensions(doc_height, doc_width);
180         etc.get_dimensions(ex_height, ex_width);
181 
182         for (const auto& ht : avail_help) {
183             format_help_text_for_term(*ht, min(70UL, doc_width), doc_al,
184                                       help_count > 1);
185             if (help_count == 1) {
186                 format_example_text_for_term(*ht,
187                                              eval_example,
188                                              min(70UL, ex_width), ex_al);
189             }
190         }
191 
192         if (!doc_al.empty()) {
193             lnav_data.ld_doc_source.replace_with(doc_al);
194             dtc.reload_data();
195 
196             lnav_data.ld_example_source.replace_with(ex_al);
197             etc.reload_data();
198 
199             has_doc = true;
200         }
201     }
202 
203     auto ident_iter = find_string_attr_containing(sa, &SQL_IDENTIFIER_ATTR, al.nearest_text(x));
204     if (ident_iter != sa.end()) {
205         auto ident = al.get_substring(ident_iter->sa_range);
206         auto intern_ident = intern_string::lookup(ident);
207         auto vtab = lnav_data.ld_vtab_manager->lookup_impl(intern_ident);
208         auto vtab_module_iter = vtab_module_ddls.find(intern_ident);
209         string ddl;
210 
211         if (vtab != nullptr) {
212             ddl = trim(vtab->get_table_statement());
213         } else if (vtab_module_iter != vtab_module_ddls.end()) {
214             ddl = vtab_module_iter->second;
215         } else {
216             auto table_ddl_iter = lnav_data.ld_table_ddl.find(ident);
217 
218             if (table_ddl_iter != lnav_data.ld_table_ddl.end()) {
219                 ddl = table_ddl_iter->second;
220             }
221         }
222 
223         if (!ddl.empty()) {
224             lnav_data.ld_preview_source.replace_with(ddl)
225                 .set_text_format(text_format_t::TF_SQL)
226                 .truncate_to(30);
227             lnav_data.ld_preview_status_source.get_description()
228                 .set_value("Definition for table -- %s",
229                            ident.c_str());
230         }
231     }
232 
233     return has_doc;
234 }
235 
rl_change(readline_curses * rc)236 void rl_change(readline_curses *rc)
237 {
238     static const std::set<std::string> COMMANDS_WITH_SQL = {
239         "filter-expr",
240         "mark-expr",
241     };
242 
243     textview_curses *tc = get_textview_for_mode(lnav_data.ld_mode);
244 
245     tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
246     tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
247     lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
248     lnav_data.ld_preview_source.clear();
249     lnav_data.ld_preview_status_source.get_description()
250         .set_cylon(false)
251         .clear();
252 
253     switch (lnav_data.ld_mode) {
254         case LNM_COMMAND: {
255             static string last_command;
256             static int generation = 0;
257 
258             string line = rc->get_line_buffer();
259             vector<string> args;
260             auto iter = lnav_commands.end();
261 
262             split_ws(line, args);
263 
264             if (args.empty()) {
265                 generation = 0;
266             } else if (args[0] != last_command) {
267                 last_command = args[0];
268                 generation = 0;
269             } else {
270                 generation += 1;
271             }
272 
273             auto os = tc->get_overlay_source();
274             if (!args.empty() && os != nullptr) {
275                 auto fos = dynamic_cast<field_overlay_source *>(os);
276 
277                 if (fos != nullptr) {
278                     if (generation == 0) {
279                         auto& top_ctx = fos->fos_contexts.top();
280 
281                         if (COMMANDS_WITH_SQL.count(args[0]) > 0) {
282                             top_ctx.c_prefix = ":";
283                             top_ctx.c_show = true;
284                             top_ctx.c_show_discovered = false;
285                         } else {
286                             top_ctx.c_prefix = ":";
287                             top_ctx.c_show = false;
288                         }
289                     }
290                 }
291             }
292 
293             if (!args.empty()) {
294                 iter = lnav_commands.find(args[0]);
295             }
296             if (iter == lnav_commands.end()) {
297                 lnav_data.ld_doc_source.clear();
298                 lnav_data.ld_example_source.clear();
299                 lnav_data.ld_preview_source.clear();
300                 lnav_data.ld_preview_status_source.get_description()
301                     .set_cylon(false)
302                     .clear();
303                 lnav_data.ld_bottom_source.set_prompt(LNAV_CMD_PROMPT);
304                 lnav_data.ld_bottom_source.grep_error("");
305             }
306             else if (args[0] == "config" && args.size() > 1) {
307                 yajlpp_parse_context ypc("input", &lnav_config_handlers);
308 
309                 ypc.set_path(args[1])
310                     .with_obj(lnav_config);
311                 ypc.update_callbacks();
312 
313                 if (ypc.ypc_current_handler != nullptr) {
314                     const json_path_handler_base *jph = ypc.ypc_current_handler;
315                     char help_text[1024];
316 
317                     snprintf(help_text, sizeof(help_text),
318                              ANSI_BOLD("%s %s") " -- %s    " ABORT_MSG,
319                              jph->jph_property.c_str(),
320                              jph->jph_synopsis,
321                              jph->jph_description);
322                     lnav_data.ld_bottom_source.set_prompt(help_text);
323                     lnav_data.ld_bottom_source.grep_error("");
324                 }
325                 else {
326                     lnav_data.ld_bottom_source.grep_error(
327                             "Unknown configuration option: " + args[1]);
328                 }
329             }
330             else if ((args[0] != "filter-expr" && args[0] != "mark-expr") ||
331                      !rl_sql_help(rc)) {
332                 readline_context::command_t &cmd = *iter->second;
333                 const help_text &ht = cmd.c_help;
334 
335                 if (ht.ht_name) {
336                     textview_curses &dtc = lnav_data.ld_doc_view;
337                     textview_curses &etc = lnav_data.ld_example_view;
338                     unsigned long width;
339                     vis_line_t height;
340                     attr_line_t al;
341 
342                     dtc.get_dimensions(height, width);
343                     format_help_text_for_term(ht, min(70UL, width), al);
344                     lnav_data.ld_doc_source.replace_with(al);
345                     dtc.set_needs_update();
346 
347                     al.clear();
348                     etc.get_dimensions(height, width);
349                     format_example_text_for_term(ht, eval_example, width, al);
350                     lnav_data.ld_example_source.replace_with(al);
351                     etc.set_needs_update();
352                 }
353 
354                 if (cmd.c_prompt != nullptr && generation == 0 &&
355                     trim(line) == args[0]) {
356                     string new_prompt = cmd.c_prompt(
357                         lnav_data.ld_exec_context, line);
358 
359                     if (!new_prompt.empty()) {
360                         rc->rewrite_line(line.length(), new_prompt);
361                     }
362                 }
363 
364                 lnav_data.ld_bottom_source.grep_error("");
365                 lnav_data.ld_status[LNS_BOTTOM].window_change();
366             }
367             break;
368         }
369         case LNM_EXEC: {
370             string line = rc->get_line_buffer();
371             size_t name_end = line.find(' ');
372             string script_name = line.substr(0, name_end);
373             auto& scripts = injector::get<available_scripts&>();
374             auto iter = scripts.as_scripts.find(script_name);
375 
376             if (iter == scripts.as_scripts.end() ||
377                 iter->second[0].sm_description.empty()) {
378                 lnav_data.ld_bottom_source.set_prompt(
379                         "Enter a script to execute: " ABORT_MSG);
380             }
381             else {
382                 struct script_metadata &meta = iter->second[0];
383                 char help_text[1024];
384 
385                 snprintf(help_text, sizeof(help_text),
386                          ANSI_BOLD("%s") " -- %s   " ABORT_MSG,
387                          meta.sm_synopsis.c_str(),
388                          meta.sm_description.c_str());
389                 lnav_data.ld_bottom_source.set_prompt(help_text);
390             }
391             break;
392         }
393         default:
394             break;
395     }
396 }
397 
rl_search_internal(readline_curses * rc,ln_mode_t mode,bool complete=false)398 static void rl_search_internal(readline_curses *rc, ln_mode_t mode, bool complete = false)
399 {
400     textview_curses *tc = get_textview_for_mode(mode);
401     string term_val;
402     string name;
403 
404     tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
405     tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
406     lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
407     tc->reload_data();
408 
409     switch (mode) {
410     case LNM_SEARCH:
411     case LNM_SEARCH_FILTERS:
412     case LNM_SEARCH_FILES:
413         name = "$search";
414         break;
415 
416     case LNM_CAPTURE:
417         require(0);
418         name = "$capture";
419         break;
420 
421     case LNM_COMMAND: {
422         lnav_data.ld_exec_context.ec_dry_run = true;
423 
424         lnav_data.ld_preview_generation += 1;
425         lnav_data.ld_preview_status_source.get_description()
426             .set_cylon(false)
427             .clear();
428         lnav_data.ld_preview_source.clear();
429         auto result = execute_command(lnav_data.ld_exec_context, rc->get_value());
430 
431         if (result.isOk()) {
432             auto msg = result.unwrap();
433 
434             if (msg.empty()) {
435                 lnav_data.ld_bottom_source.set_prompt(LNAV_CMD_PROMPT);
436                 lnav_data.ld_bottom_source.grep_error("");
437             } else {
438                 lnav_data.ld_bottom_source.set_prompt(msg);
439                 lnav_data.ld_bottom_source.grep_error("");
440             }
441         } else {
442             lnav_data.ld_bottom_source.set_prompt("");
443             lnav_data.ld_bottom_source.grep_error(result.unwrapErr());
444         }
445 
446         lnav_data.ld_preview_view.reload_data();
447 
448         lnav_data.ld_exec_context.ec_dry_run = false;
449         return;
450     }
451 
452     case LNM_SQL: {
453         term_val = trim(rc->get_value() + ";");
454 
455         if (!term_val.empty() && term_val[0] == '.') {
456             lnav_data.ld_bottom_source.grep_error("");
457         } else if (!sqlite3_complete(term_val.c_str())) {
458             lnav_data.ld_bottom_source.
459                 grep_error("sql error: incomplete statement");
460         } else {
461             auto_mem<sqlite3_stmt> stmt(sqlite3_finalize);
462             int retcode;
463 
464             retcode = sqlite3_prepare_v2(lnav_data.ld_db,
465                                          rc->get_value().c_str(),
466                                          -1,
467                                          stmt.out(),
468                                          nullptr);
469             if (retcode != SQLITE_OK) {
470                 const char *errmsg = sqlite3_errmsg(lnav_data.ld_db);
471 
472                 lnav_data.ld_bottom_source.
473                     grep_error(fmt::format("sql error: {}", errmsg));
474             } else {
475                 lnav_data.ld_bottom_source.grep_error("");
476             }
477         }
478 
479         if (!rl_sql_help(rc)) {
480             rl_set_help();
481             lnav_data.ld_preview_source.clear();
482         }
483         return;
484     }
485 
486     case LNM_PAGING:
487     case LNM_FILTER:
488     case LNM_FILES:
489     case LNM_EXEC:
490     case LNM_USER:
491         return;
492     }
493 
494     if (!complete) {
495         tc->set_top(lnav_data.ld_search_start_line);
496     }
497     tc->execute_search(rc->get_value());
498 }
499 
rl_search(readline_curses * rc)500 void rl_search(readline_curses *rc)
501 {
502     textview_curses *tc = get_textview_for_mode(lnav_data.ld_mode);
503 
504     rl_search_internal(rc, lnav_data.ld_mode);
505     tc->set_follow_search_for(0, {});
506 }
507 
lnav_rl_abort(readline_curses * rc)508 void lnav_rl_abort(readline_curses *rc)
509 {
510     textview_curses *tc = get_textview_for_mode(lnav_data.ld_mode);
511 
512     lnav_data.ld_bottom_source.set_prompt("");
513     lnav_data.ld_example_source.clear();
514     lnav_data.ld_doc_source.clear();
515     lnav_data.ld_preview_status_source.get_description()
516         .set_cylon(false)
517         .clear();
518     lnav_data.ld_preview_source.clear();
519     tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
520     tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
521     lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
522 
523     vector<string> errors;
524     lnav_config = rollback_lnav_config;
525     reload_config(errors);
526 
527     lnav_data.ld_bottom_source.grep_error("");
528     switch (lnav_data.ld_mode) {
529     case LNM_SEARCH:
530         tc->set_top(lnav_data.ld_search_start_line);
531         tc->revert_search();
532         break;
533     case LNM_SQL:
534         tc->reload_data();
535         break;
536     default:
537         break;
538     }
539     lnav_data.ld_rl_view->set_value("");
540     lnav_data.ld_mode = LNM_PAGING;
541 }
542 
rl_callback_int(readline_curses * rc,bool is_alt)543 static void rl_callback_int(readline_curses *rc, bool is_alt)
544 {
545     textview_curses *tc = get_textview_for_mode(lnav_data.ld_mode);
546     exec_context &ec = lnav_data.ld_exec_context;
547     string alt_msg;
548 
549     lnav_data.ld_bottom_source.set_prompt("");
550     lnav_data.ld_doc_source.clear();
551     lnav_data.ld_example_source.clear();
552     lnav_data.ld_preview_status_source.get_description()
553         .set_cylon(false)
554         .clear();
555     lnav_data.ld_preview_source.clear();
556     tc->get_highlights().erase({highlight_source_t::PREVIEW, "preview"});
557     tc->get_highlights().erase({highlight_source_t::PREVIEW, "bodypreview"});
558     lnav_data.ld_log_source.set_preview_sql_filter(nullptr);
559 
560     auto new_mode = LNM_PAGING;
561 
562     switch (lnav_data.ld_mode) {
563         case LNM_SEARCH_FILTERS:
564             new_mode = LNM_FILTER;
565             break;
566         case LNM_SEARCH_FILES:
567             new_mode = LNM_FILES;
568             break;
569         default:
570             break;
571     }
572 
573     auto old_mode = std::exchange(lnav_data.ld_mode, new_mode);
574     switch (old_mode) {
575     case LNM_PAGING:
576     case LNM_FILTER:
577     case LNM_FILES:
578         require(0);
579         break;
580 
581     case LNM_COMMAND:
582         rc->set_alt_value("");
583         rc->set_value(execute_command(ec, rc->get_value())
584                           .map(ok_prefix)
585                           .orElse(err_to_ok).unwrap());
586         break;
587 
588         case LNM_USER:
589             rc->set_alt_value("");
590             ec.ec_local_vars.top()["value"] = rc->get_value();
591             rc->set_value("");
592             break;
593 
594     case LNM_SEARCH:
595     case LNM_SEARCH_FILTERS:
596     case LNM_SEARCH_FILES:
597     case LNM_CAPTURE:
598         rl_search_internal(rc, old_mode, true);
599         if (!rc->get_value().empty()) {
600             auto_mem<FILE> pfile(pclose);
601             vis_bookmarks &bm = tc->get_bookmarks();
602             const auto &bv = bm[&textview_curses::BM_SEARCH];
603             vis_line_t vl = is_alt ? bv.prev(tc->get_top()) :
604                 bv.next(tc->get_top());
605 
606             pfile = sysclip::open(sysclip::type_t::FIND);
607             if (pfile.in() != nullptr) {
608                 fprintf(pfile, "%s", rc->get_value().c_str());
609             }
610             if (vl != -1_vl) {
611                 tc->set_top(vl);
612             } else {
613                 tc->set_follow_search_for(2000, [tc, is_alt, &bm]() {
614                     if (bm[&textview_curses::BM_SEARCH].empty()) {
615                         return false;
616                     }
617 
618                     if (is_alt && tc->is_searching()) {
619                         return false;
620                     }
621 
622                     vis_line_t first_hit;
623 
624                     if (is_alt) {
625                         first_hit = bm[&textview_curses::BM_SEARCH].prev(
626                             vis_line_t(tc->get_top()));
627                     } else {
628                         first_hit = bm[&textview_curses::BM_SEARCH].next(
629                             vis_line_t(tc->get_top() - 1));
630                     }
631                     if (first_hit != -1) {
632                         if (first_hit > 0) {
633                             --first_hit;
634                         }
635                         tc->set_top(first_hit);
636                     }
637 
638                     return true;
639                 });
640             }
641             rc->set_value("search: " + rc->get_value());
642             rc->set_alt_value(HELP_MSG_2(
643                                   n, N,
644                                   "to move forward/backward through search results"));
645         }
646         break;
647 
648     case LNM_SQL: {
649         auto result = execute_sql(ec, rc->get_value(), alt_msg);
650         db_label_source &dls = lnav_data.ld_db_row_source;
651         string prompt;
652 
653         if (result.isOk()) {
654             auto msg = result.unwrap();
655 
656             if (!msg.empty()) {
657                 prompt = ok_prefix("SQL Result: " + msg);
658                 if (dls.dls_rows.size() > 1) {
659                     ensure_view(&lnav_data.ld_views[LNV_DB]);
660                 }
661             }
662         } else {
663             prompt = result.orElse(err_to_ok).unwrap();
664         }
665 
666         rc->set_value(prompt);
667         rc->set_alt_value(alt_msg);
668         break;
669     }
670 
671     case LNM_EXEC: {
672         auto_mem<FILE> tmpout(fclose);
673 
674         tmpout = std::tmpfile();
675 
676         if (!tmpout) {
677             rc->set_value("Unable to open temporary output file: " +
678                           string(strerror(errno)));
679         } else {
680             auto fd_copy = auto_fd::dup_of(fileno(tmpout));
681             char desc[256], timestamp[32];
682             time_t current_time = time(nullptr);
683             string path_and_args = rc->get_value();
684 
685             {
686                 exec_context::output_guard og(ec, "tmp", std::make_pair(tmpout.release(), fclose));
687 
688                 string result = execute_file(ec, path_and_args)
689                     .map(ok_prefix)
690                     .orElse(err_to_ok).unwrap();
691                 string::size_type lf_index = result.find('\n');
692                 if (lf_index != string::npos) {
693                     result = result.substr(0, lf_index);
694                 }
695                 rc->set_value(result);
696             }
697 
698             struct stat st;
699 
700             if (fstat(fd_copy, &st) != -1 && st.st_size > 0) {
701                 strftime(timestamp, sizeof(timestamp),
702                          "%a %b %d %H:%M:%S %Z",
703                          localtime(&current_time));
704                 snprintf(desc, sizeof(desc),
705                          "Output of %s (%s)",
706                          path_and_args.c_str(),
707                          timestamp);
708                 lnav_data.ld_active_files.fc_file_names[desc]
709                     .with_fd(fd_copy)
710                     .with_include_in_session(false)
711                     .with_detect_format(false);
712                 lnav_data.ld_files_to_front.emplace_back(desc, 0);
713 
714                 if (lnav_data.ld_rl_view != nullptr) {
715                     lnav_data.ld_rl_view->set_alt_value(
716                         HELP_MSG_1(X, "to close the file"));
717                 }
718             }
719         }
720         break;
721     }
722     }
723 }
724 
rl_callback(readline_curses * rc)725 void rl_callback(readline_curses *rc)
726 {
727     rl_callback_int(rc, false);
728 }
729 
rl_alt_callback(readline_curses * rc)730 void rl_alt_callback(readline_curses *rc)
731 {
732     rl_callback_int(rc, true);
733 }
734 
rl_display_matches(readline_curses * rc)735 void rl_display_matches(readline_curses *rc)
736 {
737     const auto &matches = rc->get_matches();
738     auto &tc = lnav_data.ld_match_view;
739     unsigned long width;
740     __attribute((unused))
741     unsigned long height;
742     int max_len, cols;
743 
744     getmaxyx(lnav_data.ld_window, height, width);
745 
746     max_len = rc->get_max_match_length() + 2;
747     cols = max(1UL, width / max_len);
748 
749     if (matches.empty()) {
750         lnav_data.ld_match_source.clear();
751     }
752     else {
753         string current_match = rc->get_match_string();
754         int curr_col = 0;
755         attr_line_t al;
756         bool add_nl = false;
757 
758         for (const auto &match : matches) {
759             if (add_nl) {
760                 al.append(1, '\n');
761                 add_nl = false;
762             }
763             if (match == current_match) {
764                 al.append(match, &view_curses::VC_STYLE, A_REVERSE);
765             } else {
766                 al.append(match);
767             }
768             curr_col += 1;
769             if (curr_col < cols) {
770                 int padding = max_len - match.size();
771 
772                 al.append(padding, ' ');
773             } else {
774                 curr_col = 0;
775                 add_nl = true;
776             }
777         }
778         lnav_data.ld_match_source.replace_with(al);
779     }
780 
781     tc.reload_data();
782 }
783 
rl_display_next(readline_curses * rc)784 void rl_display_next(readline_curses *rc)
785 {
786     textview_curses &tc = lnav_data.ld_match_view;
787 
788     if (tc.get_top() >= (tc.get_top_for_last_row() - 1)) {
789         tc.set_top(0_vl);
790     }
791     else {
792         tc.shift_top(tc.get_height());
793     }
794 }
795 
rl_completion_request(readline_curses * rc)796 void rl_completion_request(readline_curses *rc)
797 {
798     isc::to<tailer::looper&, services::remote_tailer_t>()
799         .send([rc](auto &tlooper) {
800             auto rp_opt = humanize::network::path::from_str(
801                 rc->get_remote_complete_path());
802             if (rp_opt) {
803                 tlooper.complete_path(*rp_opt);
804             }
805         });
806 }
807