On macOS GUI builds, chdir(HOME) runs before deferred file opens are processed, breaking relative paths passed on the command line. Resolve each argv path to absolute immediately during argument parsing. Bump version to 1.11.2. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
340 lines
8.7 KiB
C++
340 lines
8.7 KiB
C++
#include <clocale>
|
|
#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;
|
|
}
|
|
|
|
|
|
int
|
|
main(int argc, char *argv[])
|
|
{
|
|
std::setlocale(LC_ALL, "");
|
|
|
|
// 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>();
|
|
}
|
|
|
|
#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);
|
|
}
|
|
|
|
fe->Shutdown();
|
|
|
|
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;
|
|
}
|
|
} |