From b41946c47076d4413afeff3183653eed34bb420c Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 29 Nov 2025 21:33:45 -0800 Subject: [PATCH] Various minor bug cleanups. --- .idea/workspace.xml | 18 ++- .junie/guidelines.md | 8 +- Command.cc | 69 ++++++--- Command.h | 88 +++++------ Editor.cc | 8 +- Editor.h | 29 ++++ GUIFrontend.cc | 154 ++++++++++---------- GUIFrontend.h | 24 +-- GUIInputHandler.cc | 226 +++++++++++++++------------- GUIInputHandler.h | 3 + GUIRenderer.cc | 315 +++++++++++++++++++++------------------- KKeymap.cc | 100 ++++++++++--- KKeymap.h | 8 + TerminalFrontend.cc | 42 ++++-- TerminalInputHandler.cc | 100 ++++++------- TerminalInputHandler.h | 16 +- TerminalRenderer.cc | 208 +++++++++++++------------- TerminalRenderer.h | 7 +- 18 files changed, 803 insertions(+), 620 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index e6284e8..e272582 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -35,7 +35,23 @@ + + + + + + + + + + + + + + + + diff --git a/.junie/guidelines.md b/.junie/guidelines.md index 2b32fd8..636a722 100644 --- a/.junie/guidelines.md +++ b/.junie/guidelines.md @@ -8,6 +8,8 @@ WordStar/VDE family and emacs. The spiritual parent is `mg(1)`. These guidelines summarize the goals, interfaces, key operations, and current development practices for kte. +Style note: all code should be formatted with the current CLion C++ style. + ## Goals - Keep the core small, fast, and understandable. @@ -28,10 +30,10 @@ Prerequisites: a C++17 compiler, CMake, and ncurses development headers/libs. - Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev` - Configure and build (example): - - `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug` - - `cmake --build cmake-build-debug` + - `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug` + - `cmake --build cmake-build-debug` - Run: - - `./cmake-build-debug/kte [files]` + - `./cmake-build-debug/kte [files]` Project entry point: `main.cpp` diff --git a/Command.cc b/Command.cc index 28e0309..3c59460 100644 --- a/Command.cc +++ b/Command.cc @@ -296,7 +296,22 @@ cmd_save_as(CommandContext &ctx) static bool cmd_quit(CommandContext &ctx) { - // Placeholder: actual app loop should react to this status or a future flag + Buffer *buf = ctx.editor.CurrentBuffer(); + // If a confirmation is already pending, quit now without saving + if (ctx.editor.QuitConfirmPending()) { + ctx.editor.SetQuitConfirmPending(false); + ctx.editor.SetQuitRequested(true); + ctx.editor.SetStatus("Quit requested"); + return true; + } + // If current buffer exists and is dirty, warn and arm confirmation + if (buf && buf->Dirty()) { + ctx.editor.SetStatus("Unsaved changes. C-k q to quit without saving"); + ctx.editor.SetQuitConfirmPending(true); + return true; + } + // Otherwise quit immediately + ctx.editor.SetQuitRequested(true); ctx.editor.SetStatus("Quit requested"); return true; } @@ -329,6 +344,16 @@ cmd_save_and_quit(CommandContext &ctx) } } ctx.editor.SetStatus("Save and quit requested"); + ctx.editor.SetQuitRequested(true); + return true; +} + + +static bool +cmd_quit_now(CommandContext &ctx) +{ + ctx.editor.SetQuitRequested(true); + ctx.editor.SetStatus("Quit requested"); return true; } @@ -1065,24 +1090,7 @@ cmd_word_next(CommandContext &ctx) while (repeat-- > 0) { if (y >= rows.size()) break; - // Skip whitespace to the right - while (y < rows.size()) { - if (y >= rows.size()) - break; - if (x < rows[y].size() && std::isspace(static_cast(rows[y][x]))) { - ++x; - continue; - } - if (x >= rows[y].size()) { - if (y + 1 >= rows.size()) - break; - ++y; - x = 0; - continue; - } - break; - } - // Skip word characters to the right + // First, if currently on a word, skip to its end while (y < rows.size()) { if (x < rows[y].size() && is_word_char(static_cast(rows[y][x]))) { ++x; @@ -1097,6 +1105,23 @@ cmd_word_next(CommandContext &ctx) } break; } + // Then, skip any non-word characters (including punctuation and whitespace) + while (y < rows.size()) { + if (x < rows[y].size()) { + unsigned char c = static_cast(rows[y][x]); + if (is_word_char(c)) + break; + ++x; + continue; + } + if (x >= rows[y].size()) { + if (y + 1 >= rows.size()) + break; + ++y; + x = 0; + continue; + } + } } buf->SetCursor(x, y); ensure_cursor_visible(ctx.editor, *buf); @@ -1163,6 +1188,7 @@ InstallDefaultCommands() CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save}); CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as}); CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit}); + CommandRegistry::Register({CommandId::QuitNow, "quit-now", "Quit editor immediately", cmd_quit_now}); CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit}); CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh}); CommandRegistry::Register( @@ -1205,6 +1231,11 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count) const Command *cmd = CommandRegistry::FindById(id); if (!cmd) return false; + // If a quit confirmation was pending and the user invoked something other + // than the soft quit again, cancel the pending confirmation. + if (ed.QuitConfirmPending() && id != CommandId::Quit && id != CommandId::KPrefix) { + ed.SetQuitConfirmPending(false); + } CommandContext ctx{ed, arg, count}; return cmd->handler ? cmd->handler(ctx) : false; } diff --git a/Command.h b/Command.h index a9196fa..6d37725 100644 --- a/Command.h +++ b/Command.h @@ -14,47 +14,48 @@ class Editor; // Identifiers for editor commands. This is intentionally small for now and // will grow as features are implemented. enum class CommandId { - Save, - SaveAs, - // Placeholders for future commands from ke's model - Quit, - SaveAndQuit, - Refresh, // force redraw - KPrefix, // show "C-k _" prompt in status when entering k-command - FindStart, // begin incremental search (placeholder) - OpenFileStart, // begin open-file prompt - // Editing - InsertText, // arg: text to insert at cursor (UTF-8, no newlines) - Newline, // insert a newline at cursor - Backspace, // delete char before cursor (may join lines) - DeleteChar, // delete char at cursor (may join lines) - // Navigation (basic) - MoveLeft, - MoveRight, - MoveUp, - MoveDown, - MoveHome, - MoveEnd, - PageUp, - PageDown, - WordPrev, - WordNext, - // Direct cursor placement - MoveCursorTo, // arg: "y:x" (zero-based row:col) - // Meta - UnknownKCommand, // arg: single character that was not recognized after C-k + Save, + SaveAs, + // Placeholders for future commands from ke's model + Quit, + QuitNow, // immediate quit, no confirmation + SaveAndQuit, + Refresh, // force redraw + KPrefix, // show "C-k _" prompt in status when entering k-command + FindStart, // begin incremental search (placeholder) + OpenFileStart, // begin open-file prompt + // Editing + InsertText, // arg: text to insert at cursor (UTF-8, no newlines) + Newline, // insert a newline at cursor + Backspace, // delete char before cursor (may join lines) + DeleteChar, // delete char at cursor (may join lines) + // Navigation (basic) + MoveLeft, + MoveRight, + MoveUp, + MoveDown, + MoveHome, + MoveEnd, + PageUp, + PageDown, + WordPrev, + WordNext, + // Direct cursor placement + MoveCursorTo, // arg: "y:x" (zero-based row:col) + // Meta + UnknownKCommand, // arg: single character that was not recognized after C-k }; // Context passed to command handlers. struct CommandContext { - Editor &editor; + Editor &editor; - // Optional argument string (e.g., filename for SaveAs). - std::string arg; + // Optional argument string (e.g., filename for SaveAs). + std::string arg; - // Optional repeat count (C-u support). 0 means not provided. - int count = 0; + // Optional repeat count (C-u support). 0 means not provided. + int count = 0; }; @@ -62,26 +63,26 @@ using CommandHandler = std::function; // return true on struct Command { - CommandId id; - std::string name; // stable, unique name (e.g., "save", "save-as") - std::string help; // short help/description - CommandHandler handler; + CommandId id; + std::string name; // stable, unique name (e.g., "save", "save-as") + std::string help; // short help/description + CommandHandler handler; }; // Simple global registry. Not thread-safe; suitable for this app. class CommandRegistry { public: - static void Register(const Command &cmd); + static void Register(const Command &cmd); - static const Command *FindById(CommandId id); + static const Command *FindById(CommandId id); - static const Command *FindByName(const std::string &name); + static const Command *FindByName(const std::string &name); - static const std::vector &All(); + static const std::vector &All(); private: - static std::vector &storage_(); + static std::vector &storage_(); }; @@ -91,6 +92,7 @@ void InstallDefaultCommands(); // Dispatcher entry points for the input layer // Returns true if the command executed successfully. bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0); + bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0); #endif // KTE_COMMAND_H diff --git a/Editor.cc b/Editor.cc index 8752bfa..7cc257c 100644 --- a/Editor.cc +++ b/Editor.cc @@ -113,9 +113,11 @@ Editor::Reset() no_kill_ = 0; dirtyex_ = 0; msg_.clear(); - msgtm_ = 0; - uarg_ = 0; - ucount_ = 0; + msgtm_ = 0; + uarg_ = 0; + ucount_ = 0; + quit_requested_ = false; + quit_confirm_pending_ = false; buffers_.clear(); curbuf_ = 0; } diff --git a/Editor.h b/Editor.h index 95b75fa..f42e060 100644 --- a/Editor.h +++ b/Editor.h @@ -111,6 +111,31 @@ public: } + // --- Quit/Exit state --- + void SetQuitRequested(bool on) + { + quit_requested_ = on; + } + + + [[nodiscard]] bool QuitRequested() const + { + return quit_requested_; + } + + + void SetQuitConfirmPending(bool on) + { + quit_confirm_pending_ = on; + } + + + [[nodiscard]] bool QuitConfirmPending() const + { + return quit_confirm_pending_; + } + + [[nodiscard]] std::time_t StatusTime() const { return msgtm_; @@ -357,6 +382,10 @@ private: std::vector buffers_; std::size_t curbuf_ = 0; // index into buffers_ + // Quit state + bool quit_requested_ = false; + bool quit_confirm_pending_ = false; + // Search state bool search_active_ = false; std::string search_query_; diff --git a/GUIFrontend.cc b/GUIFrontend.cc index 4688cb7..1cd2df5 100644 --- a/GUIFrontend.cc +++ b/GUIFrontend.cc @@ -19,7 +19,7 @@ static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatib bool GUIFrontend::Init(Editor &ed) { - (void)ed; // editor dimensions will be initialized during the first Step() frame + (void) ed; // editor dimensions will be initialized during the first Step() frame if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) { return false; } @@ -49,25 +49,25 @@ GUIFrontend::Init(Editor &ed) IMGUI_CHECKVERSION(); ImGui::CreateContext(); - ImGuiIO &io = ImGui::GetIO(); - (void) io; - ImGui::StyleColorsDark(); + ImGuiIO &io = ImGui::GetIO(); + (void) io; + ImGui::StyleColorsDark(); if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_)) return false; if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) return false; - // Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists - int w, h; - SDL_GetWindowSize(window_, &w, &h); - width_ = w; - height_ = h; + // Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists + int w, h; + SDL_GetWindowSize(window_, &w, &h); + width_ = w; + height_ = h; - // Initialize GUI font from embedded default - LoadGuiFont_(nullptr, 16.f); + // Initialize GUI font from embedded default + LoadGuiFont_(nullptr, 16.f); - return true; + return true; } @@ -78,77 +78,80 @@ GUIFrontend::Step(Editor &ed, bool &running) while (SDL_PollEvent(&e)) { ImGui_ImplSDL2_ProcessEvent(&e); switch (e.type) { - case SDL_QUIT: - running = false; - break; - case SDL_WINDOWEVENT: - if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { - width_ = e.window.data1; - height_ = e.window.data2; - } - break; - default: - break; + case SDL_QUIT: + running = false; + break; + case SDL_WINDOWEVENT: + if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) { + width_ = e.window.data1; + height_ = e.window.data2; + } + break; + default: + break; } // Map input to commands input_.ProcessSDLEvent(e); } - // Execute pending mapped inputs (drain queue) + // Execute pending mapped inputs (drain queue) for (;;) { MappedInput mi; if (!input_.Poll(mi)) break; if (mi.hasCommand) { Execute(ed, mi.id, mi.arg, mi.count); - if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) { - running = false; - } } } - // Start a new ImGui frame - ImGui_ImplOpenGL3_NewFrame(); - ImGui_ImplSDL2_NewFrame(window_); - ImGui::NewFrame(); + if (ed.QuitRequested()) { + running = false; + } - // Update editor logical rows/cols using current ImGui metrics and display size - { - ImGuiIO &io = ImGui::GetIO(); - float line_h = ImGui::GetTextLineHeightWithSpacing(); - float ch_w = ImGui::CalcTextSize("M").x; - if (line_h <= 0.0f) line_h = 16.0f; - if (ch_w <= 0.0f) ch_w = 8.0f; - // Prefer ImGui IO display size; fall back to cached SDL window size - float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast(width_); - float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast(height_); + // Start a new ImGui frame + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplSDL2_NewFrame(window_); + ImGui::NewFrame(); - // Account for the GUI window padding and the status bar height used in GUIRenderer - const ImGuiStyle &style = ImGui::GetStyle(); - float pad_x = style.WindowPadding.x; - float pad_y = style.WindowPadding.y; - // Status bar reserves one frame height (with spacing) inside the window - float status_h = ImGui::GetFrameHeightWithSpacing(); + // Update editor logical rows/cols using current ImGui metrics and display size + { + ImGuiIO &io = ImGui::GetIO(); + float line_h = ImGui::GetTextLineHeightWithSpacing(); + float ch_w = ImGui::CalcTextSize("M").x; + if (line_h <= 0.0f) + line_h = 16.0f; + if (ch_w <= 0.0f) + ch_w = 8.0f; + // Prefer ImGui IO display size; fall back to cached SDL window size + float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast(width_); + float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast(height_); - float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x); - float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h); + // Account for the GUI window padding and the status bar height used in GUIRenderer + const ImGuiStyle &style = ImGui::GetStyle(); + float pad_x = style.WindowPadding.x; + float pad_y = style.WindowPadding.y; + // Status bar reserves one frame height (with spacing) inside the window + float status_h = ImGui::GetFrameHeightWithSpacing(); - // Visible content rows inside the scroll child - std::size_t content_rows = static_cast(std::floor(avail_h / line_h)); - // Editor::Rows includes the status line; add 1 back for it. - std::size_t rows = std::max(1, content_rows + 1); - std::size_t cols = static_cast(std::max(1.0f, std::floor(avail_w / ch_w))); + float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x); + float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h); - // Only update if changed to avoid churn - if (rows != ed.Rows() || cols != ed.Cols()) { - ed.SetDimensions(rows, cols); - } - } + // Visible content rows inside the scroll child + std::size_t content_rows = static_cast(std::floor(avail_h / line_h)); + // Editor::Rows includes the status line; add 1 back for it. + std::size_t rows = std::max(1, content_rows + 1); + std::size_t cols = static_cast(std::max(1.0f, std::floor(avail_w / ch_w))); - // No runtime font UI; always use embedded font. + // Only update if changed to avoid churn + if (rows != ed.Rows() || cols != ed.Cols()) { + ed.SetDimensions(rows, cols); + } + } - // Draw editor UI - renderer_.Draw(ed); + // No runtime font UI; always use embedded font. + + // Draw editor UI + renderer_.Draw(ed); // Render ImGui::Render(); @@ -184,18 +187,19 @@ GUIFrontend::Shutdown() bool GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px) { - ImGuiIO &io = ImGui::GetIO(); - io.Fonts->Clear(); - ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( - (void*)DefaultFontRegularCompressedData, - (int)DefaultFontRegularCompressedSize, - size_px); - if (!font) { - font = io.Fonts->AddFontDefault(); - } - (void) font; - io.Fonts->Build(); - return true; + ImGuiIO &io = ImGui::GetIO(); + io.Fonts->Clear(); + ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( + (void *) DefaultFontRegularCompressedData, + (int) DefaultFontRegularCompressedSize, + size_px); + if (!font) { + font = io.Fonts->AddFontDefault(); + } + (void) font; + io.Fonts->Build(); + return true; } -// No runtime font reload or system font resolution in this simplified build. \ No newline at end of file + +// No runtime font reload or system font resolution in this simplified build. diff --git a/GUIFrontend.h b/GUIFrontend.h index 15b6446..71c0487 100644 --- a/GUIFrontend.h +++ b/GUIFrontend.h @@ -13,25 +13,25 @@ typedef void *SDL_GLContext; class GUIFrontend final : public Frontend { public: - GUIFrontend() = default; + GUIFrontend() = default; - ~GUIFrontend() override = default; + ~GUIFrontend() override = default; - bool Init(Editor &ed) override; + bool Init(Editor &ed) override; - void Step(Editor &ed, bool &running) override; + void Step(Editor &ed, bool &running) override; - void Shutdown() override; + void Shutdown() override; private: - bool LoadGuiFont_(const char *path, float size_px); + bool LoadGuiFont_(const char *path, float size_px); - GUIInputHandler input_{}; - GUIRenderer renderer_{}; - SDL_Window *window_ = nullptr; - SDL_GLContext gl_ctx_ = nullptr; - int width_ = 1280; - int height_ = 800; + GUIInputHandler input_{}; + GUIRenderer renderer_{}; + SDL_Window *window_ = nullptr; + SDL_GLContext gl_ctx_ = nullptr; + int width_ = 1280; + int height_ = 800; }; #endif // KTE_GUI_FRONTEND_H diff --git a/GUIInputHandler.cc b/GUIInputHandler.cc index 85a266b..e46532b 100644 --- a/GUIInputHandler.cc +++ b/GUIInputHandler.cc @@ -7,12 +7,12 @@ static bool map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput &out) { - // Ctrl handling - const bool is_ctrl = (mod & KMOD_CTRL) != 0; - const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0; + // Ctrl handling + const bool is_ctrl = (mod & KMOD_CTRL) != 0; + const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0; - // Movement and basic keys - switch (key) { + // Movement and basic keys + switch (key) { case SDLK_LEFT: out = {true, CommandId::MoveLeft, "", 0}; return true; @@ -28,21 +28,28 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput case SDLK_HOME: out = {true, CommandId::MoveHome, "", 0}; return true; - case SDLK_END: - out = {true, CommandId::MoveEnd, "", 0}; - return true; - case SDLK_PAGEUP: - out = {true, CommandId::PageUp, "", 0}; - return true; - case SDLK_PAGEDOWN: - out = {true, CommandId::PageDown, "", 0}; - return true; - case SDLK_DELETE: - out = {true, CommandId::DeleteChar, "", 0}; - return true; + case SDLK_END: + out = {true, CommandId::MoveEnd, "", 0}; + return true; + case SDLK_PAGEUP: + out = {true, CommandId::PageUp, "", 0}; + return true; + case SDLK_PAGEDOWN: + out = {true, CommandId::PageDown, "", 0}; + return true; + case SDLK_DELETE: + out = {true, CommandId::DeleteChar, "", 0}; + return true; case SDLK_BACKSPACE: out = {true, CommandId::Backspace, "", 0}; return true; + case SDLK_TAB: + // Insert a literal tab character + out.hasCommand = true; + out.id = CommandId::InsertText; + out.arg = "\t"; + out.count = 0; + return true; case SDLK_RETURN: case SDLK_KP_ENTER: out = {true, CommandId::Newline, "", 0}; @@ -55,90 +62,59 @@ map_key(const SDL_Keycode key, const SDL_Keymod mod, bool &k_prefix, MappedInput break; } - if (is_ctrl) { - switch (key) { - case SDLK_k: - case SDLK_KP_EQUALS: // treat Ctrl-K - k_prefix = true; - out = {true, CommandId::KPrefix, "", 0}; - return true; - case SDLK_n: // C-n: down - out = {true, CommandId::MoveDown, "", 0}; - return true; - case SDLK_p: // C-p: up - out = {true, CommandId::MoveUp, "", 0}; - return true; - case SDLK_f: // C-f: right - out = {true, CommandId::MoveRight, "", 0}; - return true; - case SDLK_b: // C-b: left - out = {true, CommandId::MoveLeft, "", 0}; - return true; - case SDLK_a: - out = {true, CommandId::MoveHome, "", 0}; - return true; - case SDLK_e: - out = {true, CommandId::MoveEnd, "", 0}; - return true; - case SDLK_g: - k_prefix = false; - out = {true, CommandId::Refresh, "", 0}; - return true; - case SDLK_l: - out = {true, CommandId::Refresh, "", 0}; - return true; - case SDLK_s: - out = {true, CommandId::FindStart, "", 0}; - return true; - case SDLK_q: - out = {true, CommandId::Quit, "", 0}; - return true; - case SDLK_x: - out = {true, CommandId::SaveAndQuit, "", 0}; - return true; - default: - break; - } - } + if (is_ctrl) { + if (key == SDLK_k || key == SDLK_KP_EQUALS) { + k_prefix = true; + out = {true, CommandId::KPrefix, "", 0}; + return true; + } + // Map other control chords via shared keymap + if (key >= SDLK_a && key <= SDLK_z) { + int ascii_key = static_cast('a' + (key - SDLK_a)); + CommandId id; + if (KLookupCtrlCommand(ascii_key, id)) { + out = {true, id, "", 0}; + return true; + } + } + } - // Alt/Meta bindings (ESC f/b equivalent) - if (is_alt) { - switch (key) { - case SDLK_b: - out = {true, CommandId::WordPrev, "", 0}; - return true; - case SDLK_f: - out = {true, CommandId::WordNext, "", 0}; - return true; - default: - break; - } - } + // Alt/Meta bindings (ESC f/b equivalent) + if (is_alt) { + if (key >= SDLK_a && key <= SDLK_z) { + int ascii_key = static_cast('a' + (key - SDLK_a)); + CommandId id; + if (KLookupEscCommand(ascii_key, id)) { + out = {true, id, "", 0}; + return true; + } + } + } - if (k_prefix) { - k_prefix = false; - // Normalize SDL key to ASCII where possible - int ascii_key = 0; - if (key >= SDLK_SPACE && key <= SDLK_z) { - ascii_key = static_cast(key); - } - bool ctrl2 = (mod & KMOD_CTRL) != 0; - if (ascii_key != 0) { - ascii_key = KLowerAscii(ascii_key); - CommandId id; - if (KLookupKCommand(ascii_key, ctrl2, id)) { - out = {true, id, "", 0}; - return true; - } - // Unknown k-command: report the typed character - char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; - std::string arg(1, c); - out = {true, CommandId::UnknownKCommand, arg, 0}; - return true; - } - out.hasCommand = false; - return true; - } + if (k_prefix) { + k_prefix = false; + // Normalize SDL key to ASCII where possible + int ascii_key = 0; + if (key >= SDLK_SPACE && key <= SDLK_z) { + ascii_key = static_cast(key); + } + bool ctrl2 = (mod & KMOD_CTRL) != 0; + if (ascii_key != 0) { + ascii_key = KLowerAscii(ascii_key); + CommandId id; + if (KLookupKCommand(ascii_key, ctrl2, id)) { + out = {true, id, "", 0}; + return true; + } + // Unknown k-command: report the typed character + char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; + std::string arg(1, c); + out = {true, CommandId::UnknownKCommand, arg, 0}; + return true; + } + out.hasCommand = false; + return true; + } return false; } @@ -150,16 +126,60 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e) MappedInput mi; bool produced = false; switch (e.type) { - case SDL_KEYDOWN: - produced = map_key(e.key.keysym.sym, SDL_Keymod(e.key.keysym.mod), k_prefix_, mi); - break; + case SDL_KEYDOWN: { + // Remember whether we were in k-prefix before handling this key + bool was_k_prefix = k_prefix_; + SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod); + const SDL_Keycode key = e.key.keysym.sym; + produced = map_key(key, mods, k_prefix_, mi); + // Suppress the immediate following SDL_TEXTINPUT only in cases where + // SDL would also emit a text input for the same physical keystroke: + // - k-prefix printable suffix keys (no Ctrl), and + // - Alt/Meta modified printable letters. + // Do NOT suppress for non-text keys like Tab/Enter/Backspace/arrows/etc., + // otherwise the next normal character would be dropped. + if (produced && mi.hasCommand) { + const bool is_ctrl = (mods & KMOD_CTRL) != 0; + const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0; + const bool is_printable_letter = (key >= SDLK_SPACE && key <= SDLK_z); + const bool is_non_text_key = + key == SDLK_TAB || key == SDLK_RETURN || key == SDLK_KP_ENTER || + key == SDLK_BACKSPACE || key == SDLK_DELETE || key == SDLK_ESCAPE || + key == SDLK_LEFT || key == SDLK_RIGHT || key == SDLK_UP || key == SDLK_DOWN || + key == SDLK_HOME || key == SDLK_END || key == SDLK_PAGEUP || key == SDLK_PAGEDOWN; + + bool should_suppress = false; + if (!is_non_text_key) { + // k-prefix then a printable key normally generates TEXTINPUT + if (was_k_prefix && is_printable_letter && !is_ctrl) { + should_suppress = true; + } + // Alt/Meta + letter can also generate TEXTINPUT on some platforms + if (is_alt && key >= SDLK_a && key <= SDLK_z) { + should_suppress = true; + } + } + if (should_suppress) { + suppress_text_input_once_ = true; + } + } + } + break; case SDL_TEXTINPUT: - if (e.text.text[0] != '\0') { + // Ignore text input while in k-prefix, or once after a command-producing keydown + if (suppress_text_input_once_) { + suppress_text_input_once_ = false; // consume suppression + produced = true; // consumed input + break; + } + if (!k_prefix_ && e.text.text[0] != '\0') { mi.hasCommand = true; mi.id = CommandId::InsertText; mi.arg = std::string(e.text.text); mi.count = 0; produced = true; + } else { + produced = true; // consumed while k-prefix is active } break; default: diff --git a/GUIInputHandler.h b/GUIInputHandler.h index 673200b..cb25f13 100644 --- a/GUIInputHandler.h +++ b/GUIInputHandler.h @@ -27,6 +27,9 @@ private: std::mutex mu_; std::queue q_; bool k_prefix_ = false; + // When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT + // event produced by SDL for the same keystroke to avoid inserting stray characters. + bool suppress_text_input_once_ = false; }; #endif // KTE_GUI_INPUT_HANDLER_H diff --git a/GUIRenderer.cc b/GUIRenderer.cc index d115b05..7034538 100644 --- a/GUIRenderer.cc +++ b/GUIRenderer.cc @@ -56,77 +56,81 @@ 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. - bool forced_scroll = false; - { - std::size_t desired_top = buf->Rowoffs(); - long current_top = static_cast(scroll_y / row_h); - if (static_cast(desired_top) != current_top) { - ImGui::SetScrollY(static_cast(desired_top) * row_h); - scroll_y = ImGui::GetScrollY(); - forced_scroll = true; - } - } - // 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. - { - 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(scroll_y / row_h); - long vis_rows = static_cast(child_h / row_h); - if (vis_rows < 1) vis_rows = 1; - long last_row = first_row + vis_rows - 1; + // 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. + bool forced_scroll = false; + { + std::size_t desired_top = buf->Rowoffs(); + long current_top = static_cast(scroll_y / row_h); + if (static_cast(desired_top) != current_top) { + ImGui::SetScrollY(static_cast(desired_top) * row_h); + scroll_y = ImGui::GetScrollY(); + forced_scroll = true; + } + } + // 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. + { + 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(scroll_y / row_h); + long vis_rows = static_cast(child_h / row_h); + if (vis_rows < 1) + 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 (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { - long cyr = static_cast(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(lines.size())) - new_row = static_cast(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(new_row)]; - new_col = std::min(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(cy); - // Update visible range again in case content changed - first_row = static_cast(ImGui::GetScrollY() / row_h); - last_row = first_row + vis_rows - 1; - } - } + // A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row + if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) { + long cyr = static_cast(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(lines.size())) + new_row = static_cast(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(new_row)]; + new_col = std::min(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(cy); + // Update visible range again in case content changed + first_row = static_cast(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(cy) != prev_cursor_y) { - long cyr = static_cast(cy); - if (cyr < first_row || cyr > last_row) { - float target = (static_cast(cyr) - std::max(0L, vis_rows / 2)) * row_h; - float max_y = ImGui::GetScrollMaxY(); - if (target < 0.f) target = 0.f; - if (max_y >= 0.f && target > max_y) target = max_y; - ImGui::SetScrollY(target); - // refresh local variables - scroll_y = ImGui::GetScrollY(); - first_row = static_cast(scroll_y / 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(cy) != prev_cursor_y) { + long cyr = static_cast(cy); + if (cyr < first_row || cyr > last_row) { + float target = (static_cast(cyr) - std::max(0L, vis_rows / 2)) * row_h; + float max_y = ImGui::GetScrollMaxY(); + if (target < 0.f) + target = 0.f; + if (max_y >= 0.f && target > max_y) + target = max_y; + ImGui::SetScrollY(target); + // refresh local variables + scroll_y = ImGui::GetScrollY(); + first_row = static_cast(scroll_y / row_h); + last_row = first_row + vis_rows - 1; + } + } - prev_scroll_y = ImGui::GetScrollY(); - prev_cursor_y = static_cast(cy); - } + prev_scroll_y = ImGui::GetScrollY(); + prev_cursor_y = static_cast(cy); + } // Handle mouse click before rendering to avoid dependent on drawn items if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) { ImVec2 mp = ImGui::GetIO().MousePos; @@ -190,98 +194,103 @@ GUIRenderer::Draw(Editor &ed) ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); } } - ImGui::EndChild(); + ImGui::EndChild(); - // Status bar spanning full width - ImGui::Separator(); + // Status bar spanning full width + ImGui::Separator(); - // Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark) - // Compute full content width and draw a filled background rectangle - ImVec2 win_pos = ImGui::GetWindowPos(); - ImVec2 cr_min = ImGui::GetWindowContentRegionMin(); - ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); - float x0 = win_pos.x + cr_min.x; - float x1 = win_pos.x + cr_max.x; - ImVec2 cursor = ImGui::GetCursorScreenPos(); - float bar_h = ImGui::GetFrameHeight(); - ImVec2 p0(x0, cursor.y); - ImVec2 p1(x1, cursor.y + bar_h); - ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive); - ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col); - // Build left text - std::string left; - left.reserve(256); - left += "kge"; // GUI app name - left += " "; - left += KTE_VERSION_STR; - std::string fname = buf->Filename(); - if (!fname.empty()) { - try { fname = std::filesystem::path(fname).filename().string(); } catch (...) {} - } else { - fname = "[no name]"; - } - left += " "; - left += fname; - if (buf->Dirty()) left += " *"; + // Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark) + // Compute full content width and draw a filled background rectangle + ImVec2 win_pos = ImGui::GetWindowPos(); + ImVec2 cr_min = ImGui::GetWindowContentRegionMin(); + ImVec2 cr_max = ImGui::GetWindowContentRegionMax(); + float x0 = win_pos.x + cr_min.x; + float x1 = win_pos.x + cr_max.x; + ImVec2 cursor = ImGui::GetCursorScreenPos(); + float bar_h = ImGui::GetFrameHeight(); + ImVec2 p0(x0, cursor.y); + ImVec2 p1(x1, cursor.y + bar_h); + ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive); + ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col); + // Build left text + std::string left; + left.reserve(256); + left += "kge"; // GUI app name + left += " "; + left += KTE_VERSION_STR; + std::string fname = buf->Filename(); + if (!fname.empty()) { + try { + fname = std::filesystem::path(fname).filename().string(); + } catch (...) {} + } else { + fname = "[no name]"; + } + left += " "; + left += fname; + if (buf->Dirty()) + left += " *"; - // Build right text (cursor/mark) - int row1 = static_cast(buf->Cury()) + 1; - int col1 = static_cast(buf->Curx()) + 1; - bool have_mark = buf->MarkSet(); - int mrow1 = have_mark ? static_cast(buf->MarkCury()) + 1 : 0; - int mcol1 = have_mark ? static_cast(buf->MarkCurx()) + 1 : 0; - char rbuf[128]; - if (have_mark) std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1); - else std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1); - std::string right = rbuf; + // Build right text (cursor/mark) + int row1 = static_cast(buf->Cury()) + 1; + int col1 = static_cast(buf->Curx()) + 1; + bool have_mark = buf->MarkSet(); + int mrow1 = have_mark ? static_cast(buf->MarkCury()) + 1 : 0; + int mcol1 = have_mark ? static_cast(buf->MarkCurx()) + 1 : 0; + char rbuf[128]; + if (have_mark) + std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1); + else + 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 + const std::string &msg = ed.Status(); - // Measurements - ImVec2 left_sz = ImGui::CalcTextSize(left.c_str()); - ImVec2 right_sz = ImGui::CalcTextSize(right.c_str()); - float pad = 6.f; - float left_x = p0.x + pad; - float right_x = p1.x - pad - right_sz.x; - if (right_x < left_x + left_sz.x + pad) { - // Not enough room; clip left to fit - float max_left = std::max(0.0f, right_x - left_x - pad); - if (max_left < left_sz.x && max_left > 10.0f) { - // Render a clipped left using a child region - ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f)); - ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true); - ImGui::TextUnformatted(left.c_str()); - ImGui::PopClipRect(); - } - } else { - // Draw left normally - ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f)); - ImGui::TextUnformatted(left.c_str()); - } + // Measurements + ImVec2 left_sz = ImGui::CalcTextSize(left.c_str()); + ImVec2 right_sz = ImGui::CalcTextSize(right.c_str()); + float pad = 6.f; + float left_x = p0.x + pad; + float right_x = p1.x - pad - right_sz.x; + if (right_x < left_x + left_sz.x + pad) { + // Not enough room; clip left to fit + float max_left = std::max(0.0f, right_x - left_x - pad); + if (max_left < left_sz.x && max_left > 10.0f) { + // Render a clipped left using a child region + ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f)); + ImGui::PushClipRect(ImVec2(left_x, p0.y), ImVec2(right_x - pad, p1.y), true); + ImGui::TextUnformatted(left.c_str()); + ImGui::PopClipRect(); + } + } else { + // Draw left normally + ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - left_sz.y) * 0.5f)); + ImGui::TextUnformatted(left.c_str()); + } - // Draw right - ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f)); - ImGui::TextUnformatted(right.c_str()); + // Draw right + ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f)); + ImGui::TextUnformatted(right.c_str()); - // Draw middle message centered in remaining space - if (!msg.empty()) { - float mid_left = left_x + left_sz.x + pad; - float mid_right = std::max(right_x - pad, mid_left); - float mid_w = std::max(0.0f, mid_right - mid_left); - if (mid_w > 1.0f) { - ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str()); - float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f); - // Clip to middle region - ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true); - ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f)); - ImGui::TextUnformatted(msg.c_str()); - ImGui::PopClipRect(); - } - } - // Advance cursor to after the bar to keep layout consistent - ImGui::Dummy(ImVec2(x1 - x0, bar_h)); - } + // Draw middle message centered in remaining space + if (!msg.empty()) { + float mid_left = left_x + left_sz.x + pad; + float mid_right = std::max(right_x - pad, mid_left); + float mid_w = std::max(0.0f, mid_right - mid_left); + if (mid_w > 1.0f) { + ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str()); + float msg_x = mid_left + std::max(0.0f, (mid_w - msg_sz.x) * 0.5f); + // Clip to middle region + ImGui::PushClipRect(ImVec2(mid_left, p0.y), ImVec2(mid_right, p1.y), true); + ImGui::SetCursorScreenPos(ImVec2(msg_x, p0.y + (bar_h - msg_sz.y) * 0.5f)); + ImGui::TextUnformatted(msg.c_str()); + ImGui::PopClipRect(); + } + } + // Advance cursor to after the bar to keep layout consistent + ImGui::Dummy(ImVec2(x1 - x0, bar_h)); + } ImGui::End(); ImGui::PopStyleVar(3); diff --git a/KKeymap.cc b/KKeymap.cc index 0445d37..adc820d 100644 --- a/KKeymap.cc +++ b/KKeymap.cc @@ -13,28 +13,88 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool out = CommandId::SaveAndQuit; return true; // C-k C-x case 'q': - out = CommandId::Quit; + out = CommandId::QuitNow; return true; // C-k C-q (quit immediately) default: break; } - } else { - switch (k) { - case 's': - out = CommandId::Save; - return true; // C-k s - case 'e': - out = CommandId::OpenFileStart; - return true; // C-k e (open file) - case 'x': - out = CommandId::SaveAndQuit; - return true; // C-k x - case 'q': - out = CommandId::Quit; - return true; // C-k q - default: - break; - } - } - return false; + } else { + switch (k) { + case 's': + out = CommandId::Save; + return true; // C-k s + case 'e': + out = CommandId::OpenFileStart; + return true; // C-k e (open file) + case 'x': + out = CommandId::SaveAndQuit; + return true; // C-k x + case 'q': + out = CommandId::Quit; + return true; // C-k q + default: + break; + } + } + return false; +} + + +auto +KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool +{ + const int k = KLowerAscii(ascii_key); + switch (k) { + case 'n': + out = CommandId::MoveDown; + return true; + case 'p': + out = CommandId::MoveUp; + return true; + case 'f': + out = CommandId::MoveRight; + return true; + case 'b': + out = CommandId::MoveLeft; + return true; + case 'a': + out = CommandId::MoveHome; + return true; + case 'e': + out = CommandId::MoveEnd; + return true; + case 's': + out = CommandId::FindStart; + return true; + case 'l': + out = CommandId::Refresh; + return true; + case 'g': + out = CommandId::Refresh; + return true; + case 'x': + out = CommandId::SaveAndQuit; // direct C-x mapping (GUI had this) + return true; + default: + break; + } + return false; +} + + +auto +KLookupEscCommand(const int ascii_key, CommandId &out) -> bool +{ + const int k = KLowerAscii(ascii_key); + switch (k) { + case 'b': + out = CommandId::WordPrev; + return true; + case 'f': + out = CommandId::WordNext; + return true; + default: + break; + } + return false; } diff --git a/KKeymap.h b/KKeymap.h index 01185dc..31e3611 100644 --- a/KKeymap.h +++ b/KKeymap.h @@ -13,6 +13,14 @@ // Returns true and sets out if a mapping exists; false otherwise. bool KLookupKCommand(int ascii_key, bool ctrl, CommandId &out); +// Lookup direct Control-chord commands (e.g., C-n, C-p, C-f, ...). +// ascii_key should be the lowercase ASCII of the letter (e.g., 'n' for C-n). +bool KLookupCtrlCommand(int ascii_key, CommandId &out); + +// Lookup ESC/Meta + key commands (e.g., ESC f/b). +// ascii_key should be the lowercase ASCII of the letter. +bool KLookupEscCommand(int ascii_key, CommandId &out); + // Utility: normalize an int keycode to lowercased ASCII if it's in printable range. inline int KLowerAscii(const int key) diff --git a/TerminalFrontend.cc b/TerminalFrontend.cc index e0630e0..b897d45 100644 --- a/TerminalFrontend.cc +++ b/TerminalFrontend.cc @@ -1,4 +1,5 @@ #include +#include #include #include "Editor.h" @@ -9,21 +10,29 @@ bool TerminalFrontend::Init(Editor &ed) { - initscr(); - cbreak(); - noecho(); - keypad(stdscr, TRUE); - // Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals) - meta(stdscr, TRUE); - // Make ESC key sequences resolve quickly so ESC+ works as meta + // Ensure Ctrl-S/Ctrl-Q reach the application by disabling XON/XOFF flow control + { + struct termios tio{}; + if (tcgetattr(STDIN_FILENO, &tio) == 0) { + tio.c_iflag &= static_cast(~IXON); + (void) tcsetattr(STDIN_FILENO, TCSANOW, &tio); + } + } + initscr(); + cbreak(); + noecho(); + keypad(stdscr, TRUE); + // Enable 8-bit meta key sequences (Alt/ESC-prefix handling in terminals) + meta(stdscr, TRUE); + // Make ESC key sequences resolve quickly so ESC+ works as meta #ifdef set_escdelay - set_escdelay(50); + set_escdelay(50); #endif - nodelay(stdscr, TRUE); - curs_set(1); - // Enable mouse support if available - mouseinterval(0); - mousemask(ALL_MOUSE_EVENTS, nullptr); + nodelay(stdscr, TRUE); + curs_set(1); + // Enable mouse support if available + mouseinterval(0); + mousemask(ALL_MOUSE_EVENTS, nullptr); int r = 0, c = 0; getmaxyx(stdscr, r, c); @@ -52,15 +61,16 @@ TerminalFrontend::Step(Editor &ed, bool &running) if (input_.Poll(mi)) { if (mi.hasCommand) { Execute(ed, mi.id, mi.arg, mi.count); - if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) { - running = false; - } } } else { // Avoid busy loop usleep(1000); } + if (ed.QuitRequested()) { + running = false; + } + renderer_.Draw(ed); } diff --git a/TerminalInputHandler.cc b/TerminalInputHandler.cc index 74159cc..3033476 100644 --- a/TerminalInputHandler.cc +++ b/TerminalInputHandler.cc @@ -95,38 +95,23 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou out = {true, CommandId::Refresh, "", 0}; return true; } - if (ch == CTRL('L')) { - out = {true, CommandId::Refresh, "", 0}; + // Tab (note: terminals encode Tab and C-i as the same code 9) + if (ch == '\t') { + k_prefix = false; + out.hasCommand = true; + out.id = CommandId::InsertText; + out.arg = "\t"; + out.count = 0; return true; } - if (ch == CTRL('S')) { - out = {true, CommandId::FindStart, "", 0}; - return true; - } - // Emacs-style movement aliases - if (ch == CTRL('N')) { // C-n: down - out = {true, CommandId::MoveDown, "", 0}; - return true; - } - if (ch == CTRL('P')) { // C-p: up - out = {true, CommandId::MoveUp, "", 0}; - return true; - } - if (ch == CTRL('F')) { // C-f: right/forward - out = {true, CommandId::MoveRight, "", 0}; - return true; - } - if (ch == CTRL('B')) { // C-b: left/back - out = {true, CommandId::MoveLeft, "", 0}; - return true; - } - if (ch == CTRL('A')) { - out = {true, CommandId::MoveHome, "", 0}; - return true; - } - if (ch == CTRL('E')) { - out = {true, CommandId::MoveEnd, "", 0}; - return true; + // Generic Control-chord lookup (after handling special prefixes/cancel) + if (ch >= 1 && ch <= 26) { + int ascii_key = 'a' + (ch - 1); + CommandId id; + if (KLookupCtrlCommand(ascii_key, id)) { + out = {true, id, "", 0}; + return true; + } } // Enter @@ -142,18 +127,15 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou return true; } - // If previous key was ESC, interpret as meta + // If previous key was ESC, interpret as meta and use ESC keymap if (esc_meta) { esc_meta = false; int ascii_key = ch; if (ascii_key >= 'A' && ascii_key <= 'Z') ascii_key = ascii_key - 'A' + 'a'; - if (ascii_key == 'b') { - out = {true, CommandId::WordPrev, "", 0}; - return true; - } - if (ascii_key == 'f') { - out = {true, CommandId::WordNext, "", 0}; + CommandId id; + if (KLookupEscCommand(ascii_key, id)) { + out = {true, id, "", 0}; return true; } // Unhandled meta key: no command @@ -161,29 +143,29 @@ map_key_to_command(const int ch, bool &k_prefix, bool &esc_meta, MappedInput &ou return true; } - if (k_prefix) { - k_prefix = false; // single next key only - // Determine if this is a control chord (e.g., C-x) and normalize - bool ctrl = false; - int ascii_key = ch; - if (ch >= 1 && ch <= 26) { - ctrl = true; - ascii_key = 'a' + (ch - 1); - } - // For letters, normalize to lowercase ASCII - ascii_key = KLowerAscii(ascii_key); + if (k_prefix) { + k_prefix = false; // single next key only + // Determine if this is a control chord (e.g., C-x) and normalize + bool ctrl = false; + int ascii_key = ch; + if (ch >= 1 && ch <= 26) { + ctrl = true; + ascii_key = 'a' + (ch - 1); + } + // For letters, normalize to lowercase ASCII + ascii_key = KLowerAscii(ascii_key); - CommandId id; - if (KLookupKCommand(ascii_key, ctrl, id)) { - out = {true, id, "", 0}; - } else { - // Show unknown k-command message with the typed character - char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; - std::string arg(1, c); - out = {true, CommandId::UnknownKCommand, arg, 0}; - } - return true; - } + CommandId id; + if (KLookupKCommand(ascii_key, ctrl, id)) { + out = {true, id, "", 0}; + } else { + // Show unknown k-command message with the typed character + char c = (ascii_key >= 0x20 && ascii_key <= 0x7e) ? static_cast(ascii_key) : '?'; + std::string arg(1, c); + out = {true, CommandId::UnknownKCommand, arg, 0}; + } + return true; + } // Printable ASCII if (ch >= 0x20 && ch <= 0x7E) { diff --git a/TerminalInputHandler.h b/TerminalInputHandler.h index c17963e..18e64c3 100644 --- a/TerminalInputHandler.h +++ b/TerminalInputHandler.h @@ -11,19 +11,19 @@ class TerminalInputHandler final : public InputHandler { public: - TerminalInputHandler(); + TerminalInputHandler(); - ~TerminalInputHandler() override; + ~TerminalInputHandler() override; - bool Poll(MappedInput &out) override; + bool Poll(MappedInput &out) override; private: - bool decode_(MappedInput &out); + bool decode_(MappedInput &out); - // ke-style prefix state - bool k_prefix_ = false; // true after C-k until next key or ESC - // Simple meta (ESC) state for ESC sequences like ESC b/f - bool esc_meta_ = false; + // ke-style prefix state + bool k_prefix_ = false; // true after C-k until next key or ESC + // Simple meta (ESC) state for ESC sequences like ESC b/f + bool esc_meta_ = false; }; #endif // KTE_TERMINAL_INPUT_HANDLER_H diff --git a/TerminalRenderer.cc b/TerminalRenderer.cc index 14383eb..a15dfff 100644 --- a/TerminalRenderer.cc +++ b/TerminalRenderer.cc @@ -26,14 +26,14 @@ TerminalRenderer::Draw(Editor &ed) int rows, cols; getmaxyx(stdscr, rows, cols); - // Clear screen - erase(); + // Clear screen + erase(); const Buffer *buf = ed.CurrentBuffer(); int content_rows = rows - 1; // last line is status - int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area - if (buf) { + int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area + if (buf) { const auto &lines = buf->Rows(); std::size_t rowoffs = buf->Rowoffs(); std::size_t coloffs = buf->Coloffs(); @@ -139,113 +139,117 @@ TerminalRenderer::Draw(Editor &ed) std::size_t rx = buf->Rx(); // render x computed by command layer int cur_y = static_cast(cy) - static_cast(buf->Rowoffs()); int cur_x = static_cast(rx) - static_cast(buf->Coloffs()); - if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { - // remember where to leave the terminal cursor after status is drawn - saved_cur_y = cur_y; - saved_cur_x = cur_x; - move(cur_y, cur_x); - } - } else { - mvaddstr(0, 0, "[no buffer]"); - } + if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { + // remember where to leave the terminal cursor after status is drawn + saved_cur_y = cur_y; + saved_cur_x = cur_x; + move(cur_y, cur_x); + } + } else { + mvaddstr(0, 0, "[no buffer]"); + } - // Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark - move(rows - 1, 0); - attron(A_REVERSE); + // Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark + move(rows - 1, 0); + attron(A_REVERSE); - // Fill the status line with spaces first - for (int i = 0; i < cols; ++i) addch(' '); + // Fill the status line with spaces first + for (int i = 0; i < cols; ++i) + addch(' '); - // Build left segment - std::string left; - { - const char *app = "kte"; - left.reserve(256); - left += app; - left += " "; - left += KTE_VERSION_STR; // already includes leading 'v' - const Buffer *b = buf; - std::string fname; - if (b) { - fname = b->Filename(); - } - if (!fname.empty()) { - try { - fname = std::filesystem::path(fname).filename().string(); - } catch (...) { - // keep original on any error - } - } else { - fname = "[no name]"; - } - left += " "; - left += fname; - if (b && b->Dirty()) - left += " *"; - } + // Build left segment + std::string left; + { + const char *app = "kte"; + left.reserve(256); + left += app; + left += " "; + left += KTE_VERSION_STR; // already includes leading 'v' + const Buffer *b = buf; + std::string fname; + if (b) { + fname = b->Filename(); + } + if (!fname.empty()) { + try { + fname = std::filesystem::path(fname).filename().string(); + } catch (...) { + // keep original on any error + } + } else { + fname = "[no name]"; + } + left += " "; + left += fname; + if (b && b->Dirty()) + left += " *"; + } - // Build right segment (cursor and mark) - std::string right; - { - int row1 = 0, col1 = 0; - int mrow1 = 0, mcol1 = 0; - bool have_mark = false; - if (buf) { - row1 = static_cast(buf->Cury()) + 1; - col1 = static_cast(buf->Curx()) + 1; - if (buf->MarkSet()) { - have_mark = true; - mrow1 = static_cast(buf->MarkCury()) + 1; - mcol1 = static_cast(buf->MarkCurx()) + 1; - } - } - char rbuf[128]; - if (have_mark) - std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1); - else - std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1); - right = rbuf; - } + // Build right segment (cursor and mark) + std::string right; + { + int row1 = 0, col1 = 0; + int mrow1 = 0, mcol1 = 0; + bool have_mark = false; + if (buf) { + row1 = static_cast(buf->Cury()) + 1; + col1 = static_cast(buf->Curx()) + 1; + if (buf->MarkSet()) { + have_mark = true; + mrow1 = static_cast(buf->MarkCury()) + 1; + mcol1 = static_cast(buf->MarkCurx()) + 1; + } + } + char rbuf[128]; + if (have_mark) + std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: %d,%d", row1, col1, mrow1, mcol1); + else + std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1); + right = rbuf; + } - // Compute placements with truncation rules: prioritize left and right; middle gets remaining - int rlen = static_cast(right.size()); - if (rlen > cols) { - // Hard clip right if too long - right = right.substr(static_cast(rlen - cols), static_cast(cols)); - rlen = cols; - } - int left_max = std::max(0, cols - rlen - 1); // leave at least 1 space between left and right areas - int llen = static_cast(left.size()); - if (llen > left_max) llen = left_max; + // Compute placements with truncation rules: prioritize left and right; middle gets remaining + int rlen = static_cast(right.size()); + if (rlen > cols) { + // Hard clip right if too long + right = right.substr(static_cast(rlen - cols), static_cast(cols)); + rlen = cols; + } + int left_max = std::max(0, cols - rlen - 1); // leave at least 1 space between left and right areas + int llen = static_cast(left.size()); + if (llen > left_max) + llen = left_max; - // Draw left - if (llen > 0) mvaddnstr(rows - 1, 0, left.c_str(), llen); + // Draw left + if (llen > 0) + mvaddnstr(rows - 1, 0, left.c_str(), llen); - // Draw right, flush to end - int rstart = std::max(0, cols - rlen); - if (rlen > 0) mvaddnstr(rows - 1, rstart, right.c_str(), rlen); + // Draw right, flush to end + int rstart = std::max(0, cols - rlen); + if (rlen > 0) + mvaddnstr(rows - 1, rstart, right.c_str(), rlen); - // Middle message - const std::string &msg = ed.Status(); - if (!msg.empty()) { - int mid_start = llen + 1; // one space after left - int mid_end = rstart - 1; // one space before right - if (mid_end >= mid_start) { - int avail = mid_end - mid_start + 1; - int mlen = static_cast(msg.size()); - int mdraw = std::min(avail, mlen); - int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area - mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw); - } - } + // Middle message + const std::string &msg = ed.Status(); + if (!msg.empty()) { + int mid_start = llen + 1; // one space after left + int mid_end = rstart - 1; // one space before right + if (mid_end >= mid_start) { + int avail = mid_end - mid_start + 1; + int mlen = static_cast(msg.size()); + int mdraw = std::min(avail, mlen); + int mstart = mid_start + std::max(0, (avail - mdraw) / 2); // center within middle area + mvaddnstr(rows - 1, mstart, msg.c_str(), mdraw); + } + } - attroff(A_REVERSE); + attroff(A_REVERSE); - // Restore terminal cursor to the content position so a visible caret - // remains in the editing area (not on the status line). - if (saved_cur_y >= 0 && saved_cur_x >= 0) { - move(saved_cur_y, saved_cur_x); - } + // Restore terminal cursor to the content position so a visible caret + // remains in the editing area (not on the status line). + if (saved_cur_y >= 0 && saved_cur_x >= 0) { + move(saved_cur_y, saved_cur_x); + } - refresh(); + refresh(); } diff --git a/TerminalRenderer.h b/TerminalRenderer.h index b354c02..bb34d60 100644 --- a/TerminalRenderer.h +++ b/TerminalRenderer.h @@ -9,10 +9,11 @@ class TerminalRenderer final : public Renderer { public: - TerminalRenderer(); - ~TerminalRenderer() override; + TerminalRenderer(); - void Draw(Editor &ed) override; + ~TerminalRenderer() override; + + void Draw(Editor &ed) override; }; #endif // KTE_TERMINAL_RENDERER_H