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:
2025-11-30 04:28:40 -08:00
parent 65869bd143
commit b8942b9804
10 changed files with 309 additions and 60 deletions

28
.idea/workspace.xml generated
View File

@@ -33,8 +33,17 @@
</configurations> </configurations>
</component> </component>
<component name="ChangeListManager"> <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.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- 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.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- 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$/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> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -128,7 +137,7 @@
<recent name="$PROJECT_DIR$/docs" /> <recent name="$PROJECT_DIR$/docs" />
</key> </key>
</component> </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"> <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"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
@@ -162,7 +171,7 @@
<option name="number" value="Default" /> <option name="number" value="Default" />
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1764457173148</updated> <updated>1764457173148</updated>
<workItem from="1764457174208" duration="42867000" /> <workItem from="1764457174208" duration="46874000" />
</task> </task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions."> <task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -220,7 +229,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1764501532446</updated> <updated>1764501532446</updated>
</task> </task>
<option name="localTasksCounter" value="8" /> <task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- 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 /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <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 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.&#10;&#10;- Document `TestFrontend` for programmatic testing, including examples and usage details.&#10;- Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." /> <MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update.&#10;&#10;- Document `TestFrontend` for programmatic testing, including examples and usage details.&#10;- Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." /> <MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- 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.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." /> <MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- 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.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." />
</component> </component>
<component name="XSLT-Support.FileAssociations.UIState"> <component name="XSLT-Support.FileAssociations.UIState">
<expand /> <expand />

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17) 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. # Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -167,11 +167,11 @@ if (${BUILD_GUI})
install(TARGETS kge install(TARGETS kge
BUNDLE DESTINATION . BUNDLE DESTINATION .
) )
else() else ()
install(TARGETS kge install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
) )
endif() endif ()
# Install kge man page only when GUI is built # Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1) install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
endif () endif ()

View File

@@ -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 --- // --- Buffers: switch/next/prev/close ---
static bool static bool
cmd_buffer_switch_start(CommandContext &ctx) cmd_buffer_switch_start(CommandContext &ctx)
@@ -990,6 +1000,37 @@ cmd_newline(CommandContext &ctx)
} else { } else {
ctx.editor.SetStatus("Nothing to confirm"); 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; return true;
} }
@@ -2372,6 +2413,10 @@ InstallDefaultCommands()
CommandRegistry::Register({ CommandRegistry::Register({
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to 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 // Undo/Redo
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo}); CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo}); CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});

View File

@@ -72,6 +72,8 @@ enum class CommandId {
// Buffer operations // Buffer operations
ReloadBuffer, // reload buffer from disk (C-k l) ReloadBuffer, // reload buffer from disk (C-k l)
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a) 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 // Meta
UnknownKCommand, // arg: single character that was not recognized after C-k UnknownKCommand, // arg: single character that was not recognized after C-k
}; };

View File

@@ -302,7 +302,7 @@ public:
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) --- // --- 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) void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)

View File

@@ -279,6 +279,24 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
MappedInput mi; MappedInput mi;
bool produced = false; bool produced = false;
switch (e.type) { 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: { case SDL_KEYDOWN: {
// Remember state before mapping; used for TEXTINPUT suppression heuristics // Remember state before mapping; used for TEXTINPUT suppression heuristics
const bool was_k_prefix = k_prefix_; const bool was_k_prefix = k_prefix_;

View File

@@ -45,7 +45,7 @@ GUIRenderer::Draw(Editor &ed)
const auto &lines = buf->Rows(); const auto &lines = buf->Rows();
// Reserve space for status bar at bottom // Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar); ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Detect click-to-move inside this scroll region // Detect click-to-move inside this scroll region
ImVec2 list_origin = ImGui::GetCursorScreenPos(); ImVec2 list_origin = ImGui::GetCursorScreenPos();
float scroll_y = ImGui::GetScrollY(); float scroll_y = ImGui::GetScrollY();
@@ -56,24 +56,42 @@ GUIRenderer::Draw(Editor &ed)
const float line_h = ImGui::GetTextLineHeight(); const float line_h = ImGui::GetTextLineHeight();
const float row_h = ImGui::GetTextLineHeightWithSpacing(); const float row_h = ImGui::GetTextLineHeightWithSpacing();
const float space_w = ImGui::CalcTextSize(" ").x; const float space_w = ImGui::CalcTextSize(" ").x;
// If the command layer requested a specific top-of-screen (via Buffer::Rowoffs), // Two-way sync between Buffer::Rowoffs and ImGui scroll position:
// force the ImGui scroll to match so paging aligns the first visible row. // - 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; bool forced_scroll = false;
{ {
std::size_t desired_top = buf->Rowoffs(); static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
long current_top = static_cast<long>(scroll_y / row_h); static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
if (static_cast<long>(desired_top) != current_top) {
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h); 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(); scroll_y = ImGui::GetScrollY();
forced_scroll = true; 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. // Synchronize cursor and scrolling.
// A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row. // Ensure the cursor is visible even on the first frame or when it didn't move,
// B) When the cursor moves (via keyboard commands), scroll it back into view. // 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 // Compute visible row range using the child window height
float child_h = ImGui::GetWindowHeight(); float child_h = ImGui::GetWindowHeight();
long first_row = static_cast<long>(scroll_y / row_h); long first_row = static_cast<long>(scroll_y / row_h);
@@ -82,37 +100,7 @@ GUIRenderer::Draw(Editor &ed)
vis_rows = 1; vis_rows = 1;
long last_row = first_row + 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 if (!forced_scroll) {
// 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) {
long cyr = static_cast<long>(cy); long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) { if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h; 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; 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 // Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { 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::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
std::string right = rbuf; std::string right = rbuf;
// Middle message // Middle message: if a prompt is active, show "Label: text"; otherwise show status
const std::string &msg = ed.Status(); std::string msg;
if (ed.PromptActive()) {
msg = ed.PromptLabel();
if (!msg.empty())
msg += ": ";
msg += ed.PromptText();
} else {
msg = ed.Status();
}
// Measurements // Measurements
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str()); ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());

View File

@@ -86,6 +86,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'a': case 'a':
out = CommandId::MarkAllAndJumpEnd; out = CommandId::MarkAllAndJumpEnd;
return true; // C-k a (mark all and jump to end) return true; // C-k a (mark all and jump to end)
case 'g':
out = CommandId::JumpToLine;
return true; // C-k g (goto line)
default: default:
break; break;
} }

View File

@@ -35,6 +35,19 @@ map_key_to_command(const int ch,
case KEY_MOUSE: { case KEY_MOUSE: {
MEVENT ev{}; MEVENT ev{};
if (getmouse(&ev) == OK) { 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 // React to left button click/press
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) { if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
char buf[64]; char buf[64];

165
main.cc
View File

@@ -1,7 +1,12 @@
#include <iostream> #include <iostream>
#include <memory>
#include <string> #include <string>
#include <cctype>
#include <unistd.h> #include <unistd.h>
#include <getopt.h> #include <getopt.h>
#include <signal.h>
#include <cstdio>
#include <sys/stat.h>
#include "Editor.h" #include "Editor.h"
#include "Command.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 int
main(int argc, const char *argv[]) main(int argc, const char *argv[])
{ {
@@ -104,18 +212,67 @@ main(int argc, const char *argv[])
use_gui = false; use_gui = false;
#endif #endif
} }
// If using GUI, detach from the controlling terminal so the terminal can be closed.
DetachFromTerminalIfGUI(use_gui);
#endif #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) { if (optind < argc) {
std::size_t pending_line = 0; // 0 = no pending line
for (int i = optind; i < argc; ++i) { 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; std::string err;
const std::string path = argv[i]; const std::string path = arg;
if (!editor.OpenFile(path, err)) { if (!editor.OpenFile(path, err)) {
editor.SetStatus("open: " + err); editor.SetStatus("open: " + err);
std::cerr << "kte: " << err << "\n"; 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 { } else {
// Create a single empty buffer // Create a single empty buffer
editor.AddBuffer(Buffer()); editor.AddBuffer(Buffer());
@@ -129,11 +286,11 @@ main(int argc, const char *argv[])
std::unique_ptr<Frontend> fe; std::unique_ptr<Frontend> fe;
#if defined(KTE_BUILD_GUI) #if defined(KTE_BUILD_GUI)
if (use_gui) { if (use_gui) {
fe.reset(new GUIFrontend()); fe = std::make_unique<GUIFrontend>();
} else } else
#endif #endif
{ {
fe.reset(new TerminalFrontend()); fe = std::make_unique<TerminalFrontend>();
} }
if (!fe->Init(editor)) { if (!fe->Init(editor)) {