- 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
376 lines
9.7 KiB
C++
376 lines
9.7 KiB
C++
#include <clocale>
|
|
#include <langinfo.h>
|
|
#include <cctype>
|
|
#include <cerrno>
|
|
#include <cstdio>
|
|
#include <cstring>
|
|
#include <getopt.h>
|
|
#include <iostream>
|
|
#include <limits>
|
|
#include <memory>
|
|
#include <algorithm>
|
|
#include <chrono>
|
|
#include <random>
|
|
#include <thread>
|
|
#include <signal.h>
|
|
#include <filesystem>
|
|
#include <string>
|
|
#include <unistd.h>
|
|
#include <sys/stat.h>
|
|
|
|
#include "Command.h"
|
|
#include "Editor.h"
|
|
#include "Frontend.h"
|
|
#include "TerminalFrontend.h"
|
|
#include "ErrorHandler.h"
|
|
|
|
#if defined(KTE_BUILD_GUI)
|
|
#if defined(KTE_USE_QT)
|
|
#include "QtFrontend.h"
|
|
#else
|
|
#include "ImGuiFrontend.h"
|
|
#endif
|
|
#endif
|
|
|
|
|
|
#ifndef KTE_VERSION_STR
|
|
# define KTE_VERSION_STR "devel"
|
|
#endif
|
|
|
|
|
|
static void
|
|
PrintUsage(const char *prog)
|
|
{
|
|
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
|
|
<< "Options:\n"
|
|
<< " -g, --gui Use GUI frontend (if built)\n"
|
|
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
|
<< " -h, --help Show this help and exit\n"
|
|
<< " -V, --version Show version and exit\n"
|
|
<< " --stress-highlighter[=SECONDS] Run a short highlighter stress harness (debug aid)\n";
|
|
}
|
|
|
|
|
|
static int
|
|
RunStressHighlighter(unsigned seconds)
|
|
{
|
|
// Build a synthetic buffer with code-like content
|
|
Buffer buf;
|
|
buf.SetFiletype("cpp");
|
|
buf.SetSyntaxEnabled(true);
|
|
buf.EnsureHighlighter();
|
|
// Seed with many lines
|
|
const int N = 1200;
|
|
for (int i = 0; i < N; ++i) {
|
|
std::string line = "int v" + std::to_string(i) + " = " + std::to_string(i) + "; // line\n";
|
|
buf.insert_row(i, line);
|
|
}
|
|
// Remove the extra last empty row if any artifacts
|
|
// Simulate a viewport of ~60 rows
|
|
const int viewport_rows = 60;
|
|
const auto start_ts = std::chrono::steady_clock::now();
|
|
std::mt19937 rng{1234567u};
|
|
std::uniform_int_distribution<int> row_d(0, N - 1);
|
|
std::uniform_int_distribution<int> op_d(0, 2);
|
|
std::uniform_int_distribution<int> sleep_d(0, 2);
|
|
|
|
// Loop performing edits and highlighter queries while background worker runs
|
|
while (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - start_ts).count() <
|
|
seconds) {
|
|
int fr = row_d(rng);
|
|
if (fr + viewport_rows >= N)
|
|
fr = std::max(0, N - viewport_rows - 1);
|
|
buf.SetOffsets(static_cast<std::size_t>(fr), 0);
|
|
if (buf.Highlighter()) {
|
|
buf.Highlighter()->PrefetchViewport(buf, fr, viewport_rows, buf.Version());
|
|
}
|
|
// Do a few direct GetLine calls over the viewport to shake the caches
|
|
if (buf.Highlighter()) {
|
|
for (int r = 0; r < viewport_rows; r += 7) {
|
|
(void) buf.Highlighter()->GetLine(buf, fr + r, buf.Version());
|
|
}
|
|
}
|
|
// Random simple edit
|
|
int op = op_d(rng);
|
|
int r = row_d(rng);
|
|
if (op == 0) {
|
|
buf.insert_text(r, 0, "/*X*/");
|
|
buf.SetDirty(true);
|
|
} else if (op == 1) {
|
|
buf.delete_text(r, 0, 1);
|
|
buf.SetDirty(true);
|
|
} else {
|
|
// split and join occasionally
|
|
buf.split_line(r, 0);
|
|
buf.join_lines(std::min(r + 1, N - 1));
|
|
buf.SetDirty(true);
|
|
}
|
|
// tiny sleep to allow background thread to interleave
|
|
if (sleep_d(rng) == 0) {
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
|
|
// kte edits UTF-8 text. With LANG unset (common in containers and ssh
|
|
// sessions) the C locale cannot decode or encode non-ASCII characters, and
|
|
// typed or pasted non-ASCII text was dropped (with the rest of the paste).
|
|
// Use a UTF-8 character-type locale when the environment does not give one.
|
|
static void
|
|
ensure_utf8_ctype()
|
|
{
|
|
auto is_utf8 = [] {
|
|
std::string cs = nl_langinfo(CODESET);
|
|
for (auto &c: cs)
|
|
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
|
|
return cs == "utf-8" || cs == "utf8";
|
|
};
|
|
if (is_utf8())
|
|
return;
|
|
for (const char *name: {"C.UTF-8", "C.utf8", "en_US.UTF-8", "en_US.utf8"}) {
|
|
if (std::setlocale(LC_CTYPE, name) && is_utf8())
|
|
return;
|
|
}
|
|
}
|
|
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
std::setlocale(LC_ALL, "");
|
|
ensure_utf8_ctype();
|
|
|
|
// Ensure the error handler (and its log file) is initialised early.
|
|
kte::ErrorHandler::Instance();
|
|
|
|
Editor editor;
|
|
|
|
// CLI parsing using getopt_long
|
|
bool req_gui = false;
|
|
[[maybe_unused]] bool req_term = false;
|
|
bool show_help = false;
|
|
bool show_version = false;
|
|
|
|
static struct option long_opts[] = {
|
|
{"gui", no_argument, nullptr, 'g'},
|
|
{"term", no_argument, nullptr, 't'},
|
|
{"help", no_argument, nullptr, 'h'},
|
|
{"version", no_argument, nullptr, 'V'},
|
|
{"stress-highlighter", optional_argument, nullptr, 1000},
|
|
{nullptr, 0, nullptr, 0}
|
|
};
|
|
|
|
int opt;
|
|
int long_index = 0;
|
|
unsigned stress_seconds = 0;
|
|
while ((opt = getopt_long(argc, argv, "gthV", long_opts, &long_index)) != -1) {
|
|
switch (opt) {
|
|
case 'g':
|
|
req_gui = true;
|
|
break;
|
|
case 't':
|
|
req_term = true;
|
|
break;
|
|
case 'h':
|
|
show_help = true;
|
|
break;
|
|
case 'V':
|
|
show_version = true;
|
|
break;
|
|
case 1000: {
|
|
stress_seconds = 5; // default
|
|
if (optarg && *optarg) {
|
|
try {
|
|
unsigned v = static_cast<unsigned>(std::stoul(optarg));
|
|
if (v > 0 && v < 36000)
|
|
stress_seconds = v;
|
|
} catch (...) {}
|
|
}
|
|
break;
|
|
}
|
|
case '?':
|
|
default:
|
|
PrintUsage(argv[0]);
|
|
return 2;
|
|
}
|
|
}
|
|
|
|
if (show_help) {
|
|
PrintUsage(argv[0]);
|
|
return 0;
|
|
}
|
|
if (show_version) {
|
|
std::cout << "kte " << KTE_VERSION_STR << "\n";
|
|
return 0;
|
|
}
|
|
|
|
if (stress_seconds > 0) {
|
|
return RunStressHighlighter(stress_seconds);
|
|
}
|
|
|
|
// Top-level exception handler to prevent data loss and ensure cleanup
|
|
try {
|
|
// Determine frontend
|
|
#if !defined(KTE_BUILD_GUI)
|
|
if (req_gui) {
|
|
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed."
|
|
<<
|
|
std::endl;
|
|
return 2;
|
|
}
|
|
#else
|
|
bool use_gui = false;
|
|
if (req_gui) {
|
|
use_gui = true;
|
|
} else if (req_term) {
|
|
use_gui = false;
|
|
} else {
|
|
|
|
|
|
|
|
// Default depends on build target: kge defaults to GUI, kte to terminal
|
|
#if defined(KTE_DEFAULT_GUI)
|
|
use_gui = true;
|
|
#else
|
|
use_gui = false;
|
|
#endif
|
|
}
|
|
#endif
|
|
|
|
// Open files passed on the CLI; support +N to jump to line N in the next file.
|
|
// If no files are provided, create an empty buffer.
|
|
if (optind < argc) {
|
|
// Seed a scratch buffer so the UI has something to show while deferred opens
|
|
// (and potential swap recovery prompts) are processed.
|
|
editor.AddBuffer(Buffer());
|
|
std::size_t pending_line = 0; // 0 = no pending line
|
|
for (int i = optind; i < argc; ++i) {
|
|
const char *arg = argv[i];
|
|
if (arg && arg[0] == '+') {
|
|
// Parse +<digits>
|
|
const char *p = arg + 1;
|
|
if (*p != '\0') {
|
|
bool all_digits = true;
|
|
for (const char *q = p; *q; ++q) {
|
|
if (!std::isdigit(static_cast<unsigned char>(*q))) {
|
|
all_digits = false;
|
|
break;
|
|
}
|
|
}
|
|
if (all_digits) {
|
|
// Clamp to >=1 later; 0 disables.
|
|
try {
|
|
unsigned long v = std::stoul(p);
|
|
if (v > std::numeric_limits<std::size_t>::max()) {
|
|
std::cerr <<
|
|
"kte: Warning: Line number too large, ignoring\n";
|
|
pending_line = 0;
|
|
} else {
|
|
pending_line = static_cast<std::size_t>(v);
|
|
}
|
|
} catch (...) {
|
|
// Ignore malformed huge numbers
|
|
pending_line = 0;
|
|
}
|
|
continue; // look for the next file arg
|
|
}
|
|
}
|
|
// Fall through: not a +number, treat as filename starting with '+'
|
|
}
|
|
|
|
// Resolve to absolute path now, before any
|
|
// chdir (macOS GUI changes CWD to HOME before
|
|
// deferred opens are processed).
|
|
std::string path = arg;
|
|
try {
|
|
std::filesystem::path p(path);
|
|
if (p.is_relative()) {
|
|
path = std::filesystem::absolute(p).string();
|
|
}
|
|
} catch (...) {
|
|
// Fall through with original path
|
|
}
|
|
editor.RequestOpenFile(path, pending_line);
|
|
pending_line = 0; // consumed (if set)
|
|
}
|
|
// If we ended with a pending +N but no subsequent file, ignore it.
|
|
} else {
|
|
// Create a single empty buffer
|
|
editor.AddBuffer(Buffer());
|
|
editor.SetStatus("new: empty buffer");
|
|
}
|
|
|
|
// Install built-in commands
|
|
InstallDefaultCommands();
|
|
|
|
// Select frontend
|
|
std::unique_ptr<Frontend> fe;
|
|
#if defined(KTE_BUILD_GUI)
|
|
if (use_gui) {
|
|
fe = std::make_unique<GUIFrontend>();
|
|
} else
|
|
#endif
|
|
{
|
|
fe = std::make_unique<TerminalFrontend>();
|
|
}
|
|
|
|
// Guarantee Shutdown() runs on every exit path from here on (normal
|
|
// return, early return, or exception unwinding e.g. from Init/Execute/
|
|
// Step) so a crash never leaves the terminal in raw/cbreak mode.
|
|
struct FrontendShutdownGuard {
|
|
Frontend *fe;
|
|
|
|
|
|
~FrontendShutdownGuard()
|
|
{
|
|
if (fe)
|
|
fe->Shutdown();
|
|
}
|
|
} shutdown_guard{fe.get()};
|
|
|
|
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
|
if (use_gui) {
|
|
/* likely using the .app, so need to cd */
|
|
const char *home = getenv("HOME");
|
|
if (!home) {
|
|
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
|
return 1;
|
|
}
|
|
if (chdir(home) != 0) {
|
|
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
|
<< std::strerror(errno) << std::endl;
|
|
return 1;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
if (!fe->Init(argc, argv, editor)) {
|
|
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
|
return 1;
|
|
}
|
|
|
|
Execute(editor, CommandId::CenterOnCursor);
|
|
|
|
bool running = true;
|
|
while (running) {
|
|
fe->Step(editor, running);
|
|
}
|
|
|
|
return 0;
|
|
} catch (const std::exception &e) {
|
|
std::string msg = std::string("Unhandled exception: ") + e.what();
|
|
kte::ErrorHandler::Instance().Critical("main", msg);
|
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
|
<< "kte encountered an unhandled exception: " << e.what() << "\n"
|
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
|
return 1;
|
|
} catch (...) {
|
|
kte::ErrorHandler::Instance().Critical("main", "Unknown exception");
|
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
|
<< "kte encountered an unknown exception.\n"
|
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
|
return 1;
|
|
}
|
|
} |