Add GUI initialization updates and improve navigation commands.
- Implement terminal detachment for GUI mode to enable terminal closure post-launch. - Add `+N` support for opening files at specific line numbers and refine cursor positioning. - Introduce `JumpToLine` command for direct navigation by line number. - Enhance mouse wheel handling for line-wise scrolling.
This commit is contained in:
28
.idea/workspace.xml
generated
28
.idea/workspace.xml
generated
@@ -33,8 +33,17 @@
|
||||
</configurations>
|
||||
</component>
|
||||
<component name="ChangeListManager">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`.">
|
||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built.">
|
||||
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/TerminalInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/main.cc" beforeDir="false" afterPath="$PROJECT_DIR$/main.cc" afterDir="false" />
|
||||
</list>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||
@@ -128,7 +137,7 @@
|
||||
<recent name="$PROJECT_DIR$/docs" />
|
||||
</key>
|
||||
</component>
|
||||
<component name="RunManager" selected="CMake Application.imgui">
|
||||
<component name="RunManager" selected="CMake Application.kge">
|
||||
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
|
||||
<method v="2">
|
||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||
@@ -162,7 +171,7 @@
|
||||
<option name="number" value="Default" />
|
||||
<option name="presentableId" value="Default" />
|
||||
<updated>1764457173148</updated>
|
||||
<workItem from="1764457174208" duration="42867000" />
|
||||
<workItem from="1764457174208" duration="46874000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
@@ -220,7 +229,15 @@
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764501532446</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="8" />
|
||||
<task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built.">
|
||||
<option name="closed" value="true" />
|
||||
<created>1764502480274</created>
|
||||
<option name="number" value="00008" />
|
||||
<option name="presentableId" value="LOCAL-00008" />
|
||||
<option name="project" value="LOCAL" />
|
||||
<updated>1764502480274</updated>
|
||||
</task>
|
||||
<option name="localTasksCounter" value="9" />
|
||||
<servers />
|
||||
</component>
|
||||
<component name="TypeScriptGeneratedFilesManager">
|
||||
@@ -241,7 +258,8 @@
|
||||
<MESSAGE value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
|
||||
<MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update. - Document `TestFrontend` for programmatic testing, including examples and usage details. - Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
|
||||
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure. - Delete `packaging.cmake` to streamline build system. - Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`. - Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing. - Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation. - Enhance kill ring operations and new prompt workflows in `Editor`." />
|
||||
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built." />
|
||||
<option name="LAST_COMMIT_MESSAGE" value="Add man pages for `kge` and `kte` with installation targets in CMake. - Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples. - Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`. - Ensure `kge` man page installation is conditional on GUI being built." />
|
||||
</component>
|
||||
<component name="XSLT-Support.FileAssociations.UIState">
|
||||
<expand />
|
||||
|
||||
@@ -4,7 +4,7 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(KTE_VERSION "0.1.0")
|
||||
set(KTE_VERSION "1.0.0")
|
||||
|
||||
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
@@ -167,11 +167,11 @@ if (${BUILD_GUI})
|
||||
install(TARGETS kge
|
||||
BUNDLE DESTINATION .
|
||||
)
|
||||
else()
|
||||
else ()
|
||||
install(TARGETS kge
|
||||
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
|
||||
)
|
||||
endif()
|
||||
endif ()
|
||||
# Install kge man page only when GUI is built
|
||||
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
|
||||
endif ()
|
||||
|
||||
45
Command.cc
45
Command.cc
@@ -646,6 +646,16 @@ cmd_open_file_start(CommandContext &ctx)
|
||||
}
|
||||
|
||||
|
||||
static bool
|
||||
cmd_jump_to_line_start(CommandContext &ctx)
|
||||
{
|
||||
// Start a prompt to read a 1-based line number and jump there (clamped)
|
||||
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
|
||||
ctx.editor.SetStatus("Goto line: ");
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
// --- Buffers: switch/next/prev/close ---
|
||||
static bool
|
||||
cmd_buffer_switch_start(CommandContext &ctx)
|
||||
@@ -990,6 +1000,37 @@ cmd_newline(CommandContext &ctx)
|
||||
} else {
|
||||
ctx.editor.SetStatus("Nothing to confirm");
|
||||
}
|
||||
} else if (kind == Editor::PromptKind::GotoLine) {
|
||||
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||
if (!buf) {
|
||||
ctx.editor.SetStatus("No buffer");
|
||||
return true;
|
||||
}
|
||||
std::size_t nrows = buf->Nrows();
|
||||
if (nrows == 0) {
|
||||
buf->SetCursor(0, 0);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
ctx.editor.SetStatus("Empty buffer");
|
||||
return true;
|
||||
}
|
||||
// Parse 1-based line number; on failure, keep cursor and show status
|
||||
std::size_t line1 = 0;
|
||||
try {
|
||||
if (!value.empty())
|
||||
line1 = static_cast<std::size_t>(std::stoull(value));
|
||||
} catch (...) {
|
||||
line1 = 0;
|
||||
}
|
||||
if (line1 == 0) {
|
||||
ctx.editor.SetStatus("Goto canceled (invalid line)");
|
||||
return true;
|
||||
}
|
||||
std::size_t y = line1 - 1; // convert to 0-based
|
||||
if (y >= nrows)
|
||||
y = nrows - 1; // clamp to last line
|
||||
buf->SetCursor(0, y);
|
||||
ensure_cursor_visible(ctx.editor, *buf);
|
||||
ctx.editor.SetStatus("Goto line " + std::to_string(line1));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -2372,6 +2413,10 @@ InstallDefaultCommands()
|
||||
CommandRegistry::Register({
|
||||
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
|
||||
});
|
||||
// Direct navigation by line number
|
||||
CommandRegistry::Register({
|
||||
CommandId::JumpToLine, "goto-line", "Prompt for line and jump", cmd_jump_to_line_start
|
||||
});
|
||||
// Undo/Redo
|
||||
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
||||
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
||||
|
||||
@@ -72,6 +72,8 @@ enum class CommandId {
|
||||
// Buffer operations
|
||||
ReloadBuffer, // reload buffer from disk (C-k l)
|
||||
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
|
||||
// Direct navigation by line number
|
||||
JumpToLine, // prompt for line and jump (C-k g)
|
||||
// Meta
|
||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||
};
|
||||
|
||||
2
Editor.h
2
Editor.h
@@ -302,7 +302,7 @@ public:
|
||||
|
||||
|
||||
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
|
||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch };
|
||||
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine };
|
||||
|
||||
|
||||
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
|
||||
|
||||
@@ -279,6 +279,24 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||
MappedInput mi;
|
||||
bool produced = false;
|
||||
switch (e.type) {
|
||||
case SDL_MOUSEWHEEL: {
|
||||
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
|
||||
int dy = e.wheel.y;
|
||||
#ifdef SDL_MOUSEWHEEL_FLIPPED
|
||||
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
|
||||
dy = -dy;
|
||||
#endif
|
||||
if (dy != 0) {
|
||||
int repeat = dy > 0 ? dy : -dy;
|
||||
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown;
|
||||
std::lock_guard<std::mutex> lk(mu_);
|
||||
for (int i = 0; i < repeat; ++i) {
|
||||
q_.push(MappedInput{true, id, std::string(), 0});
|
||||
}
|
||||
return true; // consumed
|
||||
}
|
||||
return false;
|
||||
}
|
||||
case SDL_KEYDOWN: {
|
||||
// Remember state before mapping; used for TEXTINPUT suppression heuristics
|
||||
const bool was_k_prefix = k_prefix_;
|
||||
|
||||
@@ -45,7 +45,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
const auto &lines = buf->Rows();
|
||||
// Reserve space for status bar at bottom
|
||||
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
|
||||
ImGuiWindowFlags_HorizontalScrollbar);
|
||||
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
|
||||
// Detect click-to-move inside this scroll region
|
||||
ImVec2 list_origin = ImGui::GetCursorScreenPos();
|
||||
float scroll_y = ImGui::GetScrollY();
|
||||
@@ -56,24 +56,42 @@ GUIRenderer::Draw(Editor &ed)
|
||||
const float line_h = ImGui::GetTextLineHeight();
|
||||
const float row_h = ImGui::GetTextLineHeightWithSpacing();
|
||||
const float space_w = ImGui::CalcTextSize(" ").x;
|
||||
// If the command layer requested a specific top-of-screen (via Buffer::Rowoffs),
|
||||
// force the ImGui scroll to match so paging aligns the first visible row.
|
||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||
// This prevents clicks/wheel from being immediately overridden by stale offsets.
|
||||
bool forced_scroll = false;
|
||||
{
|
||||
std::size_t desired_top = buf->Rowoffs();
|
||||
long current_top = static_cast<long>(scroll_y / row_h);
|
||||
if (static_cast<long>(desired_top) != current_top) {
|
||||
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h);
|
||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
||||
|
||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||
|
||||
// Detect programmatic change (e.g., keyboard navigation ensured visibility)
|
||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
||||
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
|
||||
scroll_y = ImGui::GetScrollY();
|
||||
forced_scroll = true;
|
||||
} else {
|
||||
// If user scrolled (scroll_y changed), update buffer row offset accordingly
|
||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||
// Keep horizontal offset owned by GUI; only update vertical offset here
|
||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||
mbuf->Coloffs());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Update trackers for next frame
|
||||
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||
prev_scroll_y = ImGui::GetScrollY();
|
||||
}
|
||||
// Synchronize cursor and scrolling.
|
||||
// A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row.
|
||||
// B) When the cursor moves (via keyboard commands), scroll it back into view.
|
||||
// Ensure the cursor is visible even on the first frame or when it didn't move,
|
||||
// unless we already forced scrolling from Buffer::Rowoffs this frame.
|
||||
{
|
||||
static float prev_scroll_y = -1.0f;
|
||||
static long prev_cursor_y = -1;
|
||||
// Compute visible row range using the child window height
|
||||
float child_h = ImGui::GetWindowHeight();
|
||||
long first_row = static_cast<long>(scroll_y / row_h);
|
||||
@@ -82,37 +100,7 @@ GUIRenderer::Draw(Editor &ed)
|
||||
vis_rows = 1;
|
||||
long last_row = first_row + vis_rows - 1;
|
||||
|
||||
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
|
||||
// Skip this when we just forced a scroll alignment this frame (programmatic change).
|
||||
if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
long new_row = (cyr < first_row) ? first_row : last_row;
|
||||
if (new_row < 0)
|
||||
new_row = 0;
|
||||
if (new_row >= static_cast<long>(lines.size()))
|
||||
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
|
||||
// Clamp column to line length
|
||||
std::size_t new_col = 0;
|
||||
if (!lines.empty()) {
|
||||
const std::string &l = lines[static_cast<std::size_t>(new_row)];
|
||||
new_col = std::min<std::size_t>(cx, l.size());
|
||||
}
|
||||
char tmp2[64];
|
||||
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
|
||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
|
||||
cy = buf->Cury();
|
||||
cx = buf->Curx();
|
||||
cyr = static_cast<long>(cy);
|
||||
// Update visible range again in case content changed
|
||||
first_row = static_cast<long>(ImGui::GetScrollY() / row_h);
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
// B) If cursor moved since last frame and is outside the visible region, scroll to reveal it
|
||||
// Skip this when we just forced a top-of-screen alignment this frame.
|
||||
if (!forced_scroll && prev_cursor_y >= 0 && static_cast<long>(cy) != prev_cursor_y) {
|
||||
if (!forced_scroll) {
|
||||
long cyr = static_cast<long>(cy);
|
||||
if (cyr < first_row || cyr > last_row) {
|
||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
||||
@@ -128,9 +116,6 @@ GUIRenderer::Draw(Editor &ed)
|
||||
last_row = first_row + vis_rows - 1;
|
||||
}
|
||||
}
|
||||
|
||||
prev_scroll_y = ImGui::GetScrollY();
|
||||
prev_cursor_y = static_cast<long>(cy);
|
||||
}
|
||||
// Handle mouse click before rendering to avoid dependent on drawn items
|
||||
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||
@@ -245,8 +230,16 @@ GUIRenderer::Draw(Editor &ed)
|
||||
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
|
||||
std::string right = rbuf;
|
||||
|
||||
// Middle message
|
||||
const std::string &msg = ed.Status();
|
||||
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
|
||||
std::string msg;
|
||||
if (ed.PromptActive()) {
|
||||
msg = ed.PromptLabel();
|
||||
if (!msg.empty())
|
||||
msg += ": ";
|
||||
msg += ed.PromptText();
|
||||
} else {
|
||||
msg = ed.Status();
|
||||
}
|
||||
|
||||
// Measurements
|
||||
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
|
||||
|
||||
@@ -86,6 +86,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
||||
case 'a':
|
||||
out = CommandId::MarkAllAndJumpEnd;
|
||||
return true; // C-k a (mark all and jump to end)
|
||||
case 'g':
|
||||
out = CommandId::JumpToLine;
|
||||
return true; // C-k g (goto line)
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -35,6 +35,19 @@ map_key_to_command(const int ch,
|
||||
case KEY_MOUSE: {
|
||||
MEVENT ev{};
|
||||
if (getmouse(&ev) == OK) {
|
||||
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
|
||||
#ifdef BUTTON4_PRESSED
|
||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||
out = {true, CommandId::MoveUp, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
#ifdef BUTTON5_PRESSED
|
||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||
out = {true, CommandId::MoveDown, "", 0};
|
||||
return true;
|
||||
}
|
||||
#endif
|
||||
// React to left button click/press
|
||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||
char buf[64];
|
||||
|
||||
165
main.cc
165
main.cc
@@ -1,7 +1,12 @@
|
||||
#include <iostream>
|
||||
#include <memory>
|
||||
#include <string>
|
||||
#include <cctype>
|
||||
#include <unistd.h>
|
||||
#include <getopt.h>
|
||||
#include <signal.h>
|
||||
#include <cstdio>
|
||||
#include <sys/stat.h>
|
||||
|
||||
#include "Editor.h"
|
||||
#include "Command.h"
|
||||
@@ -28,6 +33,109 @@ PrintUsage(const char *prog)
|
||||
}
|
||||
|
||||
|
||||
#if defined(KTE_BUILD_GUI)
|
||||
// Detach the process from the controlling terminal when running the GUI so the
|
||||
// launching terminal can be closed. This mirrors typical GUI app behavior on
|
||||
// POSIX systems.
|
||||
static void
|
||||
DetachFromTerminalIfGUI(bool use_gui)
|
||||
{
|
||||
#if defined(__APPLE__) || defined(__linux__) || defined(__unix__)
|
||||
if (!use_gui)
|
||||
return;
|
||||
|
||||
// Ignore SIGHUP so closing the terminal won't terminate us.
|
||||
signal(SIGHUP, SIG_IGN);
|
||||
|
||||
// Helper: redirect stdio to /dev/null and optionally close extra FDs.
|
||||
auto redirect_stdio_and_close = []() {
|
||||
// Reset file mode creation mask and working directory to a safe default
|
||||
umask(0);
|
||||
chdir("/");
|
||||
|
||||
FILE *fnull_r = fopen("/dev/null", "r");
|
||||
if (fnull_r) {
|
||||
dup2(fileno(fnull_r), STDIN_FILENO);
|
||||
}
|
||||
FILE *fnull_w = fopen("/dev/null", "w");
|
||||
if (fnull_w) {
|
||||
dup2(fileno(fnull_w), STDOUT_FILENO);
|
||||
dup2(fileno(fnull_w), STDERR_FILENO);
|
||||
}
|
||||
|
||||
// Close any other inherited FDs to avoid keeping terminal/pty or pipes open
|
||||
long max_fd = sysconf(_SC_OPEN_MAX);
|
||||
if (max_fd < 0)
|
||||
max_fd = 256; // conservative fallback
|
||||
for (long fd = 3; fd < max_fd; ++fd) {
|
||||
close(static_cast<int>(fd));
|
||||
}
|
||||
};
|
||||
|
||||
#if defined(__APPLE__)
|
||||
// macOS: daemon(3) is deprecated and treated as an error with -Werror.
|
||||
// Use double-fork + setsid and redirect stdio to /dev/null.
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid > 0) {
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
if (setsid() < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid2 = fork();
|
||||
if (pid2 < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid2 > 0) {
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
redirect_stdio_and_close();
|
||||
#else
|
||||
// Prefer daemon(3) on non-Apple POSIX; fall back to manual detach if it fails.
|
||||
if (daemon(0, 0) == 0) {
|
||||
redirect_stdio_and_close();
|
||||
return;
|
||||
}
|
||||
|
||||
pid_t pid = fork();
|
||||
if (pid < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid > 0) {
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
if (setsid() < 0) {
|
||||
// bogus check
|
||||
}
|
||||
|
||||
pid_t pid2 = fork();
|
||||
if (pid2 < 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (pid2 > 0) {
|
||||
_exit(0);
|
||||
}
|
||||
|
||||
redirect_stdio_and_close();
|
||||
#endif
|
||||
#else
|
||||
(void) use_gui;
|
||||
#endif
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
int
|
||||
main(int argc, const char *argv[])
|
||||
{
|
||||
@@ -104,18 +212,67 @@ main(int argc, const char *argv[])
|
||||
use_gui = false;
|
||||
#endif
|
||||
}
|
||||
// If using GUI, detach from the controlling terminal so the terminal can be closed.
|
||||
DetachFromTerminalIfGUI(use_gui);
|
||||
#endif
|
||||
|
||||
// Open files passed on the CLI; if none, create an empty buffer
|
||||
// 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) {
|
||||
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);
|
||||
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 '+'
|
||||
}
|
||||
|
||||
std::string err;
|
||||
const std::string path = argv[i];
|
||||
const std::string path = arg;
|
||||
if (!editor.OpenFile(path, err)) {
|
||||
editor.SetStatus("open: " + err);
|
||||
std::cerr << "kte: " << err << "\n";
|
||||
} else if (pending_line > 0) {
|
||||
// Apply pending +N to the just-opened (current) buffer
|
||||
if (Buffer *b = editor.CurrentBuffer()) {
|
||||
std::size_t nrows = b->Nrows();
|
||||
std::size_t line = pending_line > 0 ? pending_line - 1 : 0;
|
||||
// 1-based to 0-based
|
||||
if (nrows > 0) {
|
||||
if (line >= nrows)
|
||||
line = nrows - 1;
|
||||
} else {
|
||||
line = 0;
|
||||
}
|
||||
b->SetCursor(0, line);
|
||||
// Do not force viewport offsets here; the frontend/renderer
|
||||
// will establish dimensions and normalize visibility on first draw.
|
||||
}
|
||||
pending_line = 0; // consumed
|
||||
}
|
||||
}
|
||||
// If we ended with a pending +N but no subsequent file, ignore it.
|
||||
} else {
|
||||
// Create a single empty buffer
|
||||
editor.AddBuffer(Buffer());
|
||||
@@ -129,11 +286,11 @@ main(int argc, const char *argv[])
|
||||
std::unique_ptr<Frontend> fe;
|
||||
#if defined(KTE_BUILD_GUI)
|
||||
if (use_gui) {
|
||||
fe.reset(new GUIFrontend());
|
||||
fe = std::make_unique<GUIFrontend>();
|
||||
} else
|
||||
#endif
|
||||
{
|
||||
fe.reset(new TerminalFrontend());
|
||||
fe = std::make_unique<TerminalFrontend>();
|
||||
}
|
||||
|
||||
if (!fe->Init(editor)) {
|
||||
|
||||
Reference in New Issue
Block a user