Files
kte/TerminalFrontend.cc
T
Claude 93901928da Force a UTF-8 ctype, stable buffer ids, redraw after deferred opens
- main: when the environment's locale is not UTF-8 (LANG unset, C/POSIX),
  switch LC_CTYPE to a UTF-8 locale so ncurses delivers and draws
  multibyte characters instead of dropping them.
- Buffer::Id(): a process-unique id that survives moves and is fresh for
  copies. The terminal renderer's skip and cursor caches and the reload
  confirmation now key on it rather than the buffer's address, which is
  reused after a buffer is closed.
- The reload confirmation is cancelled by any other command, like the
  quit confirmation.
- TerminalFrontend redraws when a deferred open starts a prompt or sets a
  status (e.g. the swap recovery question), not only when a buffer opens.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015rcVQiiWyTzoNrsvLSaAfB
2026-09-24 09:16:04 -07:00

176 lines
4.9 KiB
C++

#include <cstdio>
#include <ctime>
#include <string>
#include <ncurses.h>
#include <termios.h>
#include <unistd.h>
#include "TerminalFrontend.h"
#include "Command.h"
#include "Editor.h"
bool
TerminalFrontend::Init(int &argc, char **argv, Editor &ed)
{
(void) argc;
(void) argv;
// Keys are read from stdin. Piped input would be executed as editor
// commands (and after EOF the loop spun at full CPU with no way to quit).
if (!isatty(STDIN_FILENO)) {
std::fprintf(stderr, "kte: standard input is not a terminal\n");
return false;
}
// Ensure Control keys reach the app: disable XON/XOFF and dsusp/susp bindings (e.g., ^S/^Q, ^Y on macOS)
{
struct termios tio{};
if (tcgetattr(STDIN_FILENO, &tio) == 0) {
// Save original to restore on shutdown
orig_tio_ = tio;
have_orig_tio_ = true;
// Disable software flow control so C-s/C-q work
tio.c_iflag &= static_cast<unsigned long>(~IXON);
#ifdef IXOFF
tio.c_iflag &= static_cast<unsigned long>(~IXOFF);
#endif
// Disable dsusp/susp characters so C-y (VDSUSP on macOS) and C-z don't signal-stop the app
#ifdef _POSIX_VDISABLE
#ifdef VSUSP
tio.c_cc[VSUSP] = _POSIX_VDISABLE;
#endif
#ifdef VDSUSP
tio.c_cc[VDSUSP] = _POSIX_VDISABLE;
#endif
#endif
(void) tcsetattr(STDIN_FILENO, TCSANOW, &tio);
}
}
initscr();
cbreak();
noecho();
keypad(stdscr, TRUE);
// Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals)
meta(stdscr, TRUE);
// Make ESC key sequences resolve quickly so ESC+<key> works as meta
// set_escdelay is an ncurses extension function (not a macro), so test for
// ncurses itself rather than `#ifdef set_escdelay`, which is always false.
#if defined(NCURSES_VERSION)
set_escdelay(TerminalFrontend::kEscDelayMs);
#endif
// Make getch() block briefly instead of busy-looping; reduces CPU when idle
// Equivalent to nodelay(FALSE) with a small timeout.
timeout(16); // ~16ms (about 60Hz)
curs_set(1);
// Enable mouse support if available
mouseinterval(0);
mousemask(ALL_MOUSE_EVENTS | REPORT_MOUSE_POSITION, nullptr);
int r = 0, c = 0;
getmaxyx(stdscr, r, c);
prev_r_ = r;
prev_c_ = c;
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
// Attach editor to input handler for editor-owned features (e.g., universal argument)
input_.Attach(&ed);
// Ignore SIGINT (Ctrl-C) so it doesn't terminate the TUI.
// We'll restore the previous handler on Shutdown().
{
struct sigaction sa{};
sa.sa_handler = SIG_IGN;
sigemptyset(&sa.sa_mask);
sa.sa_flags = 0;
struct sigaction old{};
if (sigaction(SIGINT, &sa, &old) == 0) {
old_sigint_ = old;
have_old_sigint_ = true;
}
}
return true;
}
void
TerminalFrontend::Step(Editor &ed, bool &running)
{
// Handle resize and keep editor dimensions synced
int r, c;
getmaxyx(stdscr, r, c);
bool changed = first_frame_;
if (r != prev_r_ || c != prev_c_) {
resizeterm(r, c);
clear();
prev_r_ = r;
prev_c_ = c;
changed = true;
}
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
// Allow deferred opens (including swap recovery prompts) to run.
// Its return value only reports an opened buffer; a deferred open that
// starts a prompt or sets a status must be drawn too.
{
const bool prompt_before = ed.PromptActive();
const std::string status_before = ed.Status();
const std::time_t status_time_before = ed.StatusTime();
if (ed.ProcessPendingOpens())
changed = true;
if (ed.PromptActive() != prompt_before || ed.Status() != status_before ||
ed.StatusTime() != status_time_before)
changed = true;
}
// Handle all pending input before drawing: one key per frame made a
// pasted block take a frame (and a full redraw) per character. The first
// read waits up to the configured timeout; the rest do not wait.
MappedInput mi;
int handled = 0;
while (handled < 4096 && input_.PollKey(mi)) {
changed = true;
if (mi.hasCommand)
Execute(ed, mi.id, mi.arg, mi.count);
if (ed.QuitRequested())
break;
if (++handled == 1)
timeout(0);
}
if (handled > 0)
timeout(16);
if (ed.QuitRequested()) {
running = false;
}
// Redraw after input or a resize, and otherwise about once a second
// (status messages, deferred work): redrawing every 16 ms kept a core
// busy on large or long-line files while idle.
const auto now = std::chrono::steady_clock::now();
if (changed || now - last_draw_ >= std::chrono::milliseconds(1000)) {
try {
renderer_.Draw(ed);
} catch (const std::exception &e) {
ed.SetStatus(std::string("Display error: ") + e.what());
}
last_draw_ = now;
first_frame_ = false;
}
}
void
TerminalFrontend::Shutdown()
{
// Restore original terminal settings if we changed them
if (have_orig_tio_) {
(void) tcsetattr(STDIN_FILENO, TCSANOW, &orig_tio_);
have_orig_tio_ = false;
}
// Restore previous SIGINT handler
if (have_old_sigint_) {
(void) sigaction(SIGINT, &old_sigint_, nullptr);
have_old_sigint_ = false;
}
endwin();
}