2026-10-14 00:01:03 +03:00
|
|
|
/* 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 <cstdio>
|
|
|
|
|
#include <cstdlib>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <readline/readline.h>
|
|
|
|
|
#include <readline/history.h>
|
|
|
|
|
#include <sys/ioctl.h>
|
|
|
|
|
#include <unistd.h>
|
|
|
|
|
|
|
|
|
|
/* ----------------------------- 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 ------------------------ */
|
2026-10-14 15:30:12 +03:00
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-14 00:01:03 +03:00
|
|
|
std::string read_line(std::string_view prompt, bool &ok) {
|
|
|
|
|
if (!is_terminal(STDIN_FILENO)) return read_plain(prompt, ok);
|
2026-10-14 15:30:12 +03:00
|
|
|
ensure_readline_init();
|
2026-10-14 00:01:03 +03:00
|
|
|
char *line = readline(prompt.empty() ? "" : std::string(prompt).c_str());
|
2026-10-14 15:30:12 +03:00
|
|
|
setup_signal_handler();
|
|
|
|
|
if (!line) {
|
|
|
|
|
if (g_interrupted) {
|
|
|
|
|
g_interrupted = 0;
|
|
|
|
|
std::fputc('\n', stdout);
|
|
|
|
|
std::fflush(stdout);
|
|
|
|
|
ok = true;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
|
|
|
|
ok = false;
|
|
|
|
|
return "";
|
|
|
|
|
}
|
2026-10-14 00:01:03 +03:00
|
|
|
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;
|
2026-10-14 15:30:12 +03:00
|
|
|
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;
|
2026-10-14 00:01:03 +03:00
|
|
|
#endif
|
|
|
|
|
const char *c = std::getenv("COLUMNS");
|
|
|
|
|
if (c && *c) { int n = std::atoi(c); if (n > 0) return n; }
|
|
|
|
|
return 80;
|
|
|
|
|
}
|