Basic new file work, some graphics glitches fixed.

This commit is contained in:
2025-11-29 18:22:42 -08:00
parent 548d760df5
commit 932bc3c504
11 changed files with 579 additions and 355 deletions

22
.idea/workspace.xml generated
View File

@@ -23,6 +23,7 @@
<generated> <generated>
<config projectName="kte" targetName="kte" /> <config projectName="kte" targetName="kte" />
<config projectName="kte" targetName="imgui" /> <config projectName="kte" targetName="imgui" />
<config projectName="kte" targetName="kge" />
</generated> </generated>
</component> </component>
<component name="CMakeSettings"> <component name="CMakeSettings">
@@ -32,11 +33,17 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment=""> <list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/KKeymap.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/KKeymap.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cpp" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cpp" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cpp" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cpp" afterDir="false" /> <change beforePath="$PROJECT_DIR$/GUIRenderer.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cpp" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" /> <change beforePath="$PROJECT_DIR$/TerminalInputHandler.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalInputHandler.cpp" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/main.cpp" afterDir="false" /> <change beforePath="$PROJECT_DIR$/TerminalRenderer.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cpp" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -50,6 +57,9 @@
<component name="Git.Settings"> <component name="Git.Settings">
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" /> <option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
</component> </component>
<component name="HighlightingSettingsPerFile">
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
</component>
<component name="ProjectApplicationVersion"> <component name="ProjectApplicationVersion">
<option name="ide" value="CLion" /> <option name="ide" value="CLion" />
<option name="majorVersion" value="2025" /> <option name="majorVersion" value="2025" />
@@ -106,6 +116,11 @@
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method> </method>
</configuration> </configuration>
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte"> <configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
<method v="2"> <method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" /> <option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
@@ -113,6 +128,7 @@
</configuration> </configuration>
<list> <list>
<item itemvalue="CMake Application.imgui" /> <item itemvalue="CMake Application.imgui" />
<item itemvalue="CMake Application.kge" />
<item itemvalue="CMake Application.kte" /> <item itemvalue="CMake Application.kte" />
</list> </list>
</component> </component>
@@ -123,7 +139,7 @@
<option name="number" value="Default" /> <option name="number" value="Default" />
<option name="presentableId" value="Default" /> <option name="presentableId" value="Default" />
<updated>1764457173148</updated> <updated>1764457173148</updated>
<workItem from="1764457174208" duration="9674000" /> <workItem from="1764457174208" duration="10626000" />
</task> </task>
<servers /> <servers />
</component> </component>

View File

@@ -18,29 +18,29 @@ Buffer::Buffer(const std::string &path)
bool bool
Buffer::OpenFromFile(const std::string &path, std::string &err) Buffer::OpenFromFile(const std::string &path, std::string &err)
{ {
// If the file doesn't exist, initialize an empty, non-file-backed buffer // 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. // with the provided filename. Do not touch the filesystem until Save/SaveAs.
if (!std::filesystem::exists(path)) { if (!std::filesystem::exists(path)) {
rows_.clear(); rows_.clear();
nrows_ = 0; nrows_ = 0;
filename_ = path; filename_ = path;
is_file_backed_ = false; is_file_backed_ = false;
dirty_ = false; dirty_ = false;
// Reset cursor/viewport state // Reset cursor/viewport state
curx_ = cury_ = rx_ = 0; curx_ = cury_ = rx_ = 0;
rowoffs_ = coloffs_ = 0; rowoffs_ = coloffs_ = 0;
mark_set_ = false; mark_set_ = false;
mark_curx_ = mark_cury_ = 0; mark_curx_ = mark_cury_ = 0;
return true; return true;
} }
std::ifstream in(path, std::ios::in | std::ios::binary); std::ifstream in(path, std::ios::in | std::ios::binary);
if (!in) { if (!in) {
err = "Failed to open file: " + path; err = "Failed to open file: " + path;
return false; return false;
} }
rows_.clear(); rows_.clear();
std::string line; std::string line;

View File

@@ -50,6 +50,7 @@ set(COMMON_SOURCES
Buffer.cpp Buffer.cpp
Editor.cpp Editor.cpp
Command.cpp Command.cpp
KKeymap.cpp
TerminalInputHandler.cpp TerminalInputHandler.cpp
TerminalRenderer.cpp TerminalRenderer.cpp
TerminalFrontend.cpp TerminalFrontend.cpp
@@ -62,6 +63,7 @@ set(COMMON_HEADERS
Editor.h Editor.h
AppendBuffer.h AppendBuffer.h
Command.h Command.h
KKeymap.h
InputHandler.h InputHandler.h
TerminalInputHandler.h TerminalInputHandler.h
Renderer.h Renderer.h

View File

@@ -1,71 +1,89 @@
#include "Command.h"
#include <algorithm> #include <algorithm>
#include "Command.h"
#include "Editor.h" #include "Editor.h"
#include "Buffer.h" #include "Buffer.h"
// Keep buffer viewport offsets so that the cursor stays within the visible // 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 // window based on the editor's current dimensions. The bottom row is reserved
// for the status line. // 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(); const std::size_t rows = ed.Rows();
std::size_t cols = ed.Cols(); const std::size_t cols = ed.Cols();
if (rows == 0 || cols == 0) return; if (rows == 0 || cols == 0)
return;
std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status
std::size_t cury = buf.Cury(); std::size_t cury = buf.Cury();
std::size_t curx = buf.Curx(); std::size_t curx = buf.Curx();
std::size_t rowoffs = buf.Rowoffs(); std::size_t rowoffs = buf.Rowoffs();
std::size_t coloffs = buf.Coloffs(); std::size_t coloffs = buf.Coloffs();
// Vertical scrolling // Vertical scrolling
if (cury < rowoffs) { if (cury < rowoffs) {
rowoffs = cury; rowoffs = cury;
} else if (content_rows > 0 && cury >= rowoffs + content_rows) { } else if (content_rows > 0 && cury >= rowoffs + content_rows) {
rowoffs = cury - content_rows + 1; rowoffs = cury - content_rows + 1;
} }
// Clamp vertical offset to available content // Clamp vertical offset to available content
const auto total_rows = buf.Rows().size(); const auto total_rows = buf.Rows().size();
if (content_rows < total_rows) { if (content_rows < total_rows) {
std::size_t max_rowoffs = total_rows - content_rows; std::size_t max_rowoffs = total_rows - content_rows;
if (rowoffs > max_rowoffs) rowoffs = max_rowoffs; if (rowoffs > max_rowoffs)
} else { rowoffs = max_rowoffs;
rowoffs = 0; } else {
} rowoffs = 0;
}
// Horizontal scrolling // Horizontal scrolling
if (curx < coloffs) { if (curx < coloffs) {
coloffs = curx; coloffs = curx;
} else if (curx >= coloffs + cols) { } else if (curx >= coloffs + cols) {
coloffs = curx - cols + 1; 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()) { if (buf.Rows().empty()) {
buf.Rows().push_back(""); buf.Rows().push_back("");
buf.SetDirty(true); buf.SetDirty(true);
} }
} }
// (helper removed) // (helper removed)
// --- File/Session commands --- // --- File/Session commands ---
static bool cmd_save(CommandContext &ctx) static bool
cmd_save(CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to save"); ctx.editor.SetStatus("No buffer to save");
return false; return false;
} }
std::string err; 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->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"); ctx.editor.SetStatus("Buffer is not file-backed; use save-as");
return false; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to save"); ctx.editor.SetStatus("No buffer to save");
return false; return false;
} }
if (ctx.arg.empty()) { if (ctx.arg.empty()) {
ctx.editor.SetStatus("save-as requires a filename"); ctx.editor.SetStatus("save-as requires a filename");
return false; return false;
} }
std::string err; std::string err;
if (!buf->SaveAs(ctx.arg, err)) { if (!buf->SaveAs(ctx.arg, err)) {
ctx.editor.SetStatus(err); ctx.editor.SetStatus(err);
return false; return false;
} }
ctx.editor.SetStatus("Saved as " + ctx.arg); ctx.editor.SetStatus("Saved as " + ctx.arg);
return true; 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 // Placeholder: actual app loop should react to this status or a future flag
ctx.editor.SetStatus("Quit requested"); ctx.editor.SetStatus("Quit requested");
return true; 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. // Try save current buffer (if any), then mark quit requested.
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (buf && buf->Dirty()) { if (buf && buf->Dirty()) {
std::string err; std::string err;
if (buf->IsFileBacked()) { if (buf->IsFileBacked()) {
if (buf->Save(err)) { if (buf->Save(err)) {
buf->SetDirty(false); buf->SetDirty(false);
@@ -121,6 +142,13 @@ static bool cmd_save_and_quit(CommandContext &ctx)
ctx.editor.SetStatus(err); ctx.editor.SetStatus(err);
return false; return false;
} }
} else if (!buf->Filename().empty()) {
if (buf->SaveAs(buf->Filename(), err)) {
buf->SetDirty(false);
} else {
ctx.editor.SetStatus(err);
return false;
}
} else { } else {
ctx.editor.SetStatus("Buffer not file-backed; use save-as before quitting"); ctx.editor.SetStatus("Buffer not file-backed; use save-as before quitting");
return false; return false;
@@ -130,298 +158,345 @@ static bool cmd_save_and_quit(CommandContext &ctx)
return true; return true;
} }
static bool cmd_refresh(CommandContext &ctx)
static bool
cmd_refresh(CommandContext &ctx)
{ {
// Placeholder: renderer will handle this in Milestone 3 // Placeholder: renderer will handle this in Milestone 3
ctx.editor.SetStatus("Refresh requested"); ctx.editor.SetStatus("Refresh requested");
return true; return true;
} }
static bool cmd_find_start(CommandContext &ctx) static bool
cmd_kprefix(CommandContext &ctx)
{ {
// Placeholder for incremental search start // Show k-command mode hint in status
ctx.editor.SetStatus("Find (incremental) start"); ctx.editor.SetStatus("C-k _");
return true; return true;
} }
static bool
cmd_find_start(CommandContext &ctx)
{
// Placeholder for incremental search start
ctx.editor.SetStatus("Find (incremental) start");
return true;
}
// --- Editing --- // --- Editing ---
static bool cmd_insert_text(CommandContext &ctx) static bool
cmd_insert_text(CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; return false;
} }
// Disallow newlines in InsertText; they should come via Newline // Disallow newlines in InsertText; they should come via Newline
if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) { if (ctx.arg.find('\n') != std::string::npos || ctx.arg.find('\r') != std::string::npos) {
ctx.editor.SetStatus("InsertText arg must not contain newlines"); ctx.editor.SetStatus("InsertText arg must not contain newlines");
return false; return false;
} }
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
std::size_t x = buf->Curx(); std::size_t x = buf->Curx();
if (y >= rows.size()) { if (y >= rows.size()) {
rows.resize(y + 1); rows.resize(y + 1);
} }
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
rows[y].insert(x, ctx.arg); rows[y].insert(x, ctx.arg);
x += ctx.arg.size(); x += ctx.arg.size();
} }
buf->SetCursor(x, y); buf->SetCursor(x, y);
buf->SetDirty(true); buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf); ensure_cursor_visible(ctx.editor, *buf);
return true; return true;
} }
static bool cmd_newline(CommandContext &ctx)
static bool
cmd_newline(CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; return false;
} }
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
std::size_t x = buf->Curx(); std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
if (y >= rows.size()) rows.resize(y + 1); if (y >= rows.size())
std::string &line = rows[y]; rows.resize(y + 1);
std::string tail; std::string &line = rows[y];
if (x < line.size()) { std::string tail;
tail = line.substr(x); if (x < line.size()) {
line.erase(x); tail = line.substr(x);
} line.erase(x);
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), tail); }
y += 1; rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
x = 0; y += 1;
} x = 0;
buf->SetCursor(x, y); }
buf->SetDirty(true); buf->SetCursor(x, y);
ensure_cursor_visible(ctx.editor, *buf); buf->SetDirty(true);
return 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; return false;
} }
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
std::size_t x = buf->Curx(); std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
if (x > 0) { if (x > 0) {
rows[y].erase(x - 1, 1); rows[y].erase(x - 1, 1);
--x; --x;
} else if (y > 0) { } else if (y > 0) {
// join with previous line // join with previous line
std::size_t prev_len = rows[y - 1].size(); std::size_t prev_len = rows[y - 1].size();
rows[y - 1] += rows[y]; rows[y - 1] += rows[y];
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y)); rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
y = y - 1; y = y - 1;
x = prev_len; x = prev_len;
} else { } else {
// at very start; nothing to do // at very start; nothing to do
break; break;
} }
} }
buf->SetCursor(x, y); buf->SetCursor(x, y);
buf->SetDirty(true); buf->SetDirty(true);
ensure_cursor_visible(ctx.editor, *buf); ensure_cursor_visible(ctx.editor, *buf);
return true; return true;
} }
static bool cmd_delete_char(CommandContext &ctx)
static bool
cmd_delete_char(CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
ctx.editor.SetStatus("No buffer to edit"); ctx.editor.SetStatus("No buffer to edit");
return false; return false;
} }
ensure_at_least_one_line(*buf); ensure_at_least_one_line(*buf);
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t y = buf->Cury(); std::size_t y = buf->Cury();
std::size_t x = buf->Curx(); std::size_t x = buf->Curx();
int repeat = ctx.count > 0 ? ctx.count : 1; int repeat = ctx.count > 0 ? ctx.count : 1;
for (int i = 0; i < repeat; ++i) { for (int i = 0; i < repeat; ++i) {
if (y >= rows.size()) break; if (y >= rows.size())
if (x < rows[y].size()) { break;
rows[y].erase(x, 1); if (x < rows[y].size()) {
} else if (y + 1 < rows.size()) { rows[y].erase(x, 1);
// join next line } else if (y + 1 < rows.size()) {
rows[y] += rows[y + 1]; // join next line
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1)); rows[y] += rows[y + 1];
} else { rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y + 1));
break; } else {
} break;
} }
buf->SetDirty(true); }
ensure_cursor_visible(ctx.editor, *buf); buf->SetDirty(true);
return true; ensure_cursor_visible(ctx.editor, *buf);
return true;
} }
// --- Navigation --- // --- Navigation ---
// (helper removed) // (helper removed)
static bool cmd_move_left(CommandContext &ctx) static bool
cmd_move_left(CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
auto &rows = buf->Rows(); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); auto &rows = buf->Rows();
std::size_t x = buf->Curx(); std::size_t y = buf->Cury();
int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t x = buf->Curx();
while (repeat-- > 0) { int repeat = ctx.count > 0 ? ctx.count : 1;
if (x > 0) { while (repeat-- > 0) {
--x; if (x > 0) {
} else if (y > 0) { --x;
--y; } else if (y > 0) {
x = rows[y].size(); --y;
} x = rows[y].size();
} }
buf->SetCursor(x, y); }
ensure_cursor_visible(ctx.editor, *buf); buf->SetCursor(x, y);
return true; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
auto &rows = buf->Rows(); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); auto &rows = buf->Rows();
std::size_t x = buf->Curx(); std::size_t y = buf->Cury();
int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t x = buf->Curx();
while (repeat-- > 0) { int repeat = ctx.count > 0 ? ctx.count : 1;
if (y < rows.size() && x < rows[y].size()) { while (repeat-- > 0) {
++x; if (y < rows.size() && x < rows[y].size()) {
} else if (y + 1 < rows.size()) { ++x;
++y; } else if (y + 1 < rows.size()) {
x = 0; ++y;
} x = 0;
} }
buf->SetCursor(x, y); }
ensure_cursor_visible(ctx.editor, *buf); buf->SetCursor(x, y);
return true; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
auto &rows = buf->Rows(); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); auto &rows = buf->Rows();
std::size_t x = buf->Curx(); std::size_t y = buf->Cury();
int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t x = buf->Curx();
if (repeat > static_cast<int>(y)) repeat = static_cast<int>(y); int repeat = ctx.count > 0 ? ctx.count : 1;
y -= static_cast<std::size_t>(repeat); if (repeat > static_cast<int>(y))
if (x > rows[y].size()) x = rows[y].size(); repeat = static_cast<int>(y);
buf->SetCursor(x, y); y -= static_cast<std::size_t>(repeat);
ensure_cursor_visible(ctx.editor, *buf); if (x > rows[y].size())
return true; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
auto &rows = buf->Rows(); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); auto &rows = buf->Rows();
std::size_t x = buf->Curx(); std::size_t y = buf->Cury();
int repeat = ctx.count > 0 ? ctx.count : 1; std::size_t x = buf->Curx();
std::size_t max_down = rows.size() - 1 - y; int repeat = ctx.count > 0 ? ctx.count : 1;
if (repeat > static_cast<int>(max_down)) repeat = static_cast<int>(max_down); std::size_t max_down = rows.size() - 1 - y;
y += static_cast<std::size_t>(repeat); if (repeat > static_cast<int>(max_down))
if (x > rows[y].size()) x = rows[y].size(); repeat = static_cast<int>(max_down);
buf->SetCursor(x, y); y += static_cast<std::size_t>(repeat);
ensure_cursor_visible(ctx.editor, *buf); if (x > rows[y].size())
return true; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
std::size_t y = buf->Cury(); ensure_at_least_one_line(*buf);
buf->SetCursor(0, y); std::size_t y = buf->Cury();
ensure_cursor_visible(ctx.editor, *buf); buf->SetCursor(0, y);
return true; 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) return false; if (!buf)
ensure_at_least_one_line(*buf); return false;
auto &rows = buf->Rows(); ensure_at_least_one_line(*buf);
std::size_t y = buf->Cury(); auto &rows = buf->Rows();
std::size_t x = (y < rows.size()) ? rows[y].size() : 0; std::size_t y = buf->Cury();
buf->SetCursor(x, y); std::size_t x = (y < rows.size()) ? rows[y].size() : 0;
ensure_cursor_visible(ctx.editor, *buf); buf->SetCursor(x, y);
return true; ensure_cursor_visible(ctx.editor, *buf);
return true;
} }
std::vector<Command> & std::vector<Command> &
CommandRegistry::storage_() CommandRegistry::storage_()
{ {
static std::vector<Command> cmds; static std::vector<Command> cmds;
return cmds; return cmds;
} }
void void
CommandRegistry::Register(const Command &cmd) CommandRegistry::Register(const Command &cmd)
{ {
auto &v = storage_(); auto &v = storage_();
// Replace existing with same id or name // Replace existing with same id or name
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) {
return c.id == cmd.id || c.name == cmd.name; return c.id == cmd.id || c.name == cmd.name;
}); });
if (it != v.end()) { if (it != v.end()) {
*it = cmd; *it = cmd;
} else { } else {
v.push_back(cmd); v.push_back(cmd);
} }
} }
const Command * const Command *
CommandRegistry::FindById(CommandId id) CommandRegistry::FindById(CommandId id)
{ {
auto &v = storage_(); auto &v = storage_();
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.id == id; }); auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) {
return it == v.end() ? nullptr : &*it; return c.id == id;
});
return it == v.end() ? nullptr : &*it;
} }
const Command * const Command *
CommandRegistry::FindByName(const std::string &name) CommandRegistry::FindByName(const std::string &name)
{ {
auto &v = storage_(); auto &v = storage_();
auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) { return c.name == name; }); auto it = std::find_if(v.begin(), v.end(), [&](const Command &c) {
return it == v.end() ? nullptr : &*it; return c.name == name;
});
return it == v.end() ? nullptr : &*it;
} }
const std::vector<Command> & const std::vector<Command> &
CommandRegistry::All() CommandRegistry::All()
{ {
return storage_(); return storage_();
} }
@@ -433,34 +508,42 @@ InstallDefaultCommands()
CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit}); 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::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh}); 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}); CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
// Editing // Editing
CommandRegistry::Register({CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text}); CommandRegistry::Register({
CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline}); CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace}); });
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char}); CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline});
// Navigation CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left}); CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right}); // Navigation
CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up}); CommandRegistry::Register({CommandId::MoveLeft, "left", "Move cursor left", cmd_move_left});
CommandRegistry::Register({CommandId::MoveDown, "down", "Move cursor down", cmd_move_down}); CommandRegistry::Register({CommandId::MoveRight, "right", "Move cursor right", cmd_move_right});
CommandRegistry::Register({CommandId::MoveHome, "home", "Move to beginning of line", cmd_move_home}); CommandRegistry::Register({CommandId::MoveUp, "up", "Move cursor up", cmd_move_up});
CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end}); 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); const Command *cmd = CommandRegistry::FindById(id);
if (!cmd) return false; if (!cmd)
CommandContext ctx{ed, arg, count}; return false;
return cmd->handler ? cmd->handler(ctx) : 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); const Command *cmd = CommandRegistry::FindByName(name);
if (!cmd) return false; if (!cmd)
CommandContext ctx{ed, arg, count}; return false;
return cmd->handler ? cmd->handler(ctx) : false; CommandContext ctx{ed, arg, count};
return cmd->handler ? cmd->handler(ctx) : false;
} }

View File

@@ -20,6 +20,7 @@ enum class CommandId {
Quit, Quit,
SaveAndQuit, SaveAndQuit,
Refresh, // force redraw Refresh, // force redraw
KPrefix, // show "C-k _" prompt in status when entering k-command
FindStart, // begin incremental search (placeholder) FindStart, // begin incremental search (placeholder)
// Editing // Editing
InsertText, // arg: text to insert at cursor (UTF-8, no newlines) InsertText, // arg: text to insert at cursor (UTF-8, no newlines)

View File

@@ -1,6 +1,7 @@
#include "GUIInputHandler.h" #include "GUIInputHandler.h"
#include <SDL.h> #include <SDL.h>
#include "KKeymap.h"
static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput &out) 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) { switch (key) {
case SDLK_k: case SDLK_KP_EQUALS: // treat Ctrl-K case SDLK_k: case SDLK_KP_EQUALS: // treat Ctrl-K
k_prefix = true; k_prefix = true;
out = {true, CommandId::Refresh, "", 0}; out = {true, CommandId::KPrefix, "", 0};
return true; return true;
case SDLK_g: case SDLK_g:
k_prefix = false; k_prefix = false;
@@ -42,11 +43,19 @@ static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput
if (k_prefix) { if (k_prefix) {
k_prefix = false; k_prefix = false;
switch (key) { // Normalize SDL key to ASCII where possible
case SDLK_s: out = {true, CommandId::Save, "", 0}; return true; int ascii_key = 0;
case SDLK_x: out = {true, CommandId::SaveAndQuit, "", 0}; return true; if (key >= SDLK_SPACE && key <= SDLK_z) {
case SDLK_q: out = {true, CommandId::Quit, "", 0}; return true; ascii_key = static_cast<int>(key);
default: break; }
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; out.hasCommand = false;
return true; return true;

View File

@@ -4,6 +4,7 @@
#include "Buffer.h" #include "Buffer.h"
#include <imgui.h> #include <imgui.h>
#include <cstdio>
void GUIRenderer::Draw(const Editor &ed) void GUIRenderer::Draw(const Editor &ed)
{ {
@@ -35,20 +36,58 @@ void GUIRenderer::Draw(const Editor &ed)
// Reserve space for status bar at bottom // Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar); ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar);
std::size_t rowoffs = buf->Rowoffs(); 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) { 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<long>(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(); ImGui::EndChild();
// Status bar // Status bar spanning full width
ImGui::Separator(); ImGui::Separator();
const char *fname = (buf->IsFileBacked()) ? buf->Filename().c_str() : "(new)"; const char *fname = (buf->IsFileBacked()) ? buf->Filename().c_str() : "(new)";
bool dirty = buf->Dirty(); bool dirty = buf->Dirty();
ImGui::Text("%s%s %zux%zu %s", char status[1024];
fname, snprintf(status, sizeof(status), " %s%s %zux%zu %s ",
dirty ? "*" : "", fname,
ed.Rows(), ed.Cols(), dirty ? "*" : "",
ed.Status().c_str()); 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(); ImGui::End();

24
KKeymap.cpp Normal file
View File

@@ -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;
}

26
KKeymap.h Normal file
View File

@@ -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 <cctype>
// 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

View File

@@ -1,6 +1,7 @@
#include "TerminalInputHandler.h" #include "TerminalInputHandler.h"
#include <ncurses.h> #include <ncurses.h>
#include "KKeymap.h"
namespace { namespace {
constexpr int CTRL(char c) { return c & 0x1F; } 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 // Control keys
if (ch == CTRL('K')) { // C-k prefix if (ch == CTRL('K')) { // C-k prefix
k_prefix = true; k_prefix = true;
out = {true, CommandId::Refresh, "", 0}; out = {true, CommandId::KPrefix, "", 0};
return true; return true;
} }
if (ch == CTRL('G')) { // cancel 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) { if (k_prefix) {
k_prefix = false; // single next key only k_prefix = false; // single next key only
switch (ch) { // Determine if this is a control chord (e.g., C-x) and normalize
case 's': case 'S': out = {true, CommandId::Save, "", 0}; return true; bool ctrl = false;
case 'x': case 'X': out = {true, CommandId::SaveAndQuit, "", 0}; return true; int ascii_key = ch;
case 'q': case 'Q': out = {true, CommandId::Quit, "", 0}; return true; if (ch >= 1 && ch <= 26) {
default: break; 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; return true;
} }

View File

@@ -43,12 +43,27 @@ void TerminalRenderer::Draw(const Editor &ed)
clrtoeol(); 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 cy = buf->Cury();
std::size_t cx = buf->Curx(); std::size_t cx = buf->Curx();
int cur_y = static_cast<int>(cy - buf->Rowoffs()); int cur_y = static_cast<int>(cy - buf->Rowoffs());
int cur_x = static_cast<int>(cx - buf->Coloffs()); int cur_x = static_cast<int>(cx - buf->Coloffs());
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) { 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<long>(cx)];
}
}
move(cur_y, cur_x);
attron(A_REVERSE);
addch(static_cast<unsigned char>(ch));
attroff(A_REVERSE);
// Also place the terminal cursor at the same spot
move(cur_y, cur_x); move(cur_y, cur_x);
} }
} else { } else {