Files
kte/main.cc
T
kyleandClaude Opus 5.5 2125583407 Remove the Qt frontend
The Qt GUI was unused: out of make-app-release since March, not built by
the gate, and its app bundle could not start (no platform plugin). Every
renderer and input change had to be made in it separately. kge is the
ImGui frontend only.

Removes QtFrontend/QtInputHandler/QtRenderer, the KTE_USE_QT option and
its #if branches (Command.cc, GUITheme.h, main.cc), the Nix qt package
and dev shell, the commented-out Qt release steps, the Qt frontend plan,
and Qt mentions in AGENTS.md, DEVELOPER_GUIDE.md, REWRITE.md and code
comments. Git history keeps the code.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
2026-09-24 12:32:58 -07:00

372 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)
#include "ImGuiFrontend.h"
#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;
}
}