summaryrefslogtreecommitdiffstats
path: root/src/readline_curses.cc
diff options
context:
space:
mode:
Diffstat (limited to 'src/readline_curses.cc')
-rw-r--r--src/readline_curses.cc1508
1 files changed, 1508 insertions, 0 deletions
diff --git a/src/readline_curses.cc b/src/readline_curses.cc
new file mode 100644
index 0000000..ed91b3c
--- /dev/null
+++ b/src/readline_curses.cc
@@ -0,0 +1,1508 @@
+/**
+ * Copyright (c) 2007-2012, 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.
+ *
+ * @file readline_curses.cc
+ */
+
+#include <errno.h>
+#include <signal.h>
+#include <stdlib.h>
+#include <string.h>
+#include <sys/socket.h>
+#include <sys/wait.h>
+#include <unistd.h>
+
+#include "config.h"
+
+#ifdef HAVE_PTY_H
+# include <pty.h>
+#endif
+
+#ifdef HAVE_UTIL_H
+# include <util.h>
+#endif
+
+#ifdef HAVE_LIBUTIL_H
+# include <libutil.h>
+#endif
+
+#include <string>
+#include <utility>
+
+#include "base/ansi_scrubber.hh"
+#include "base/auto_mem.hh"
+#include "base/lnav_log.hh"
+#include "base/paths.hh"
+#include "base/string_util.hh"
+#include "fmt/format.h"
+#include "fts_fuzzy_match.hh"
+#include "lnav_util.hh"
+#include "readline_curses.hh"
+#include "shlex.hh"
+#include "spookyhash/SpookyV2.h"
+
+static int got_line = 0;
+static int got_abort = 0;
+static bool alt_done = 0;
+static sig_atomic_t got_timeout = 0;
+static sig_atomic_t got_winch = 0;
+static readline_curses* child_this;
+static sig_atomic_t looping = 1;
+static const int HISTORY_SIZE = 256;
+static int completion_start;
+static const int FUZZY_PEER_THRESHOLD = 30;
+
+static const char* RL_INIT[] = {
+ /*
+ * XXX Need to keep the input on a single line since the display screws
+ * up if it wraps around.
+ */
+ "set horizontal-scroll-mode on",
+ "set bell-style none",
+ "set show-all-if-ambiguous on",
+ "set show-all-if-unmodified on",
+ "set menu-complete-display-prefix on",
+ "TAB: menu-complete",
+ "\"\\e[Z\": menu-complete-backward",
+};
+
+readline_context* readline_context::loaded_context;
+std::set<std::string>* readline_context::arg_possibilities;
+static std::string last_match_str;
+static bool last_match_str_valid;
+static bool arg_needs_shlex;
+static nonstd::optional<std::string> rewrite_line_start;
+
+static void
+sigalrm(int sig)
+{
+ got_timeout = 1;
+}
+
+static void
+sigwinch(int sig)
+{
+ got_winch = 1;
+}
+
+static void
+sigterm(int sig)
+{
+ looping = 0;
+}
+
+static void
+line_ready_tramp(char* line)
+{
+ child_this->line_ready(line);
+ got_line = 1;
+ rl_callback_handler_remove();
+}
+
+static int
+sendall(int sock, const char* buf, size_t len)
+{
+ off_t offset = 0;
+
+ while (len > 0) {
+ int rc = send(sock, &buf[offset], len, 0);
+
+ if (rc == -1) {
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ return -1;
+ }
+ } else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ return 0;
+}
+
+static int
+sendstring(int sock, const char* buf, size_t len)
+{
+ if (sendall(sock, (char*) &len, sizeof(len)) == -1) {
+ return -1;
+ } else if (sendall(sock, buf, len) == -1) {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+sendcmd(int sock, char cmd, const char* buf, size_t len)
+{
+ size_t total_len = len + 2;
+ char prefix[2] = {cmd, ':'};
+
+ if (sendall(sock, (char*) &total_len, sizeof(total_len)) == -1) {
+ return -1;
+ } else if (sendall(sock, prefix, sizeof(prefix)) == -1
+ || sendall(sock, buf, len) == -1)
+ {
+ return -1;
+ }
+
+ return 0;
+}
+
+static int
+recvall(int sock, char* buf, size_t len)
+{
+ off_t offset = 0;
+
+ while (len > 0) {
+ ssize_t rc = recv(sock, &buf[offset], len, 0);
+
+ if (rc == -1) {
+ switch (errno) {
+ case EAGAIN:
+ case EINTR:
+ break;
+ default:
+ return -1;
+ }
+ } else if (rc == 0) {
+ errno = EIO;
+ return -1;
+ } else {
+ len -= rc;
+ offset += rc;
+ }
+ }
+
+ return 0;
+}
+
+static ssize_t
+recvstring(int sock, char* buf, size_t len)
+{
+ ssize_t retval;
+
+ if (recvall(sock, (char*) &retval, sizeof(retval)) == -1) {
+ return -1;
+ } else if (retval > (ssize_t) len) {
+ return -1;
+ } else if (recvall(sock, buf, retval) == -1) {
+ return -1;
+ }
+
+ return retval;
+}
+
+char*
+readline_context::completion_generator(const char* text_in, int state)
+{
+ static std::vector<std::string> matches;
+
+ std::vector<std::string> long_matches;
+ char* retval = nullptr;
+
+ if (state == 0) {
+ auto text_str = std::string(text_in);
+
+ if (arg_needs_shlex) {
+ shlex arg_lexer(text_str);
+ std::map<std::string, std::string> scope;
+ std::string result;
+
+ if (arg_lexer.eval(result, scope)) {
+ text_str = result;
+ }
+ }
+
+ matches.clear();
+ if (arg_possibilities != nullptr) {
+ for (const auto& poss : (*arg_possibilities)) {
+ auto cmpfunc
+ = (loaded_context->is_case_sensitive() ? strncmp
+ : strncasecmp);
+ auto poss_str = poss.c_str();
+
+ // Check for an exact match and for the quoted version.
+ if (cmpfunc(text_str.c_str(), poss_str, text_str.length()) == 0
+ || ((strchr(loaded_context->rc_quote_chars, poss_str[0])
+ != nullptr)
+ && cmpfunc(text_str.c_str(),
+ &poss_str[1],
+ text_str.length())
+ == 0))
+ {
+ auto poss_slash_count
+ = std::count(poss.begin(), poss.end(), '/');
+
+ if (endswith(poss, "/")) {
+ poss_slash_count -= 1;
+ }
+ if (std::count(text_str.begin(), text_str.end(), '/')
+ == poss_slash_count)
+ {
+ matches.emplace_back(poss);
+ } else {
+ long_matches.emplace_back(poss);
+ }
+ }
+ }
+
+ if (matches.empty()) {
+ matches = std::move(long_matches);
+ }
+
+ if (matches.empty()) {
+ std::vector<std::pair<int, std::string>> fuzzy_matches;
+ std::vector<std::pair<int, std::string>> fuzzy_long_matches;
+
+ for (const auto& poss : (*arg_possibilities)) {
+ std::string poss_str = tolower(poss);
+ int score = 0;
+
+ if (fts::fuzzy_match(
+ text_str.c_str(), poss_str.c_str(), score)
+ && score > 0)
+ {
+ if (score <= 0) {
+ continue;
+ }
+
+ auto poss_slash_count
+ = std::count(poss_str.begin(), poss_str.end(), '/');
+ if (endswith(poss, "/")) {
+ poss_slash_count -= 1;
+ }
+ if (std::count(text_str.begin(), text_str.end(), '/')
+ == poss_slash_count)
+ {
+ fuzzy_matches.emplace_back(score, poss);
+ } else {
+ fuzzy_long_matches.emplace_back(score, poss);
+ }
+ }
+ }
+
+ if (fuzzy_matches.empty()) {
+ fuzzy_matches = std::move(fuzzy_long_matches);
+ }
+
+ if (!fuzzy_matches.empty()) {
+ stable_sort(
+ begin(fuzzy_matches),
+ end(fuzzy_matches),
+ [](auto l, auto r) { return r.first < l.first; });
+
+ int highest = fuzzy_matches[0].first;
+
+ for (const auto& pair : fuzzy_matches) {
+ if (highest - pair.first < FUZZY_PEER_THRESHOLD) {
+ matches.push_back(pair.second);
+ } else {
+ break;
+ }
+ }
+ }
+ }
+ }
+
+ if (matches.size() == 1) {
+ if (text_str == matches[0]) {
+ matches.pop_back();
+ }
+
+ last_match_str_valid = false;
+ if (sendstring(
+ child_this->rc_command_pipe[readline_curses::RCF_SLAVE],
+ "m:0:0:0",
+ 7)
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+ }
+
+ if (!matches.empty()) {
+ retval = strdup(matches.back().c_str());
+ matches.pop_back();
+ }
+
+ return retval;
+}
+
+char**
+readline_context::attempted_completion(const char* text, int start, int end)
+{
+ char** retval = nullptr;
+
+ completion_start = start;
+ if (start == 0
+ && loaded_context->rc_possibilities.find("__command")
+ != loaded_context->rc_possibilities.end())
+ {
+ arg_possibilities = &loaded_context->rc_possibilities["__command"];
+ arg_needs_shlex = false;
+ rl_completion_append_character = loaded_context->rc_append_character;
+ } else {
+ char* space;
+ std::string cmd;
+ std::vector<std::string> prefix;
+ int point = rl_point;
+ while (point > 0 && rl_line_buffer[point] != ' ') {
+ point -= 1;
+ }
+ shlex lexer(rl_line_buffer, point);
+ std::map<std::string, std::string> scope;
+
+ arg_possibilities = nullptr;
+ rl_completion_append_character = 0;
+ if (lexer.split(prefix, scope)) {
+ auto prefix2
+ = fmt::format(FMT_STRING("{}"), fmt::join(prefix, "\x1f"));
+ auto prefix_iter = loaded_context->rc_prefixes.find(prefix2);
+
+ if (prefix_iter != loaded_context->rc_prefixes.end()) {
+ arg_possibilities
+ = &(loaded_context->rc_possibilities[prefix_iter->second]);
+ arg_needs_shlex = false;
+ }
+ }
+
+ if (arg_possibilities == nullptr) {
+ space = strchr(rl_line_buffer, ' ');
+ if (space == nullptr) {
+ space = rl_line_buffer + strlen(rl_line_buffer);
+ }
+ cmd = std::string(rl_line_buffer, space - rl_line_buffer);
+
+ auto iter = loaded_context->rc_prototypes.find(cmd);
+
+ if (iter == loaded_context->rc_prototypes.end()) {
+ if (loaded_context->rc_possibilities.find("*")
+ != loaded_context->rc_possibilities.end())
+ {
+ arg_possibilities = &loaded_context->rc_possibilities["*"];
+ arg_needs_shlex = false;
+ rl_completion_append_character
+ = loaded_context->rc_append_character;
+ }
+ } else {
+ std::vector<std::string>& proto
+ = loaded_context->rc_prototypes[cmd];
+
+ if (proto.empty()) {
+ arg_possibilities = nullptr;
+ } else if (proto[0] == "filename") {
+ shlex fn_lexer(rl_line_buffer, rl_point);
+ std::vector<std::string> fn_list;
+ int found = 0;
+
+ fn_lexer.split(fn_list, scope);
+
+ const auto& last_fn = fn_list.size() <= 1 ? ""
+ : fn_list.back();
+
+ if (last_fn.find(':') != std::string::npos) {
+ auto rp_iter = loaded_context->rc_possibilities.find(
+ "remote-path");
+ if (rp_iter != loaded_context->rc_possibilities.end()) {
+ for (const auto& poss : rp_iter->second) {
+ if (startswith(poss, last_fn.c_str())) {
+ found += 1;
+ }
+ }
+ if (found) {
+ arg_possibilities = &rp_iter->second;
+ arg_needs_shlex = false;
+ }
+ }
+ if (!found || (endswith(last_fn, "/") && found == 1)) {
+ char msg[2048];
+
+ snprintf(
+ msg, sizeof(msg), "\t:%s", last_fn.c_str());
+ sendstring(child_this->rc_command_pipe[1],
+ msg,
+ strlen(msg));
+ }
+ }
+ if (!found) {
+ static std::set<std::string> file_name_set;
+
+ file_name_set.clear();
+ auto_mem<char> completed_fn;
+ int fn_state = 0;
+ auto recent_netlocs_iter
+ = loaded_context->rc_possibilities.find(
+ "recent-netlocs");
+
+ if (recent_netlocs_iter
+ != loaded_context->rc_possibilities.end())
+ {
+ file_name_set.insert(
+ recent_netlocs_iter->second.begin(),
+ recent_netlocs_iter->second.end());
+ }
+ while ((completed_fn = rl_filename_completion_function(
+ last_fn.c_str(), fn_state))
+ != nullptr)
+ {
+ file_name_set.insert(completed_fn.in());
+ fn_state += 1;
+ }
+ arg_possibilities = &file_name_set;
+ arg_needs_shlex = true;
+ }
+ } else {
+ arg_possibilities
+ = &(loaded_context->rc_possibilities[proto[0]]);
+ arg_needs_shlex = false;
+ }
+ }
+ }
+ }
+
+ retval = rl_completion_matches(text, completion_generator);
+ if (retval == nullptr) {
+ rl_attempted_completion_over = 1;
+ }
+
+ return retval;
+}
+
+static int
+rubout_char_or_abort(int count, int key)
+{
+ if (rl_line_buffer[0] == '\0') {
+ rl_done = true;
+ got_abort = 1;
+ got_line = 0;
+ return 0;
+ } else {
+ return rl_rubout(count, '\b');
+ }
+}
+
+static int
+alt_done_func(int count, int key)
+{
+ alt_done = true;
+ rl_newline(count, key);
+ return 0;
+}
+
+int
+readline_context::command_complete(int count, int key)
+{
+ if (loaded_context->rc_possibilities.find("__command")
+ != loaded_context->rc_possibilities.end())
+ {
+ char* space = strchr(rl_line_buffer, ' ');
+
+ if (space == nullptr) {
+ return rl_menu_complete(count, key);
+ }
+ }
+ return rl_insert(count, key);
+}
+
+readline_context::readline_context(std::string name,
+ readline_context::command_map_t* commands,
+ bool case_sensitive)
+ : rc_name(std::move(name)), rc_case_sensitive(case_sensitive),
+ rc_quote_chars("\"'"), rc_highlighter(nullptr)
+{
+ if (commands != nullptr) {
+ command_map_t::iterator iter;
+
+ for (iter = commands->begin(); iter != commands->end(); ++iter) {
+ std::string cmd = iter->first;
+
+ this->rc_possibilities["__command"].insert(cmd);
+ iter->second->c_func(
+ INIT_EXEC_CONTEXT, cmd, this->rc_prototypes[cmd]);
+ }
+ }
+
+ memset(&this->rc_history, 0, sizeof(this->rc_history));
+ history_set_history_state(&this->rc_history);
+
+ auto config_dir = lnav::paths::dotlnav();
+ auto hpath = (config_dir / this->rc_name).string() + ".history";
+ read_history(hpath.c_str());
+ this->save();
+
+ this->rc_append_character = ' ';
+}
+
+void
+readline_context::load()
+{
+ char buffer[128];
+
+ rl_completer_word_break_characters = (char*) " \t\n|()"; /* XXX */
+ /*
+ * XXX Need to keep the input on a single line since the display screws
+ * up if it wraps around.
+ */
+ snprintf(buffer,
+ sizeof(buffer),
+ "set completion-ignore-case %s",
+ this->rc_case_sensitive ? "off" : "on");
+ rl_parse_and_bind(buffer); /* NOTE: buffer is modified */
+
+ loaded_context = this;
+ rl_attempted_completion_function = attempted_completion;
+ history_set_history_state(&this->rc_history);
+ for (auto& rc_var : this->rc_vars) {
+ *(rc_var.rv_dst.ch) = (char*) rc_var.rv_val.ch;
+ }
+}
+
+void
+readline_context::save()
+{
+ HISTORY_STATE* hs = history_get_history_state();
+
+ this->rc_history = *hs;
+ free(hs);
+ hs = nullptr;
+}
+
+readline_curses::readline_curses(
+ std::shared_ptr<pollable_supervisor> supervisor)
+ : pollable(supervisor, pollable::category::interactive),
+ rc_focus(noop_func{}), rc_change(noop_func{}), rc_perform(noop_func{}),
+ rc_alt_perform(noop_func{}), rc_timeout(noop_func{}),
+ rc_abort(noop_func{}), rc_display_match(noop_func{}),
+ rc_display_next(noop_func{}), rc_blur(noop_func{}),
+ rc_completion_request(noop_func{})
+{
+}
+
+readline_curses::~readline_curses()
+{
+ this->rc_pty[RCF_MASTER].reset();
+ this->rc_command_pipe[RCF_MASTER].reset();
+ if (this->rc_child == 0) {
+ _exit(0);
+ } else if (this->rc_child > 0) {
+ int status;
+
+ log_debug("terminating readline child %d", this->rc_child);
+ log_perror(kill(this->rc_child, SIGTERM));
+ this->rc_child = -1;
+
+ while (wait(&status) < 0 && (errno == EINTR)) {
+ ;
+ }
+ log_debug(" child %d has exited", this->rc_child);
+ }
+}
+
+void
+readline_curses::store_matches(char** matches, int num_matches, int max_len)
+{
+ static int match_index = 0;
+ char msg[64];
+ int rc;
+
+ max_len = 0;
+ for (int lpc = 0; lpc <= num_matches; lpc++) {
+ max_len = std::max(max_len, (int) strlen(matches[lpc]));
+ }
+
+ if (last_match_str_valid && strcmp(last_match_str.c_str(), matches[0]) == 0)
+ {
+ match_index += 1;
+ rc = snprintf(msg, sizeof(msg), "n:%d", match_index);
+
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE], msg, rc) == -1) {
+ _exit(1);
+ }
+ } else {
+ match_index = 0;
+ rc = snprintf(msg,
+ sizeof(msg),
+ "m:%d:%d:%d",
+ completion_start,
+ num_matches,
+ max_len);
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE], msg, rc) == -1) {
+ _exit(1);
+ }
+ for (int lpc = 1; lpc <= num_matches; lpc++) {
+ if (sendstring(child_this->rc_command_pipe[RCF_SLAVE],
+ matches[lpc],
+ strlen(matches[lpc]))
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+
+ last_match_str = matches[0];
+ last_match_str_valid = true;
+ }
+}
+
+void
+readline_curses::start()
+{
+ if (this->rc_child > 0) {
+ return;
+ }
+
+ struct winsize ws;
+ int sp[2];
+
+ if (socketpair(PF_UNIX, SOCK_STREAM, 0, sp) < 0) {
+ throw error(errno);
+ }
+
+ this->rc_command_pipe[RCF_MASTER] = sp[RCF_MASTER];
+ this->rc_command_pipe[RCF_SLAVE] = sp[RCF_SLAVE];
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == -1) {
+ throw error(errno);
+ }
+
+ if (openpty(this->rc_pty[RCF_MASTER].out(),
+ this->rc_pty[RCF_SLAVE].out(),
+ nullptr,
+ nullptr,
+ &ws)
+ < 0)
+ {
+ perror("error: failed to open terminal(openpty)");
+ throw error(errno);
+ }
+
+ if ((this->rc_child = fork()) == -1) {
+ throw error(errno);
+ }
+
+ if (this->rc_child != 0) {
+ this->rc_command_pipe[RCF_SLAVE].reset();
+ this->rc_pty[RCF_SLAVE].reset();
+ return;
+ }
+
+ {
+ char buffer[1024];
+
+ this->rc_command_pipe[RCF_MASTER].reset();
+ this->rc_pty[RCF_MASTER].reset();
+
+ signal(SIGALRM, sigalrm);
+ signal(SIGWINCH, sigwinch);
+ signal(SIGINT, sigterm);
+ signal(SIGTERM, sigterm);
+
+ dup2(this->rc_pty[RCF_SLAVE], STDIN_FILENO);
+ dup2(this->rc_pty[RCF_SLAVE], STDOUT_FILENO);
+
+ setenv("TERM", "vt52", 1);
+
+ rl_initialize();
+ using_history();
+ stifle_history(HISTORY_SIZE);
+
+ rl_add_defun("rubout-char-or-abort", rubout_char_or_abort, '\b');
+ rl_add_defun("alt-done", alt_done_func, '\x0a');
+ // rl_add_defun("command-complete", readline_context::command_complete,
+ // ' ');
+
+ for (const auto* init_cmd : RL_INIT) {
+ snprintf(buffer, sizeof(buffer), "%s", init_cmd);
+ rl_parse_and_bind(buffer); /* NOTE: buffer is modified */
+ }
+
+ child_this = this;
+ }
+
+ std::map<int, readline_context*>::iterator current_context;
+ int maxfd;
+
+ require(!this->rc_contexts.empty());
+
+ rl_completion_display_matches_hook = store_matches;
+
+ current_context = this->rc_contexts.end();
+
+ maxfd = std::max(STDIN_FILENO, this->rc_command_pipe[RCF_SLAVE].get());
+
+ while (looping) {
+ fd_set ready_rfds;
+ int rc;
+
+ FD_ZERO(&ready_rfds);
+ if (current_context != this->rc_contexts.end()) {
+ FD_SET(STDIN_FILENO, &ready_rfds);
+ }
+ FD_SET(this->rc_command_pipe[RCF_SLAVE], &ready_rfds);
+
+ rc = select(maxfd + 1, &ready_rfds, nullptr, nullptr, nullptr);
+ if (rc < 0) {
+ switch (errno) {
+ case EINTR:
+ break;
+ }
+ } else {
+ if (FD_ISSET(STDIN_FILENO, &ready_rfds)) {
+ static uint64_t last_h1, last_h2;
+
+ struct itimerval itv;
+
+ itv.it_value.tv_sec = 0;
+ itv.it_value.tv_usec = KEY_TIMEOUT;
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 0;
+ setitimer(ITIMER_REAL, &itv, nullptr);
+
+ rl_callback_read_char();
+ if (RL_ISSTATE(RL_STATE_DONE) && !got_line) {
+ got_line = 1;
+ this->line_ready("");
+ rl_callback_handler_remove();
+ } else {
+ uint64_t h1 = 1, h2 = 2;
+
+ if (rl_last_func == readline_context::command_complete) {
+ rl_last_func = rl_menu_complete;
+ }
+
+ bool complete_done
+ = (rl_last_func != rl_menu_complete
+ && rl_last_func != rl_backward_menu_complete);
+
+ if (complete_done) {
+ last_match_str_valid = false;
+ } else if (rewrite_line_start
+ && !startswith(rl_line_buffer,
+ rewrite_line_start->c_str()))
+ {
+ // If the line was rewritten, the extra text stays on
+ // the screen, so we need to delete it, make sure the
+ // append character is there, and redisplay. For
+ // example, ':co<TAB>' will complete ':comment' and
+ // append the current comment. Pressing '<TAB>' again
+ // would switch to ':config' and the comment text would
+ // be left on the display.
+ rl_delete_text(rl_point, rl_end);
+ if (rl_completion_append_character
+ && rl_line_buffer[rl_point]
+ != rl_completion_append_character)
+ {
+ char buf[2]
+ = {(char) rl_completion_append_character, '\0'};
+
+ rl_insert_text(buf);
+ }
+ rl_redisplay();
+ }
+ rewrite_line_start = nonstd::nullopt;
+
+ SpookyHash::Hash128(rl_line_buffer, rl_end, &h1, &h2);
+
+ if (h1 == last_h1 && h2 == last_h2) {
+ // do nothing
+ } else if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ complete_done ? 'l' : 'c',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ last_h1 = h1;
+ last_h2 = h2;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE], 'w', "", 0)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ }
+ }
+ if (FD_ISSET(this->rc_command_pipe[RCF_SLAVE], &ready_rfds)) {
+ char msg[1024 + 1];
+
+ if ((rc = recvstring(this->rc_command_pipe[RCF_SLAVE],
+ msg,
+ sizeof(msg) - 1))
+ < 0)
+ {
+ looping = false;
+ } else {
+ int context, prompt_start = 0;
+ char type[1024];
+
+ msg[rc] = '\0';
+ if (sscanf(msg, "i:%d:%n", &rl_point, &prompt_start) == 1) {
+ const char* initial = &msg[prompt_start];
+
+ rl_extend_line_buffer(strlen(initial) + 1);
+ strcpy(rl_line_buffer, initial);
+ rl_end = strlen(initial);
+ rewrite_line_start
+ = std::string(rl_line_buffer, rl_point);
+ rl_redisplay();
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 'c',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ } else if (sscanf(msg, "f:%d:%n", &context, &prompt_start)
+ == 1
+ && prompt_start != 0
+ && (current_context
+ = this->rc_contexts.find(context))
+ != this->rc_contexts.end())
+ {
+ got_abort = 0;
+ current_context->second->load();
+ rl_callback_handler_install(&msg[prompt_start],
+ line_ready_tramp);
+ last_match_str_valid = false;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 'l',
+ rl_line_buffer,
+ rl_end)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ if (sendcmd(
+ this->rc_command_pipe[RCF_SLAVE], 'w', "", 0)
+ != 0)
+ {
+ perror("line: write failed");
+ _exit(1);
+ }
+ } else if (strcmp(msg, "a") == 0) {
+ char reply[4];
+
+ rl_done = 1;
+ got_timeout = 0;
+ got_line = 1;
+ rl_callback_handler_remove();
+
+ snprintf(reply, sizeof(reply), "a");
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE],
+ reply,
+ strlen(reply))
+ == -1)
+ {
+ perror("abort: write failed");
+ _exit(1);
+ }
+ } else if (sscanf(msg,
+ "apre:%d:%1023[^\x1d]\x1d%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]
+ ->rc_prefixes[std::string(type)]
+ = std::string(&msg[prompt_start]);
+ } else if (sscanf(msg,
+ "ap:%d:%31[^:]:%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]->add_possibility(
+ std::string(type), std::string(&msg[prompt_start]));
+ if (rl_last_func == rl_complete
+ || rl_last_func == rl_menu_complete)
+ {
+ rl_last_func = NULL;
+ }
+ } else if (sscanf(msg,
+ "rp:%d:%31[^:]:%n",
+ &context,
+ type,
+ &prompt_start)
+ == 2)
+ {
+ require(this->rc_contexts[context] != nullptr);
+
+ this->rc_contexts[context]->rem_possibility(
+ std::string(type), std::string(&msg[prompt_start]));
+ } else if (sscanf(msg, "cpre:%d", &context) == 1) {
+ this->rc_contexts[context]->rc_prefixes.clear();
+ } else if (sscanf(msg, "cp:%d:%s", &context, type)) {
+ this->rc_contexts[context]->clear_possibilities(type);
+ } else {
+ log_error("unhandled message: %s", msg);
+ }
+ }
+ }
+ }
+
+ if (got_timeout) {
+ got_timeout = 0;
+ if (sendcmd(this->rc_command_pipe[RCF_SLAVE],
+ 't',
+ rl_line_buffer,
+ rl_end)
+ == -1)
+ {
+ _exit(1);
+ }
+ }
+ if (got_line) {
+ struct itimerval itv;
+
+ got_line = 0;
+ itv.it_value.tv_sec = 0;
+ itv.it_value.tv_usec = 0;
+ itv.it_interval.tv_sec = 0;
+ itv.it_interval.tv_usec = 0;
+ if (setitimer(ITIMER_REAL, &itv, nullptr) < 0) {
+ log_error("setitimer: %s", strerror(errno));
+ }
+ if (current_context != this->rc_contexts.end()) {
+ current_context->second->save();
+ current_context = this->rc_contexts.end();
+ }
+ }
+ if (got_winch) {
+ struct winsize new_ws;
+
+ if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &new_ws) == -1) {
+ throw error(errno);
+ }
+ got_winch = 0;
+ rl_set_screen_size(new_ws.ws_row, new_ws.ws_col);
+ }
+ }
+
+ if (this->rc_save_history) {
+ auto config_dir = lnav::paths::dotlnav();
+ for (auto& pair : this->rc_contexts) {
+ pair.second->load();
+
+ auto hpath
+ = (config_dir / pair.second->get_name()).string() + ".history";
+ write_history(hpath.c_str());
+ pair.second->save();
+ }
+ }
+
+ _exit(0);
+}
+
+void
+readline_curses::line_ready(const char* line)
+{
+ auto_mem<char> expanded;
+ char msg[1024] = {0};
+ int rc;
+ const char* cmd_ch = alt_done ? "D" : "d";
+
+ alt_done = false;
+ if (got_abort || line == nullptr) {
+ snprintf(msg, sizeof(msg), "a");
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE], msg, strlen(msg))
+ == -1)
+ {
+ perror("abort: write failed");
+ _exit(1);
+ }
+ return;
+ }
+
+ if (rl_line_buffer[0] == '^') {
+ rc = -1;
+ } else {
+ rc = history_expand(rl_line_buffer, expanded.out());
+ }
+ switch (rc) {
+#if 0
+ /* TODO: fix clash between history and pcre metacharacters */
+ case -1:
+ /* XXX */
+ snprintf(msg, sizeof(msg),
+ "e:unable to expand history -- %s",
+ expanded.in());
+ break;
+#endif
+
+ case -1:
+ snprintf(msg, sizeof(msg), "%s:%s", cmd_ch, line);
+ break;
+
+ case 0:
+ case 1:
+ case 2: /* XXX */
+ snprintf(msg, sizeof(msg), "%s:%s", cmd_ch, expanded.in());
+ break;
+ }
+
+ if (sendstring(this->rc_command_pipe[RCF_SLAVE], msg, strlen(msg)) == -1) {
+ perror("line_ready: write failed");
+ _exit(1);
+ }
+
+ {
+ HIST_ENTRY* entry;
+
+ if (line[0] != '\0'
+ && (history_length == 0
+ || (entry = history_get(history_base + history_length - 1))
+ == nullptr
+ || strcmp(entry->line, line) != 0))
+ {
+ add_history(line);
+ }
+ }
+}
+
+void
+readline_curses::check_poll_set(const std::vector<struct pollfd>& pollfds)
+{
+ int rc;
+
+ if (pollfd_ready(pollfds, this->rc_pty[RCF_MASTER])) {
+ char buffer[128];
+
+ rc = read(this->rc_pty[RCF_MASTER], buffer, sizeof(buffer));
+ if (rc > 0) {
+ int old_x = this->vc_x;
+
+ this->map_output(buffer, rc);
+ if (this->vc_x != old_x) {
+ this->rc_change(this);
+ }
+ }
+ }
+ if (pollfd_ready(pollfds, this->rc_command_pipe[RCF_MASTER])) {
+ char msg[1024 + 1];
+
+ rc = recvstring(
+ this->rc_command_pipe[RCF_MASTER], msg, sizeof(msg) - 1);
+ if (rc >= 0) {
+ msg[rc] = '\0';
+ if (this->rc_matches_remaining > 0) {
+ this->rc_matches.emplace_back(msg);
+ this->rc_matches_remaining -= 1;
+ if (this->rc_matches_remaining == 0) {
+ this->rc_display_match(this);
+ }
+ } else if (msg[0] == 'm') {
+ if (sscanf(msg,
+ "m:%d:%d:%d",
+ &this->rc_match_start,
+ &this->rc_matches_remaining,
+ &this->rc_max_match_length)
+ != 3)
+ {
+ require(0);
+ }
+ this->rc_matches.clear();
+ if (this->rc_matches_remaining == 0) {
+ this->rc_display_match(this);
+ }
+ this->rc_match_index = 0;
+ } else if (msg[0] == '\t') {
+ char path[2048];
+
+ if (sscanf(msg, "\t:%s", path) != 1) {
+ require(0);
+ }
+ this->rc_remote_complete_path = path;
+ this->rc_completion_request(this);
+ } else if (msg[0] == 'n') {
+ if (sscanf(msg, "n:%d", &this->rc_match_index) != 1) {
+ require(0);
+ }
+ this->rc_display_next(this);
+ } else {
+ switch (msg[0]) {
+ case 't':
+ case 'd':
+ case 'D':
+ this->rc_value = std::string(&msg[2]);
+ break;
+ }
+ switch (msg[0]) {
+ case 'a':
+ curs_set(0);
+ this->vc_line.clear();
+ this->rc_active_context = -1;
+ this->rc_matches.clear();
+ this->rc_abort(this);
+ this->rc_display_match(this);
+ this->rc_blur(this);
+ break;
+
+ case 't':
+ this->rc_timeout(this);
+ break;
+
+ case 'd':
+ case 'D':
+ curs_set(0);
+ this->rc_active_context = -1;
+ this->rc_matches.clear();
+ if (msg[0] == 'D' || this->rc_is_alt_focus) {
+ this->rc_alt_perform(this);
+ } else {
+ this->rc_perform(this);
+ }
+ this->rc_display_match(this);
+ this->rc_blur(this);
+ break;
+
+ case 'l':
+ this->rc_line_buffer = &msg[2];
+ if (this->rc_active_context != -1) {
+ this->rc_change(this);
+ }
+ this->rc_matches.clear();
+ if (this->rc_active_context != -1) {
+ this->rc_display_match(this);
+ }
+ break;
+
+ case 'c':
+ this->rc_line_buffer = &msg[2];
+ this->rc_change(this);
+ this->rc_display_match(this);
+ break;
+ case 'w':
+ this->rc_ready_for_input = true;
+ break;
+ }
+ }
+ }
+ }
+}
+
+void
+readline_curses::handle_key(int ch)
+{
+ const char* bch;
+ int len;
+
+ bch = this->map_input(ch, len);
+ if (write(this->rc_pty[RCF_MASTER], bch, len) == -1) {
+ perror("handle_key: write failed");
+ }
+}
+
+void
+readline_curses::focus(int context,
+ const std::string& prompt,
+ const std::string& initial)
+{
+ char buffer[1024];
+
+ curs_set(1);
+
+ this->rc_active_context = context;
+
+ snprintf(buffer, sizeof(buffer), "f:%d:%s", context, prompt.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("focus: write failed");
+ }
+ wmove(this->vc_window, this->get_actual_y(), this->vc_left);
+ wclrtoeol(this->vc_window);
+ if (!initial.empty()) {
+ this->rewrite_line(initial.size(), initial);
+ }
+ this->rc_is_alt_focus = false;
+ this->rc_focus(this);
+}
+
+void
+readline_curses::rewrite_line(int pos, const std::string& value)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "i:%d:%s", pos, value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("rewrite_line: write failed");
+ }
+}
+
+void
+readline_curses::abort()
+{
+ char buffer[1024];
+
+ this->vc_x = 0;
+ snprintf(buffer, sizeof(buffer), "a");
+ if (sendstring(this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer))
+ == -1)
+ {
+ perror("abort: write failed");
+ }
+}
+
+void
+readline_curses::add_prefix(int context,
+ const std::vector<std::string>& prefix,
+ const std::string& value)
+{
+ char buffer[1024];
+ auto prefix_wire = fmt::format(FMT_STRING("{}"), fmt::join(prefix, "\x1f"));
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "apre:%d:%s\x1d%s",
+ context,
+ prefix_wire.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_prefix: write failed");
+ }
+}
+
+void
+readline_curses::clear_prefixes(int context)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "cpre:%d", context);
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_possibility: write failed");
+ }
+}
+
+void
+readline_curses::add_possibility(int context,
+ const std::string& type,
+ const std::string& value)
+{
+ char buffer[1024];
+
+ if (value.empty()) {
+ return;
+ }
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "ap:%d:%s:%s",
+ context,
+ type.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("add_possibility: write failed");
+ }
+}
+
+void
+readline_curses::rem_possibility(int context,
+ const std::string& type,
+ const std::string& value)
+{
+ char buffer[1024];
+
+ snprintf(buffer,
+ sizeof(buffer),
+ "rp:%d:%s:%s",
+ context,
+ type.c_str(),
+ value.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("rem_possiblity: write failed");
+ }
+}
+
+void
+readline_curses::clear_possibilities(int context, std::string type)
+{
+ char buffer[1024];
+
+ snprintf(buffer, sizeof(buffer), "cp:%d:%s", context, type.c_str());
+ if (sendstring(
+ this->rc_command_pipe[RCF_MASTER], buffer, strlen(buffer) + 1)
+ == -1)
+ {
+ perror("clear_possiblity: write failed");
+ }
+}
+
+void
+readline_curses::do_update()
+{
+ if (!this->vc_visible || this->vc_window == nullptr) {
+ return;
+ }
+
+ if (this->rc_active_context == -1) {
+ int alt_start = -1;
+ struct line_range lr(0, 0);
+ attr_line_t alt_al;
+ view_colors& vc = view_colors::singleton();
+
+ wmove(this->vc_window, this->get_actual_y(), this->vc_left);
+ auto attrs = vc.attrs_for_role(role_t::VCR_TEXT);
+ wattr_set(this->vc_window,
+ attrs.ta_attrs,
+ vc.ensure_color_pair(attrs.ta_fg_color, attrs.ta_bg_color),
+ nullptr);
+ whline(this->vc_window, ' ', this->vc_width);
+
+ if (time(nullptr) > this->rc_value_expiration) {
+ this->rc_value.clear();
+ }
+
+ if (!this->rc_alt_value.empty()) {
+ alt_al.get_string() = this->rc_alt_value;
+ scrub_ansi_string(alt_al.get_string(), &alt_al.get_attrs());
+
+ alt_start = getmaxx(this->vc_window) - alt_al.get_string().size();
+ }
+
+ if (alt_start >= (int) (this->rc_value.length() + 5)) {
+ lr.lr_end = alt_al.get_string().length();
+ view_curses::mvwattrline(
+ this->vc_window, this->get_actual_y(), alt_start, alt_al, lr);
+ }
+
+ lr.lr_end = this->rc_value.length();
+ view_curses::mvwattrline(this->vc_window,
+ this->get_actual_y(),
+ this->vc_left,
+ this->rc_value,
+ lr);
+ this->set_x(0);
+ }
+
+ if (this->rc_active_context != -1) {
+ readline_context* rc = this->rc_contexts[this->rc_active_context];
+ readline_highlighter_t hl = rc->get_highlighter();
+ attr_line_t al = this->vc_line;
+
+ if (hl != nullptr) {
+ hl(al, this->vc_left + this->vc_x);
+ }
+ view_curses::mvwattrline(this->vc_window,
+ this->get_actual_y(),
+ this->vc_left,
+ al,
+ line_range{0, (int) this->vc_width});
+
+ wmove(
+ this->vc_window, this->get_actual_y(), this->vc_left + this->vc_x);
+ }
+}
+
+std::string
+readline_curses::get_match_string() const
+{
+ auto len = std::min((size_t) this->vc_x, this->rc_line_buffer.size())
+ - this->rc_match_start;
+ auto* context = this->get_active_context();
+
+ if (context->get_append_character() != 0) {
+ if (this->rc_line_buffer.length() > (this->rc_match_start + len - 1)
+ && this->rc_line_buffer[this->rc_match_start + len - 1]
+ == context->get_append_character())
+ {
+ len -= 1;
+ } else if (this->rc_line_buffer.length()
+ > (this->rc_match_start + len - 2)
+ && this->rc_line_buffer[this->rc_match_start + len - 2]
+ == context->get_append_character())
+ {
+ len -= 2;
+ }
+ }
+
+ return this->rc_line_buffer.substr(this->rc_match_start, len);
+}
+
+void
+readline_curses::set_value(const std::string& value)
+{
+ this->set_attr_value(attr_line_t::from_ansi_str(value.c_str()));
+}
+
+void
+readline_curses::set_attr_value(const attr_line_t& value)
+{
+ this->rc_value = value;
+ if (this->rc_value.length() > 1024) {
+ this->rc_value = this->rc_value.subline(0, 1024);
+ }
+ this->rc_value_expiration = time(nullptr) + VALUE_EXPIRATION;
+ this->set_needs_update();
+}
+
+void
+readline_curses::update_poll_set(std::vector<struct pollfd>& pollfds)
+{
+ if (this->rc_pty[RCF_MASTER] != -1) {
+ pollfds.push_back((struct pollfd){this->rc_pty[RCF_MASTER], POLLIN, 0});
+ }
+ if (this->rc_command_pipe[RCF_MASTER] != -1) {
+ pollfds.push_back(
+ (struct pollfd){this->rc_command_pipe[RCF_MASTER], POLLIN, 0});
+ }
+}