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 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -140,7 +156,7 @@
1764457173148
-
+
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