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 @@
+
+
+
+
+
+
-
-
+
+
@@ -50,6 +57,9 @@
+
+
+
@@ -106,6 +116,11 @@
+
+
+
+
+
@@ -113,6 +128,7 @@
+
@@ -123,7 +139,7 @@
1764457173148
-
+
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 {