From 932bc3c504d91075e7d18d86870e6fcb911538d0 Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 29 Nov 2025 18:22:42 -0800 Subject: [PATCH] Basic new file work, some graphics glitches fixed. --- .idea/workspace.xml | 22 +- Buffer.cpp | 40 +-- CMakeLists.txt | 2 + Command.cpp | 703 ++++++++++++++++++++++----------------- Command.h | 1 + GUIInputHandler.cpp | 21 +- GUIRenderer.cpp | 53 ++- KKeymap.cpp | 24 ++ KKeymap.h | 26 ++ TerminalInputHandler.cpp | 25 +- TerminalRenderer.cpp | 17 +- 11 files changed, 579 insertions(+), 355 deletions(-) create mode 100644 KKeymap.cpp create mode 100644 KKeymap.h diff --git a/.idea/workspace.xml b/.idea/workspace.xml index ca8e2c9..b34deef 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -23,6 +23,7 @@ + @@ -32,11 +33,17 @@ + + + + + + - - + + diff --git a/Buffer.cpp b/Buffer.cpp index f353e09..b2fec81 100644 --- a/Buffer.cpp +++ b/Buffer.cpp @@ -18,29 +18,29 @@ Buffer::Buffer(const std::string &path) bool Buffer::OpenFromFile(const std::string &path, std::string &err) { - // If the file doesn't exist, initialize an empty, non-file-backed buffer - // with the provided filename. Do not touch the filesystem until Save/SaveAs. - if (!std::filesystem::exists(path)) { - rows_.clear(); - nrows_ = 0; - filename_ = path; - is_file_backed_ = false; - dirty_ = false; + // If the file doesn't exist, initialize an empty, non-file-backed buffer + // with the provided filename. Do not touch the filesystem until Save/SaveAs. + if (!std::filesystem::exists(path)) { + rows_.clear(); + nrows_ = 0; + filename_ = path; + is_file_backed_ = false; + dirty_ = false; - // Reset cursor/viewport state - curx_ = cury_ = rx_ = 0; - rowoffs_ = coloffs_ = 0; - mark_set_ = false; - mark_curx_ = mark_cury_ = 0; + // Reset cursor/viewport state + curx_ = cury_ = rx_ = 0; + rowoffs_ = coloffs_ = 0; + mark_set_ = false; + mark_curx_ = mark_cury_ = 0; - return true; - } + return true; + } - std::ifstream in(path, std::ios::in | std::ios::binary); - if (!in) { - err = "Failed to open file: " + path; - return false; - } + std::ifstream in(path, std::ios::in | std::ios::binary); + if (!in) { + err = "Failed to open file: " + path; + return false; + } rows_.clear(); std::string line; diff --git a/CMakeLists.txt b/CMakeLists.txt index 0edb1ec..3dde060 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -50,6 +50,7 @@ set(COMMON_SOURCES Buffer.cpp Editor.cpp Command.cpp + KKeymap.cpp TerminalInputHandler.cpp TerminalRenderer.cpp TerminalFrontend.cpp @@ -62,6 +63,7 @@ set(COMMON_HEADERS Editor.h AppendBuffer.h Command.h + KKeymap.h InputHandler.h TerminalInputHandler.h Renderer.h diff --git a/Command.cpp b/Command.cpp index cad5802..6f8042b 100644 --- a/Command.cpp +++ b/Command.cpp @@ -1,71 +1,89 @@ -#include "Command.h" - #include +#include "Command.h" #include "Editor.h" #include "Buffer.h" + // Keep buffer viewport offsets so that the cursor stays within the visible // window based on the editor's current dimensions. The bottom row is reserved // for the status line. -static void ensure_cursor_visible(Editor &ed, Buffer &buf) +static void +ensure_cursor_visible(const Editor &ed, Buffer &buf) { - std::size_t rows = ed.Rows(); - std::size_t cols = ed.Cols(); - if (rows == 0 || cols == 0) return; + const std::size_t rows = ed.Rows(); + const std::size_t cols = ed.Cols(); + if (rows == 0 || cols == 0) + return; - std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status - std::size_t cury = buf.Cury(); - std::size_t curx = buf.Curx(); - std::size_t rowoffs = buf.Rowoffs(); - std::size_t coloffs = buf.Coloffs(); + std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status + std::size_t cury = buf.Cury(); + std::size_t curx = buf.Curx(); + std::size_t rowoffs = buf.Rowoffs(); + std::size_t coloffs = buf.Coloffs(); - // Vertical scrolling - if (cury < rowoffs) { - rowoffs = cury; - } else if (content_rows > 0 && cury >= rowoffs + content_rows) { - rowoffs = cury - content_rows + 1; - } + // Vertical scrolling + if (cury < rowoffs) { + rowoffs = cury; + } else if (content_rows > 0 && cury >= rowoffs + content_rows) { + rowoffs = cury - content_rows + 1; + } - // Clamp vertical offset to available content - const auto total_rows = buf.Rows().size(); - if (content_rows < total_rows) { - std::size_t max_rowoffs = total_rows - content_rows; - if (rowoffs > max_rowoffs) rowoffs = max_rowoffs; - } else { - rowoffs = 0; - } + // Clamp vertical offset to available content + const auto total_rows = buf.Rows().size(); + if (content_rows < total_rows) { + std::size_t max_rowoffs = total_rows - content_rows; + if (rowoffs > max_rowoffs) + rowoffs = max_rowoffs; + } else { + rowoffs = 0; + } - // Horizontal scrolling - if (curx < coloffs) { - coloffs = curx; - } else if (curx >= coloffs + cols) { - coloffs = curx - cols + 1; - } + // Horizontal scrolling + if (curx < coloffs) { + coloffs = curx; + } else if (curx >= coloffs + cols) { + coloffs = curx - cols + 1; + } - buf.SetOffsets(rowoffs, coloffs); + buf.SetOffsets(rowoffs, coloffs); } -static void ensure_at_least_one_line(Buffer &buf) + +static void +ensure_at_least_one_line(Buffer &buf) { - if (buf.Rows().empty()) { - buf.Rows().push_back(""); - buf.SetDirty(true); - } + if (buf.Rows().empty()) { + buf.Rows().push_back(""); + buf.SetDirty(true); + } } + // (helper removed) // --- File/Session commands --- -static bool cmd_save(CommandContext &ctx) +static bool +cmd_save(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to save"); - return false; - } + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to save"); + return false; + } std::string err; + // Allow saving directly to a filename if buffer was opened with a + // non-existent path (not yet file-backed but has a filename). if (!buf->IsFileBacked()) { + if (!buf->Filename().empty()) { + if (!buf->SaveAs(buf->Filename(), err)) { + ctx.editor.SetStatus(err); + return false; + } + buf->SetDirty(false); + ctx.editor.SetStatus("Saved " + buf->Filename()); + return true; + } ctx.editor.SetStatus("Buffer is not file-backed; use save-as"); return false; } @@ -79,41 +97,44 @@ static bool cmd_save(CommandContext &ctx) } -static bool cmd_save_as(CommandContext &ctx) +static bool +cmd_save_as(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to save"); - return false; - } - if (ctx.arg.empty()) { - ctx.editor.SetStatus("save-as requires a filename"); - return false; - } - std::string err; - if (!buf->SaveAs(ctx.arg, err)) { - ctx.editor.SetStatus(err); - return false; - } - ctx.editor.SetStatus("Saved as " + ctx.arg); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to save"); + return false; + } + if (ctx.arg.empty()) { + ctx.editor.SetStatus("save-as requires a filename"); + return false; + } + std::string err; + if (!buf->SaveAs(ctx.arg, err)) { + ctx.editor.SetStatus(err); + return false; + } + ctx.editor.SetStatus("Saved as " + ctx.arg); + return true; } -static bool cmd_quit(CommandContext &ctx) +static bool +cmd_quit(CommandContext &ctx) { - // Placeholder: actual app loop should react to this status or a future flag - ctx.editor.SetStatus("Quit requested"); - return true; + // Placeholder: actual app loop should react to this status or a future flag + ctx.editor.SetStatus("Quit requested"); + return true; } -static bool cmd_save_and_quit(CommandContext &ctx) +static bool +cmd_save_and_quit(CommandContext &ctx) { - // Try save current buffer (if any), then mark quit requested. - Buffer *buf = ctx.editor.CurrentBuffer(); - if (buf && buf->Dirty()) { - std::string err; + // Try save current buffer (if any), then mark quit requested. + Buffer *buf = ctx.editor.CurrentBuffer(); + if (buf && buf->Dirty()) { + std::string err; if (buf->IsFileBacked()) { if (buf->Save(err)) { buf->SetDirty(false); @@ -121,6 +142,13 @@ static bool cmd_save_and_quit(CommandContext &ctx) ctx.editor.SetStatus(err); return false; } + } else if (!buf->Filename().empty()) { + if (buf->SaveAs(buf->Filename(), err)) { + buf->SetDirty(false); + } else { + ctx.editor.SetStatus(err); + return false; + } } else { ctx.editor.SetStatus("Buffer not file-backed; use save-as before quitting"); return false; @@ -130,298 +158,345 @@ static bool cmd_save_and_quit(CommandContext &ctx) return true; } -static bool cmd_refresh(CommandContext &ctx) + +static bool +cmd_refresh(CommandContext &ctx) { // Placeholder: renderer will handle this in Milestone 3 ctx.editor.SetStatus("Refresh requested"); return true; } -static bool cmd_find_start(CommandContext &ctx) +static bool +cmd_kprefix(CommandContext &ctx) { - // Placeholder for incremental search start - ctx.editor.SetStatus("Find (incremental) start"); + // Show k-command mode hint in status + ctx.editor.SetStatus("C-k _"); return true; } +static bool +cmd_find_start(CommandContext &ctx) +{ + // Placeholder for incremental search start + ctx.editor.SetStatus("Find (incremental) start"); + return true; +} + + // --- Editing --- -static bool cmd_insert_text(CommandContext &ctx) +static bool +cmd_insert_text(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to edit"); - return false; - } - // Disallow newlines in InsertText; they should come via Newline - if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) { - ctx.editor.SetStatus("InsertText arg must not contain newlines"); - return false; - } - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - if (y >= rows.size()) { - rows.resize(y + 1); - } - int repeat = ctx.count > 0 ? ctx.count : 1; - for (int i = 0; i < repeat; ++i) { - rows[y].insert(x, ctx.arg); - x += ctx.arg.size(); - } - buf->SetCursor(x, y); - buf->SetDirty(true); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to edit"); + return false; + } + // Disallow newlines in InsertText; they should come via Newline + if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) { + ctx.editor.SetStatus("InsertText arg must not contain newlines"); + return false; + } + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + if (y >= rows.size()) { + rows.resize(y + 1); + } + int repeat = ctx.count > 0 ? ctx.count : 1; + for (int i = 0; i < repeat; ++i) { + rows[y].insert(x, ctx.arg); + x += ctx.arg.size(); + } + buf->SetCursor(x, y); + buf->SetDirty(true); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_newline(CommandContext &ctx) + +static bool +cmd_newline(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to edit"); - return false; - } - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - for (int i = 0; i < repeat; ++i) { - if (y >= rows.size()) rows.resize(y + 1); - std::string &line = rows[y]; - std::string tail; - if (x < line.size()) { - tail = line.substr(x); - line.erase(x); - } - rows.insert(rows.begin() + static_cast(y + 1), tail); - y += 1; - x = 0; - } - buf->SetCursor(x, y); - buf->SetDirty(true); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to edit"); + return false; + } + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + for (int i = 0; i < repeat; ++i) { + if (y >= rows.size()) + rows.resize(y + 1); + std::string &line = rows[y]; + std::string tail; + if (x < line.size()) { + tail = line.substr(x); + line.erase(x); + } + rows.insert(rows.begin() + static_cast(y + 1), tail); + y += 1; + x = 0; + } + buf->SetCursor(x, y); + buf->SetDirty(true); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_backspace(CommandContext &ctx) + +static bool +cmd_backspace(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to edit"); - return false; - } - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - for (int i = 0; i < repeat; ++i) { - if (x > 0) { - rows[y].erase(x - 1, 1); - --x; - } else if (y > 0) { - // join with previous line - std::size_t prev_len = rows[y - 1].size(); - rows[y - 1] += rows[y]; - rows.erase(rows.begin() + static_cast(y)); - y = y - 1; - x = prev_len; - } else { - // at very start; nothing to do - break; - } - } - buf->SetCursor(x, y); - buf->SetDirty(true); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to edit"); + return false; + } + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + for (int i = 0; i < repeat; ++i) { + if (x > 0) { + rows[y].erase(x - 1, 1); + --x; + } else if (y > 0) { + // join with previous line + std::size_t prev_len = rows[y - 1].size(); + rows[y - 1] += rows[y]; + rows.erase(rows.begin() + static_cast(y)); + y = y - 1; + x = prev_len; + } else { + // at very start; nothing to do + break; + } + } + buf->SetCursor(x, y); + buf->SetDirty(true); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_delete_char(CommandContext &ctx) + +static bool +cmd_delete_char(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) { - ctx.editor.SetStatus("No buffer to edit"); - return false; - } - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - for (int i = 0; i < repeat; ++i) { - if (y >= rows.size()) break; - if (x < rows[y].size()) { - rows[y].erase(x, 1); - } else if (y + 1 < rows.size()) { - // join next line - rows[y] += rows[y + 1]; - rows.erase(rows.begin() + static_cast(y + 1)); - } else { - break; - } - } - buf->SetDirty(true); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) { + ctx.editor.SetStatus("No buffer to edit"); + return false; + } + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + for (int i = 0; i < repeat; ++i) { + if (y >= rows.size()) + break; + if (x < rows[y].size()) { + rows[y].erase(x, 1); + } else if (y + 1 < rows.size()) { + // join next line + rows[y] += rows[y + 1]; + rows.erase(rows.begin() + static_cast(y + 1)); + } else { + break; + } + } + buf->SetDirty(true); + ensure_cursor_visible(ctx.editor, *buf); + return true; } + // --- Navigation --- // (helper removed) -static bool cmd_move_left(CommandContext &ctx) +static bool +cmd_move_left(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - while (repeat-- > 0) { - if (x > 0) { - --x; - } else if (y > 0) { - --y; - x = rows[y].size(); - } - } - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + while (repeat-- > 0) { + if (x > 0) { + --x; + } else if (y > 0) { + --y; + x = rows[y].size(); + } + } + buf->SetCursor(x, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_move_right(CommandContext &ctx) + +static bool +cmd_move_right(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - while (repeat-- > 0) { - if (y < rows.size() && x < rows[y].size()) { - ++x; - } else if (y + 1 < rows.size()) { - ++y; - x = 0; - } - } - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + while (repeat-- > 0) { + if (y < rows.size() && x < rows[y].size()) { + ++x; + } else if (y + 1 < rows.size()) { + ++y; + x = 0; + } + } + buf->SetCursor(x, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_move_up(CommandContext &ctx) + +static bool +cmd_move_up(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - if (repeat > static_cast(y)) repeat = static_cast(y); - y -= static_cast(repeat); - if (x > rows[y].size()) x = rows[y].size(); - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + if (repeat > static_cast(y)) + repeat = static_cast(y); + y -= static_cast(repeat); + if (x > rows[y].size()) + x = rows[y].size(); + buf->SetCursor(x, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_move_down(CommandContext &ctx) + +static bool +cmd_move_down(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = buf->Curx(); - int repeat = ctx.count > 0 ? ctx.count : 1; - std::size_t max_down = rows.size() - 1 - y; - if (repeat > static_cast(max_down)) repeat = static_cast(max_down); - y += static_cast(repeat); - if (x > rows[y].size()) x = rows[y].size(); - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = buf->Curx(); + int repeat = ctx.count > 0 ? ctx.count : 1; + std::size_t max_down = rows.size() - 1 - y; + if (repeat > static_cast(max_down)) + repeat = static_cast(max_down); + y += static_cast(repeat); + if (x > rows[y].size()) + x = rows[y].size(); + buf->SetCursor(x, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_move_home(CommandContext &ctx) + +static bool +cmd_move_home(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - std::size_t y = buf->Cury(); - buf->SetCursor(0, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + std::size_t y = buf->Cury(); + buf->SetCursor(0, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } -static bool cmd_move_end(CommandContext &ctx) + +static bool +cmd_move_end(CommandContext &ctx) { - Buffer *buf = ctx.editor.CurrentBuffer(); - if (!buf) return false; - ensure_at_least_one_line(*buf); - auto &rows = buf->Rows(); - std::size_t y = buf->Cury(); - std::size_t x = (y < rows.size()) ? rows[y].size() : 0; - buf->SetCursor(x, y); - ensure_cursor_visible(ctx.editor, *buf); - return true; + Buffer *buf = ctx.editor.CurrentBuffer(); + if (!buf) + return false; + ensure_at_least_one_line(*buf); + auto &rows = buf->Rows(); + std::size_t y = buf->Cury(); + std::size_t x = (y < rows.size()) ? rows[y].size() : 0; + buf->SetCursor(x, y); + ensure_cursor_visible(ctx.editor, *buf); + return true; } std::vector & CommandRegistry::storage_() { - static std::vector cmds; - return cmds; + static std::vector cmds; + return cmds; } void CommandRegistry::Register(const Command &cmd) { - auto &v = storage_(); - // Replace existing with same id or name - auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { - return c.id == cmd.id || c.name == cmd.name; - }); - if (it != v.end()) { - *it = cmd; - } else { - v.push_back(cmd); - } + auto &v = storage_(); + // Replace existing with same id or name + auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { + return c.id == cmd.id || c.name == cmd.name; + }); + if (it != v.end()) { + *it = cmd; + } else { + v.push_back(cmd); + } } const Command * CommandRegistry::FindById(CommandId id) { - auto &v = storage_(); - auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.id == id; }); - return it == v.end() ? nullptr : &*it; + auto &v = storage_(); + auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { + return c.id == id; + }); + return it == v.end() ? nullptr : &*it; } const Command * CommandRegistry::FindByName(const std::string &name) { - auto &v = storage_(); - auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.name == name; }); - return it == v.end() ? nullptr : &*it; + auto &v = storage_(); + auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { + return c.name == name; + }); + return it == v.end() ? nullptr : &*it; } const std::vector & CommandRegistry::All() { - return storage_(); + return storage_(); } @@ -433,34 +508,42 @@ InstallDefaultCommands() CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit}); 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({CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix}); CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start}); - // Editing - CommandRegistry::Register({CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text}); - CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline}); - CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace}); - CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char}); - // Navigation - CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left}); - CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right}); - CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up}); - CommandRegistry::Register({CommandId::MoveDown, "down", "Move cursor down", cmd_move_down}); - CommandRegistry::Register({CommandId::MoveHome, "home", "Move to beginning of line", cmd_move_home}); - CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end}); + // Editing + CommandRegistry::Register({ + CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text + }); + CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline}); + CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace}); + CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char}); + // Navigation + CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left}); + CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right}); + CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up}); + CommandRegistry::Register({CommandId::MoveDown, "down", "Move cursor down", cmd_move_down}); + CommandRegistry::Register({CommandId::MoveHome, "home", "Move to beginning of line", cmd_move_home}); + CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end}); } -bool Execute(Editor &ed, CommandId id, const std::string &arg, int count) +bool +Execute(Editor &ed, CommandId id, const std::string &arg, int count) { - const Command *cmd = CommandRegistry::FindById(id); - if (!cmd) return false; - CommandContext ctx{ed, arg, count}; - return cmd->handler ? cmd->handler(ctx) : false; + const Command *cmd = CommandRegistry::FindById(id); + if (!cmd) + return false; + CommandContext ctx{ed, arg, count}; + return cmd->handler ? cmd->handler(ctx) : false; } -bool Execute(Editor &ed, const std::string &name, const std::string &arg, int count) + +bool +Execute(Editor &ed, const std::string &name, const std::string &arg, int count) { - const Command *cmd = CommandRegistry::FindByName(name); - if (!cmd) return false; - CommandContext ctx{ed, arg, count}; - return cmd->handler ? cmd->handler(ctx) : false; + const Command *cmd = CommandRegistry::FindByName(name); + if (!cmd) + return false; + CommandContext ctx{ed, arg, count}; + return cmd->handler ? cmd->handler(ctx) : false; } diff --git a/Command.h b/Command.h index 0f3ac8a..3ee3a59 100644 --- a/Command.h +++ b/Command.h @@ -20,6 +20,7 @@ enum class CommandId { Quit, SaveAndQuit, Refresh, // force redraw + KPrefix, // show "C-k _" prompt in status when entering k-command FindStart, // begin incremental search (placeholder) // Editing InsertText, // arg: text to insert at cursor (UTF-8, no newlines) diff --git a/GUIInputHandler.cpp b/GUIInputHandler.cpp index e6f87d1..2190153 100644 --- a/GUIInputHandler.cpp +++ b/GUIInputHandler.cpp @@ -1,6 +1,7 @@ #include "GUIInputHandler.h" #include +#include "KKeymap.h" static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput &out) { @@ -26,7 +27,7 @@ static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput switch (key) { case SDLK_k: case SDLK_KP_EQUALS: // treat Ctrl-K k_prefix = true; - out = {true, CommandId::Refresh, "", 0}; + out = {true, CommandId::KPrefix, "", 0}; return true; case SDLK_g: k_prefix = false; @@ -42,11 +43,19 @@ static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput if (k_prefix) { k_prefix = false; - switch (key) { - case SDLK_s: out = {true, CommandId::Save, "", 0}; return true; - case SDLK_x: out = {true, CommandId::SaveAndQuit, "", 0}; return true; - case SDLK_q: out = {true, CommandId::Quit, "", 0}; return true; - default: break; + // 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; + } } out.hasCommand = false; return true; diff --git a/GUIRenderer.cpp b/GUIRenderer.cpp index faae808..a40c061 100644 --- a/GUIRenderer.cpp +++ b/GUIRenderer.cpp @@ -4,6 +4,7 @@ #include "Buffer.h" #include +#include void GUIRenderer::Draw(const Editor &ed) { @@ -35,20 +36,58 @@ void GUIRenderer::Draw(const Editor &ed) // Reserve space for status bar at bottom ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar); std::size_t rowoffs = buf->Rowoffs(); + std::size_t cy = buf->Cury(); + std::size_t cx = buf->Curx(); + const float line_h = ImGui::GetTextLineHeight(); + const float space_w = ImGui::CalcTextSize(" ").x; for (std::size_t i = rowoffs; i < lines.size(); ++i) { - ImGui::TextUnformatted(lines[i].c_str()); + // Capture the screen position before drawing the line + ImVec2 line_pos = ImGui::GetCursorScreenPos(); + const std::string &line = lines[i]; + ImGui::TextUnformatted(line.c_str()); + + // Draw a visible cursor indicator on the current line + if (i == cy) { + // Compute X offset by measuring text width up to cursor column + std::size_t px_count = std::min(cx, line.size()); + ImVec2 pre_sz = ImGui::CalcTextSize(line.c_str(), line.c_str() + static_cast(px_count)); + ImVec2 p0 = ImVec2(line_pos.x + pre_sz.x, line_pos.y); + ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h); + ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight + ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); + } } ImGui::EndChild(); - // Status bar + // Status bar spanning full width ImGui::Separator(); const char *fname = (buf->IsFileBacked()) ? buf->Filename().c_str() : "(new)"; bool dirty = buf->Dirty(); - ImGui::Text("%s%s %zux%zu %s", - fname, - dirty ? "*" : "", - ed.Rows(), ed.Cols(), - ed.Status().c_str()); + char status[1024]; + snprintf(status, sizeof(status), " %s%s %zux%zu %s ", + fname, + dirty ? "*" : "", + ed.Rows(), ed.Cols(), + ed.Status().c_str()); + + // 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); + // Place status text within the bar + ImVec2 text_sz = ImGui::CalcTextSize(status); + ImGui::SetCursorScreenPos(ImVec2(p0.x + 6.f, p0.y + (bar_h - text_sz.y) * 0.5f)); + ImGui::TextUnformatted(status); + // Advance cursor to after the bar to keep layout consistent + ImGui::Dummy(ImVec2(x1 - x0, bar_h)); } ImGui::End(); diff --git a/KKeymap.cpp b/KKeymap.cpp new file mode 100644 index 0000000..4efd11d --- /dev/null +++ b/KKeymap.cpp @@ -0,0 +1,24 @@ +#include "KKeymap.h" + +auto +KLookupKCommand(const int ascii_key, bool ctrl, CommandId &out) -> bool +{ + // Normalize to lowercase letter if applicable + int k = KLowerAscii(ascii_key); + + if (ctrl) { + switch (k) { + case 'x': out = CommandId::SaveAndQuit; return true; // C-k C-x + case 'q': out = CommandId::Quit; return true; // C-k C-q (quit immediately) + default: break; + } + } else { + switch (k) { + case 's': out = CommandId::Save; return true; // C-k s + case 'x': out = CommandId::SaveAndQuit; return true; // C-k x + case 'q': out = CommandId::Quit; return true; // C-k q + default: break; + } + } + return false; +} diff --git a/KKeymap.h b/KKeymap.h new file mode 100644 index 0000000..685c19f --- /dev/null +++ b/KKeymap.h @@ -0,0 +1,26 @@ +/* + * KKeymap.h - mapping for k-command (C-k prefix) keys to CommandId + */ +#ifndef KTE_KKEYMAP_H +#define KTE_KKEYMAP_H + +#include "Command.h" +#include + +// Lookup the command to execute after a C-k prefix. +// Parameters: +// - ascii_key: ASCII code of the key, preferably lowercased if it's a letter. +// - ctrl: whether Control modifier was held for this key (e.g., C-k C-x). +// Returns true and sets out if a mapping exists; false otherwise. +bool KLookupKCommand(int ascii_key, bool ctrl, CommandId &out); + +// Utility: normalize an int keycode to lowercased ASCII if it's in printable range. +inline int +KLowerAscii(const int key) +{ + if (key >= 'A' && key <= 'Z') + return key + ('a' - 'A'); + return key; +} + +#endif // KTE_KKEYMAP_H diff --git a/TerminalInputHandler.cpp b/TerminalInputHandler.cpp index e6345d2..0e64a56 100644 --- a/TerminalInputHandler.cpp +++ b/TerminalInputHandler.cpp @@ -1,6 +1,7 @@ #include "TerminalInputHandler.h" #include +#include "KKeymap.h" namespace { constexpr int CTRL(char c) { return c & 0x1F; } @@ -35,7 +36,7 @@ static bool map_key_to_command(int ch, bool &k_prefix, MappedInput &out) // Control keys if (ch == CTRL('K')) { // C-k prefix k_prefix = true; - out = {true, CommandId::Refresh, "", 0}; + out = {true, CommandId::KPrefix, "", 0}; return true; } if (ch == CTRL('G')) { // cancel @@ -53,14 +54,22 @@ static bool map_key_to_command(int ch, bool &k_prefix, MappedInput &out) if (k_prefix) { k_prefix = false; // single next key only - switch (ch) { - case 's': case 'S': out = {true, CommandId::Save, "", 0}; return true; - case 'x': case 'X': out = {true, CommandId::SaveAndQuit, "", 0}; return true; - case 'q': case 'Q': out = {true, CommandId::Quit, "", 0}; return true; - default: break; + // 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 { + out.hasCommand = false; // unknown chord after C-k } - if (ch == CTRL('Q')) { out = {true, CommandId::Quit, "", 0}; return true; } - out.hasCommand = false; // unknown chord return true; } diff --git a/TerminalRenderer.cpp b/TerminalRenderer.cpp index c9524d1..f51aee9 100644 --- a/TerminalRenderer.cpp +++ b/TerminalRenderer.cpp @@ -43,12 +43,27 @@ void TerminalRenderer::Draw(const Editor &ed) clrtoeol(); } - // Place cursor (best-effort; tabs etc. not handled yet) + // Draw a visible cursor cell by inverting the character at the cursor + // position (or a space at EOL). This makes the cursor obvious even when + // the terminal's native cursor is hidden or not prominent. std::size_t cy = buf->Cury(); std::size_t cx = buf->Curx(); int cur_y = static_cast(cy - buf->Rowoffs()); int cur_x = static_cast(cx - buf->Coloffs()); if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { + // Determine the character under the cursor (if any) + char ch = ' '; + if (cy < lines.size()) { + const std::string &cline = lines[cy]; + if (cx < cline.size()) { + ch = cline[static_cast(cx)]; + } + } + move(cur_y, cur_x); + attron(A_REVERSE); + addch(static_cast(ch)); + attroff(A_REVERSE); + // Also place the terminal cursor at the same spot move(cur_y, cur_x); } } else {