/* editor.cpp — GNU readline line editor, history and term helpers * (is_terminal, term_width). The interactive prompt is handled by * libreadline instead of a hand-rolled raw-mode terminal editor. */ #include "bantam.hpp" #include #include #include #include #include #include #include /* ----------------------------- globals ------------------------------- */ static std::string g_hist_file; static std::string home_dir() { const char *h = std::getenv("HOME"); if (h && *h) return h; return "."; } /* ----------------------------- history -------------------------------- */ void load_history() { g_hist_file = home_dir() + "/.bantam_history"; using_history(); /* A missing history file is not an error. */ read_history(g_hist_file.c_str()); } void add_history(std::string_view s) { if (s.empty()) return; std::string line(s); /* Skip an entry identical to the immediately preceding one. */ if (history_length > 0) { HIST_ENTRY *last = history_get(history_base + history_length - 1); if (last && last->line && line == last->line) return; } ::add_history(line.c_str()); } void save_history() { if (g_hist_file.empty()) return; write_history(g_hist_file.c_str()); } /* ----------------------------- read helpers --------------------------- */ std::string read_plain(std::string_view prompt, bool &ok) { if (!prompt.empty()) fputs(std::string(prompt).c_str(), stdout); fflush(stdout); char *line = nullptr; size_t cap = 0; ssize_t n = getline(&line, &cap, stdin); if (n < 0) { free(line); clearerr(stdin); ok = false; return ""; } while (n > 0 && (line[n - 1] == '\n' || line[n - 1] == '\r')) line[--n] = '\0'; std::string r(line, (size_t)n); free(line); ok = true; return r; } /* ----------------------------- readline editor ------------------------ */ static void ensure_readline_init() { static bool initialized = false; if (initialized) return; initialized = true; rl_initialize(); #if !defined(__APPLE__) && defined(RL_READLINE_VERSION) && (RL_READLINE_VERSION >= 0x0500) /* On GNU Readline (Linux, FreeBSD), terminal raw mode clears ICRNL so * Enter sends '\r' (rl_newline = submit) while Ctrl+J sends '\n' * (rl_insert = literal newline). On macOS / BSD libedit (EditLine), * ICRNL remains enabled in termios (translating Enter '\r' into '\n') and * '\n' is the sole accept-line key (ed-newline), so rebinding '\n' to * rl_insert prevents submitting any prompt. */ if (!rl_library_version || std::strncmp(rl_library_version, "EditLine", 8) != 0) { rl_bind_key('\n', rl_insert); rl_bind_key('\r', rl_newline); } #endif } std::string read_line(std::string_view prompt, bool &ok) { if (!is_terminal(STDIN_FILENO)) return read_plain(prompt, ok); ensure_readline_init(); char *line = readline(prompt.empty() ? "" : std::string(prompt).c_str()); setup_signal_handler(); if (!line) { if (g_interrupted) { g_interrupted = 0; std::fputc('\n', stdout); std::fflush(stdout); ok = true; return ""; } ok = false; return ""; } std::string r(line); free(line); ok = true; return r; } /* ----------------------------- term helpers --------------------------- */ int is_terminal(int fd) { return isatty(fd) != 0; } int term_width() { #ifdef TIOCGWINSZ struct winsize ws{}; if (ioctl(STDOUT_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; if (ioctl(STDIN_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; if (ioctl(STDERR_FILENO, TIOCGWINSZ, &ws) == 0 && ws.ws_col > 0) return (int)ws.ws_col; #endif const char *c = std::getenv("COLUMNS"); if (c && *c) { int n = std::atoi(c); if (n > 0) return n; } return 80; }