diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index b240c10..d74344c 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -22,31 +22,40 @@
+
-
+
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -100,22 +109,31 @@
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
- "settings.editor.selected.configurable": "preferences.sourceCode.C++",
+ "settings.editor.selected.configurable": "CMakeSettings",
"to.speed.mode.migration.done": "true",
"vue.rearranger.settings.migration": "true"
}
}]]>
-
+
+
+
+
+
+
+
+
+
+
@@ -124,7 +142,7 @@
1764457173148
-
+
diff --git a/.junie/guidelines.md b/.junie/guidelines.md
index 9d208c0..2b32fd8 100644
--- a/.junie/guidelines.md
+++ b/.junie/guidelines.md
@@ -11,7 +11,7 @@ development practices for kte.
## Goals
- Keep the core small, fast, and understandable.
-- Provide a terminal-first editing experience, with an optional ImGui GUI.
+- Provide an ncurses-based terminal-first editing experience, with an optional ImGui GUI.
- Preserve familiar keybindings from ke while modernizing the internals.
- Favor simple data structures (e.g., gap buffer) and incremental evolution.
@@ -22,7 +22,10 @@ development practices for kte.
## Build and Run
-Prerequisites: a C++17 compiler and CMake.
+Prerequisites: a C++17 compiler, CMake, and ncurses development headers/libs.
+
+- macOS (Homebrew): `brew install ncurses`
+- Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev`
- Configure and build (example):
- `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
@@ -38,9 +41,9 @@ Project entry point: `main.cpp`
- GapBuffer: editable in-memory text representation (`GapBuffer.h/.cpp`).
- PieceTable: experimental/alternative representation (`PieceTable.h/.cpp`).
- InputHandler: interface for handling text input (`InputHandler.h/`), along
- with `TerminalInputHandler` and `GUIInputHandler`.
+ with `TerminalInputHandler` (ncurses-based) and `GUIInputHandler`.
- Renderer: interface for rendering text (`Renderer.h`), along with
- `TerminalRenderer` and `GUIRenderer`.
+ `TerminalRenderer` (ncurses-based) and `GUIRenderer`.
- Editor: top-level editor state (`Editor.h/.cpp`).
- Command: command model (`Command.h/.cpp`).
- General purpose editor functionality (`Editing.h/.cpp`)
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 9fb5d53..e9a7d4d 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -2,8 +2,11 @@ cmake_minimum_required(VERSION 3.15)
project(kte)
set(CMAKE_CXX_STANDARD 17)
+set (KTE_VERSION "0.0.1")
-set(BUILD_GUI ON CACHE BOOL "Disable building the graphical version.")
+# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
+# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
+set(BUILD_GUI OFF CACHE BOOL "Enable building the graphical version.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" OFF)
@@ -20,7 +23,6 @@ else ()
"-Wall"
"-Wextra"
"-Werror"
- "-static"
"$<$:-g>"
"$<$:-O2>")
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
@@ -38,11 +40,19 @@ if (${BUILD_GUI})
include(cmake/imgui.cmake)
endif ()
+# NCurses for terminal mode
+find_package(Curses REQUIRED)
+include_directories(${CURSES_INCLUDE_DIR})
+
set(SOURCES
GapBuffer.cpp
PieceTable.cpp
Buffer.cpp
Editor.cpp
+ Command.cpp
+ TerminalInputHandler.cpp
+ TerminalRenderer.cpp
+ TerminalFrontend.cpp
)
set(HEADERS
@@ -51,6 +61,13 @@ set(HEADERS
Buffer.h
Editor.h
AppendBuffer.h
+ Command.h
+ InputHandler.h
+ TerminalInputHandler.h
+ Renderer.h
+ TerminalRenderer.h
+ Frontend.h
+ TerminalFrontend.h
)
add_executable(kte
@@ -63,6 +80,16 @@ if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
+target_link_libraries(kte ${CURSES_LIBRARIES})
+
if (${BUILD_GUI})
- target_link_libraries(kge imgui)
+ target_sources(kte PRIVATE
+ GUIRenderer.cpp
+ GUIRenderer.h
+ GUIInputHandler.cpp
+ GUIInputHandler.h
+ GUIFrontend.cpp
+ GUIFrontend.h)
+ target_compile_definitions(kte PRIVATE KTE_BUILD_GUI=1)
+ target_link_libraries(kte imgui)
endif ()
diff --git a/Command.cpp b/Command.cpp
new file mode 100644
index 0000000..cad5802
--- /dev/null
+++ b/Command.cpp
@@ -0,0 +1,466 @@
+#include "Command.h"
+
+#include
+
+#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)
+{
+ std::size_t rows = ed.Rows();
+ 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();
+
+ // 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;
+ }
+
+ // Horizontal scrolling
+ if (curx < coloffs) {
+ coloffs = curx;
+ } else if (curx >= coloffs + cols) {
+ coloffs = curx - cols + 1;
+ }
+
+ buf.SetOffsets(rowoffs, coloffs);
+}
+
+static void ensure_at_least_one_line(Buffer &buf)
+{
+ if (buf.Rows().empty()) {
+ buf.Rows().push_back("");
+ buf.SetDirty(true);
+ }
+}
+
+// (helper removed)
+
+// --- File/Session commands ---
+static bool cmd_save(CommandContext &ctx)
+{
+ Buffer *buf = ctx.editor.CurrentBuffer();
+ if (!buf) {
+ ctx.editor.SetStatus("No buffer to save");
+ return false;
+ }
+ std::string err;
+ if (!buf->IsFileBacked()) {
+ ctx.editor.SetStatus("Buffer is not file-backed; use save-as");
+ return false;
+ }
+ if (!buf->Save(err)) {
+ ctx.editor.SetStatus(err);
+ return false;
+ }
+ buf->SetDirty(false);
+ ctx.editor.SetStatus("Saved " + buf->Filename());
+ return true;
+}
+
+
+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;
+}
+
+
+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;
+}
+
+
+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;
+ if (buf->IsFileBacked()) {
+ if (buf->Save(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;
+ }
+ }
+ ctx.editor.SetStatus("Save and quit requested");
+ return true;
+}
+
+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)
+{
+ // Placeholder for incremental search start
+ ctx.editor.SetStatus("Find (incremental) start");
+ return true;
+}
+
+
+// --- Editing ---
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+// --- Navigation ---
+// (helper removed)
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+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;
+}
+
+
+std::vector &
+CommandRegistry::storage_()
+{
+ 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);
+ }
+}
+
+
+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;
+}
+
+
+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;
+}
+
+
+const std::vector &
+CommandRegistry::All()
+{
+ return storage_();
+}
+
+
+void
+InstallDefaultCommands()
+{
+ CommandRegistry::Register({CommandId::Save, "save", "Save current buffer", cmd_save});
+ CommandRegistry::Register({CommandId::SaveAs, "save-as", "Save current buffer as...", cmd_save_as});
+ CommandRegistry::Register({CommandId::Quit, "quit", "Quit editor (request)", cmd_quit});
+ CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
+ CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
+ 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});
+}
+
+
+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;
+}
+
+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;
+}
diff --git a/Command.h b/Command.h
new file mode 100644
index 0000000..0f3ac8a
--- /dev/null
+++ b/Command.h
@@ -0,0 +1,86 @@
+/*
+ * Command.h - command model and registry for editor actions
+ */
+#ifndef KTE_COMMAND_H
+#define KTE_COMMAND_H
+
+#include
+#include
+#include
+#include
+
+class Editor;
+
+// Identifiers for editor commands. This is intentionally small for now and
+// will grow as features are implemented.
+enum class CommandId {
+ Save,
+ SaveAs,
+ // Placeholders for future commands from ke's model
+ Quit,
+ SaveAndQuit,
+ Refresh, // force redraw
+ FindStart, // begin incremental search (placeholder)
+ // Editing
+ InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
+ Newline, // insert a newline at cursor
+ Backspace, // delete char before cursor (may join lines)
+ DeleteChar, // delete char at cursor (may join lines)
+ // Navigation (basic)
+ MoveLeft,
+ MoveRight,
+ MoveUp,
+ MoveDown,
+ MoveHome,
+ MoveEnd,
+};
+
+
+// Context passed to command handlers.
+struct CommandContext {
+ Editor &editor;
+
+ // Optional argument string (e.g., filename for SaveAs).
+ std::string arg;
+
+ // Optional repeat count (C-u support). 0 means not provided.
+ int count = 0;
+};
+
+
+using CommandHandler = std::function; // return true on success
+
+
+struct Command {
+ CommandId id;
+ std::string name; // stable, unique name (e.g., "save", "save-as")
+ std::string help; // short help/description
+ CommandHandler handler;
+};
+
+
+// Simple global registry. Not thread-safe; suitable for this app.
+class CommandRegistry {
+public:
+ static void Register(const Command &cmd);
+
+ static const Command *FindById(CommandId id);
+
+ static const Command *FindByName(const std::string &name);
+
+ static const std::vector &All();
+
+private:
+ static std::vector &storage_();
+};
+
+
+// Built-in command installers
+void InstallDefaultCommands();
+
+// Dispatcher entry points for the input layer
+// Returns true if the command executed successfully.
+bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
+bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
+
+#endif // KTE_COMMAND_H
diff --git a/Frontend.h b/Frontend.h
new file mode 100644
index 0000000..414865a
--- /dev/null
+++ b/Frontend.h
@@ -0,0 +1,27 @@
+/*
+ * Frontend.h - top-level container that couples Input + Renderer and runs the loop
+ */
+#ifndef KTE_FRONTEND_H
+#define KTE_FRONTEND_H
+
+#include
+
+class Editor;
+class InputHandler;
+class Renderer;
+
+class Frontend {
+public:
+ virtual ~Frontend() = default;
+
+ // Initialize the frontend (create window/terminal, etc.)
+ virtual bool Init(Editor &ed) = 0;
+
+ // Execute one iteration (poll input, dispatch, draw). Set running=false to exit.
+ virtual void Step(Editor &ed, bool &running) = 0;
+
+ // Shutdown/cleanup
+ virtual void Shutdown() = 0;
+};
+
+#endif // KTE_FRONTEND_H
diff --git a/GUIFrontend.cpp b/GUIFrontend.cpp
new file mode 100644
index 0000000..dce1833
--- /dev/null
+++ b/GUIFrontend.cpp
@@ -0,0 +1,121 @@
+#include "GUIFrontend.h"
+
+#include
+#include
+
+#include
+#include
+#include
+
+#include "Editor.h"
+#include "Command.h"
+
+static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
+
+bool GUIFrontend::Init(Editor &ed)
+{
+ if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
+ return false;
+ }
+
+ // GL attributes for core profile
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MAJOR_VERSION, 3);
+ SDL_GL_SetAttribute(SDL_GL_CONTEXT_MINOR_VERSION, 2);
+ SDL_GL_SetAttribute(SDL_GL_DOUBLEBUFFER, 1);
+ SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
+ SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
+
+ window_ = SDL_CreateWindow(
+ "kte",
+ SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
+ width_, height_,
+ SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
+ if (!window_) return false;
+
+ gl_ctx_ = SDL_GL_CreateContext(window_);
+ if (!gl_ctx_) return false;
+ SDL_GL_MakeCurrent(window_, gl_ctx_);
+ SDL_GL_SetSwapInterval(1); // vsync
+
+ IMGUI_CHECKVERSION();
+ ImGui::CreateContext();
+ ImGuiIO &io = ImGui::GetIO(); (void)io;
+ ImGui::StyleColorsDark();
+
+ if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_)) return false;
+ if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) return false;
+
+ // Initialize editor reported dimensions to pixels for now
+ int w, h; SDL_GetWindowSize(window_, &w, &h);
+ width_ = w; height_ = h;
+ ed.SetDimensions(static_cast(height_), static_cast(width_));
+
+ return true;
+}
+
+void GUIFrontend::Step(Editor &ed, bool &running)
+{
+ SDL_Event e;
+ while (SDL_PollEvent(&e)) {
+ ImGui_ImplSDL2_ProcessEvent(&e);
+ switch (e.type) {
+ case SDL_QUIT:
+ running = false;
+ break;
+ case SDL_WINDOWEVENT:
+ if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
+ width_ = e.window.data1;
+ height_ = e.window.data2;
+ ed.SetDimensions(static_cast(height_), static_cast(width_));
+ }
+ break;
+ default:
+ break;
+ }
+ // Map input to commands
+ input_.ProcessSDLEvent(e);
+ }
+
+ // Execute pending mapped inputs (drain queue)
+ for (;;) {
+ MappedInput mi;
+ if (!input_.Poll(mi)) break;
+ if (mi.hasCommand) {
+ Execute(ed, mi.id, mi.arg, mi.count);
+ if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
+ running = false;
+ }
+ }
+ }
+
+ // Start a new ImGui frame
+ ImGui_ImplOpenGL3_NewFrame();
+ ImGui_ImplSDL2_NewFrame(window_);
+ ImGui::NewFrame();
+
+ // Draw editor UI
+ renderer_.Draw(ed);
+
+ // Render
+ ImGui::Render();
+ int display_w, display_h;
+ SDL_GL_GetDrawableSize(window_, &display_w, &display_h);
+ glViewport(0, 0, display_w, display_h);
+ glClearColor(0.1f, 0.1f, 0.11f, 1.0f);
+ glClear(GL_COLOR_BUFFER_BIT);
+ ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
+ SDL_GL_SwapWindow(window_);
+}
+
+void GUIFrontend::Shutdown()
+{
+ ImGui_ImplOpenGL3_Shutdown();
+ ImGui_ImplSDL2_Shutdown();
+ ImGui::DestroyContext();
+
+ if (gl_ctx_) { SDL_GL_DeleteContext(gl_ctx_); gl_ctx_ = nullptr; }
+ if (window_) { SDL_DestroyWindow(window_); window_ = nullptr; }
+ SDL_Quit();
+}
diff --git a/GUIFrontend.h b/GUIFrontend.h
new file mode 100644
index 0000000..d2ec371
--- /dev/null
+++ b/GUIFrontend.h
@@ -0,0 +1,32 @@
+/*
+ * GUIFrontend - couples GUIInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
+ */
+#ifndef KTE_GUI_FRONTEND_H
+#define KTE_GUI_FRONTEND_H
+
+#include "Frontend.h"
+#include "GUIInputHandler.h"
+#include "GUIRenderer.h"
+
+struct SDL_Window;
+typedef void *SDL_GLContext;
+
+class GUIFrontend : public Frontend {
+public:
+ GUIFrontend() = default;
+ ~GUIFrontend() override = default;
+
+ bool Init(Editor &ed) override;
+ void Step(Editor &ed, bool &running) override;
+ void Shutdown() override;
+
+private:
+ GUIInputHandler input_{};
+ GUIRenderer renderer_{};
+ SDL_Window *window_ = nullptr;
+ SDL_GLContext gl_ctx_ = nullptr;
+ int width_ = 1280;
+ int height_ = 800;
+};
+
+#endif // KTE_GUI_FRONTEND_H
diff --git a/GUIInputHandler.cpp b/GUIInputHandler.cpp
new file mode 100644
index 0000000..e6f87d1
--- /dev/null
+++ b/GUIInputHandler.cpp
@@ -0,0 +1,92 @@
+#include "GUIInputHandler.h"
+
+#include
+
+static bool map_key(SDL_Keycode key, SDL_Keymod mod, bool &k_prefix, MappedInput &out)
+{
+ // Ctrl handling
+ bool is_ctrl = (mod & KMOD_CTRL) != 0;
+
+ // Movement and basic keys
+ switch (key) {
+ case SDLK_LEFT: out = {true, CommandId::MoveLeft, "", 0}; return true;
+ case SDLK_RIGHT: out = {true, CommandId::MoveRight, "", 0}; return true;
+ case SDLK_UP: out = {true, CommandId::MoveUp, "", 0}; return true;
+ case SDLK_DOWN: out = {true, CommandId::MoveDown, "", 0}; return true;
+ case SDLK_HOME: out = {true, CommandId::MoveHome, "", 0}; return true;
+ case SDLK_END: out = {true, CommandId::MoveEnd, "", 0}; return true;
+ case SDLK_DELETE: out = {true, CommandId::DeleteChar, "", 0}; return true;
+ case SDLK_BACKSPACE: out = {true, CommandId::Backspace, "", 0}; return true;
+ case SDLK_RETURN: case SDLK_KP_ENTER: out = {true, CommandId::Newline, "", 0}; return true;
+ case SDLK_ESCAPE: k_prefix = false; out = {true, CommandId::Refresh, "", 0}; return true;
+ default: break;
+ }
+
+ if (is_ctrl) {
+ switch (key) {
+ case SDLK_k: case SDLK_KP_EQUALS: // treat Ctrl-K
+ k_prefix = true;
+ out = {true, CommandId::Refresh, "", 0};
+ return true;
+ case SDLK_g:
+ k_prefix = false;
+ out = {true, CommandId::Refresh, "", 0};
+ return true;
+ case SDLK_l: out = {true, CommandId::Refresh, "", 0}; return true;
+ case SDLK_s: out = {true, CommandId::FindStart, "", 0}; return true;
+ case SDLK_q: out = {true, CommandId::Quit, "", 0}; return true;
+ case SDLK_x: out = {true, CommandId::SaveAndQuit, "", 0}; return true;
+ default: break;
+ }
+ }
+
+ if (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;
+ }
+ out.hasCommand = false;
+ return true;
+ }
+
+ return false;
+}
+
+bool GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
+{
+ MappedInput mi;
+ bool produced = false;
+ switch (e.type) {
+ case SDL_KEYDOWN:
+ produced = map_key(e.key.keysym.sym, SDL_Keymod(e.key.keysym.mod), k_prefix_, mi);
+ break;
+ case SDL_TEXTINPUT:
+ if (e.text.text[0] != '\0') {
+ mi.hasCommand = true;
+ mi.id = CommandId::InsertText;
+ mi.arg = std::string(e.text.text);
+ mi.count = 0;
+ produced = true;
+ }
+ break;
+ default:
+ break;
+ }
+ if (produced && mi.hasCommand) {
+ std::lock_guard lk(mu_);
+ q_.push(mi);
+ }
+ return produced;
+}
+
+bool GUIInputHandler::Poll(MappedInput &out)
+{
+ std::lock_guard lk(mu_);
+ if (q_.empty()) return false;
+ out = q_.front();
+ q_.pop();
+ return true;
+}
diff --git a/GUIInputHandler.h b/GUIInputHandler.h
new file mode 100644
index 0000000..d3820a7
--- /dev/null
+++ b/GUIInputHandler.h
@@ -0,0 +1,31 @@
+/*
+ * GUIInputHandler - ImGui/SDL2-based input mapping for GUI mode
+ */
+#ifndef KTE_GUI_INPUT_HANDLER_H
+#define KTE_GUI_INPUT_HANDLER_H
+
+#include
+#include
+
+#include "InputHandler.h"
+
+union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
+
+class GUIInputHandler : public InputHandler {
+public:
+ GUIInputHandler() = default;
+ ~GUIInputHandler() override = default;
+
+ // Translate an SDL event to editor command and enqueue if applicable.
+ // Returns true if it produced a mapped command or consumed input.
+ bool ProcessSDLEvent(const SDL_Event &e);
+
+ bool Poll(MappedInput &out) override;
+
+private:
+ std::mutex mu_;
+ std::queue q_;
+ bool k_prefix_ = false;
+};
+
+#endif // KTE_GUI_INPUT_HANDLER_H
diff --git a/GUIRenderer.cpp b/GUIRenderer.cpp
new file mode 100644
index 0000000..ab56a09
--- /dev/null
+++ b/GUIRenderer.cpp
@@ -0,0 +1,37 @@
+#include "GUIRenderer.h"
+
+#include "Editor.h"
+#include "Buffer.h"
+
+#include
+
+void GUIRenderer::Draw(const Editor &ed)
+{
+ ImGui::Begin("kte");
+
+ const Buffer *buf = ed.CurrentBuffer();
+ if (!buf) {
+ ImGui::TextUnformatted("[no buffer]");
+ } else {
+ const auto &lines = buf->Rows();
+ // Reserve space for status bar at bottom
+ ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false, ImGuiWindowFlags_HorizontalScrollbar);
+ std::size_t rowoffs = buf->Rowoffs();
+ for (std::size_t i = rowoffs; i < lines.size(); ++i) {
+ ImGui::TextUnformatted(lines[i].c_str());
+ }
+ ImGui::EndChild();
+
+ // Status bar
+ 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());
+ }
+
+ ImGui::End();
+}
diff --git a/GUIRenderer.h b/GUIRenderer.h
new file mode 100644
index 0000000..96a5f4a
--- /dev/null
+++ b/GUIRenderer.h
@@ -0,0 +1,17 @@
+/*
+ * GUIRenderer - ImGui-based renderer for GUI mode
+ */
+#ifndef KTE_GUI_RENDERER_H
+#define KTE_GUI_RENDERER_H
+
+#include "Renderer.h"
+
+class GUIRenderer : public Renderer {
+public:
+ GUIRenderer() = default;
+ ~GUIRenderer() override = default;
+
+ void Draw(const Editor &ed) override;
+};
+
+#endif // KTE_GUI_RENDERER_H
diff --git a/InputHandler.h b/InputHandler.h
new file mode 100644
index 0000000..cd6ef83
--- /dev/null
+++ b/InputHandler.h
@@ -0,0 +1,28 @@
+/*
+ * InputHandler.h - input abstraction and mapping to commands
+ */
+#ifndef KTE_INPUT_HANDLER_H
+#define KTE_INPUT_HANDLER_H
+
+#include
+
+#include "Command.h"
+
+// Result of translating raw input into an editor command.
+struct MappedInput {
+ bool hasCommand = false;
+ CommandId id = CommandId::Refresh;
+ std::string arg; // optional argument (e.g., text for InsertText)
+ int count = 0; // optional repeat (C-u not yet implemented)
+};
+
+class InputHandler {
+public:
+ virtual ~InputHandler() = default;
+
+ // Poll for input and translate it to a command. Non-blocking.
+ // Returns true if a command is available in 'out'. Returns false if no input.
+ virtual bool Poll(MappedInput &out) = 0;
+};
+
+#endif // KTE_INPUT_HANDLER_H
diff --git a/README.md b/README.md
index 30fd3f7..0e564fa 100644
--- a/README.md
+++ b/README.md
@@ -75,7 +75,8 @@ Interfaces
----------
- CLI: the primary interface. `kte [files]` starts in the terminal,
- adopting your `$TERM` capabilities.
+ adopting your `$TERM` capabilities. Terminal mode is implemented
+ using ncurses.
- GUI: an optional ImGui‑based frontend that embeds the same editor
core.
@@ -130,7 +131,41 @@ See `ke.md` for the canonical ke reference retained for now.
Build and Run
-------------
-Prerequisites: C++17 compiler and CMake.
+Prerequisites: C++17 compiler, CMake, and ncurses development headers/libs.
+
+Dependencies by platform
+------------------------
+
+- macOS (Homebrew)
+ - Terminal (default):
+ - `brew install ncurses`
+ - Optional GUI (enable with `-DBUILD_GUI=ON`):
+ - `brew install sdl2 freetype`
+ - OpenGL is provided by the system framework on macOS; no package needed.
+
+- Debian/Ubuntu
+ - Terminal (default):
+ - `sudo apt-get install -y libncurses5-dev libncursesw5-dev`
+ - Optional GUI (enable with `-DBUILD_GUI=ON`):
+ - `sudo apt-get install -y libsdl2-dev libfreetype6-dev mesa-common-dev`
+ - The `mesa-common-dev` package provides OpenGL headers/libs (`libGL`).
+
+- NixOS/Nix
+ - Terminal (default):
+ - Ad-hoc shell: `nix-shell -p cmake gcc ncurses`
+ - Optional GUI (enable with `-DBUILD_GUI=ON`):
+ - Ad-hoc shell: `nix-shell -p cmake gcc ncurses SDL2 freetype libGL`
+ - With flakes/devshell (example `flake.nix` inputs not provided): include
+ `ncurses` for TUI, and `SDL2`, `freetype`, `libGL` for GUI in your devShell.
+
+Notes
+-----
+
+- The GUI is OFF by default to keep SDL/OpenGL/Freetype optional. Enable it by
+ configuring with `-DBUILD_GUI=ON` and ensuring the GUI deps above are
+ installed for your platform.
+- If you previously configured with GUI ON and want to disable it, reconfigure
+ the build directory with `-DBUILD_GUI=OFF`.
Example build:
@@ -145,13 +180,50 @@ Run:
./cmake-build-debug/kte [files]
```
+CLI usage
+---------
+
+```
+kte [OPTIONS] [files]
+
+Options:
+ -g, --gui Use GUI frontend (if built)
+ -t, --term Use terminal (ncurses) frontend [default]
+ -h, --help Show help and exit
+ -V, --version Show version and exit
+```
+
+Examples:
+
+```
+# Terminal (default)
+kte foo.txt bar.txt
+
+# Explicit terminal
+kte -t foo.txt
+
+# GUI (requires building with -DBUILD_GUI=ON and GUI deps installed)
+kte --gui foo.txt
+```
+
+GUI build example
+-----------------
+
+To build with the optional GUI (after installing the GUI dependencies listed above):
+
+```
+cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug -DBUILD_GUI=ON
+cmake --build cmake-build-debug
+./cmake-build-debug/kte --gui [files]
+```
+
Status
------
- The project is under active evolution toward the above architecture
- and UX. The terminal interface is the leading target; GUI work will
- follow as a thin, optional layer. ke compatibility remains a primary
- constraint while internals modernize.
+ and UX. The terminal interface now uses ncurses for input and
+ rendering. GUI work will follow as a thin, optional layer. ke
+ compatibility remains a primary constraint while internals modernize.
Roadmap (high level)
--------------------
@@ -161,7 +233,7 @@ Roadmap (high level)
2. Introduce structured undo/redo and search/replace with
highlighting.
3. Stabilize terminal renderer and input handling across common
- terminals.
+ terminals. (initial ncurses implementation landed)
4. Add piece table as an alternative backend with runtime selection
per buffer.
5. Optional GUI frontend using ImGui; shared command palette.
@@ -174,4 +246,4 @@ References
- [ke](https://git.wntrmute.dev/kyle/ke) manual and keybinding
reference: `ke.md`
-- Inspirations: Antirez’ kilo, WordStar/VDE, Emacs, and `mg(1)`
+- Inspirations: Antirez’ kilo, WordStar/VDE, Emacs, and `mg(1)`
\ No newline at end of file
diff --git a/ROADMAP.md b/ROADMAP.md
new file mode 100644
index 0000000..85629ac
--- /dev/null
+++ b/ROADMAP.md
@@ -0,0 +1,97 @@
+kte ROADMAP — from skeleton to a working editor
+
+Scope for “working editor” v0.1
+- Runs in a terminal; opens files passed on the CLI or an empty buffer.
+- Basic navigation, insert/delete, newline handling.
+- Status line and message area; shows filename, dirty flag, cursor position.
+- Save file(s) to disk safely; quit/confirm on dirty buffers.
+- Core ke key chords: C-g (cancel), C-k s/x/q/C-q, C-l, basic arrows, Enter/Backspace, C-s (simple find).
+
+Guiding principles
+- Keep the core small and understandable; evolve incrementally.
+- Separate model (Buffer/Editor), control (Input/Command), and view (Renderer).
+- Favor terminal first; GUI hooks arrive later behind interfaces.
+
+✓ Milestone 0 — Wire up a minimal app shell
+1. main.cpp
+ - Replace demo printing with real startup using `Editor`.
+ - Parse CLI args; open each path into a buffer (create empty if none). ✓ when `kte file1 file2` loads buffers and exits cleanly.
+2. Editor integration
+ - Ensure `Editor` can open/switch/close buffers and hold status messages.
+ - Add a temporary “headless loop” to prove open/save calls work.
+
+✓ Milestone 1 — Command model
+1. Command vocabulary
+ - Flesh out `Command.h/.cpp`: enums/struct for operations and data (e.g., InsertChar, MoveCursor, Save, Quit, FindNext, etc.).
+ - Provide a dispatcher entry point callable from the input layer to mutate `Editor`/`Buffer`.
+ - Definition of done: commands exist for minimal edit/navigation/save/quit; no rendering yet.
+
+✓ Milestone 2 — Terminal input
+1. Input interfaces
+ - Add `InputHandler.h` interface plus `TerminalInputHandler` implementation.
+ - Terminal input via ncurses (`getch`, `keypad`, non‑blocking with `nodelay`), basic key decoding (arrows, Ctrl, ESC sequences).
+2. Keymap
+ - Map ke chords to `Command` (C-k prefix handling, C-g cancel, C-l refresh, C-k s/x/q/C-q, C-s find start, text input → InsertChar).
+3. Event loop
+ - Introduce the core loop in main: read key → translate to `Command` → dispatch → trigger render.
+
+Milestone 3 — Terminal renderer
+1. View interfaces
+ - Add `Renderer.h` with `TerminalRenderer` implementation (ncurses‑based).
+2. Minimal draw
+ - Render viewport lines from current buffer; draw status bar (filename, dirty, row:col, message).
+ - Handle scrolling when cursor moves past edges; support window resize (SIGWINCH).
+3. Cursor
+ - Place terminal cursor at logical buffer location (account for tabs later; start with plain text).
+
+Milestone 4 — Buffer fundamentals to support editing
+1. GapBuffer
+ - Ensure `GapBuffer` supports insert char, backspace, delete, newline, and efficient cursor moves.
+2. Buffer API
+ - File I/O (open/save), dirty tracking, encoding/line ending kept simple (UTF‑8, LF) for v0.1.
+ - Cursor state, mark (optional later), and viewport bookkeeping.
+3. Basic motions
+ - Left/Right/Up/Down, Home/End, PageUp/PageDown; word f/b (optional in v0.1).
+
+Milestone 5 — Core editing loop complete
+1. Tighten loop timing
+ - Ensure keystroke→update→render latency is reliably low; avoid unnecessary redraws.
+2. Status/messages
+ - `Editor::SetStatus()` shows transient messages; C-l forces full refresh.
+3. Prompts
+ - Minimal prompt line for save‑as/confirm quit; blocking read in prompt mode is acceptable for v0.1.
+
+Milestone 6 — Search (minimal)
+1. Incremental search (C-s)
+ - Simple forward substring search with live highlight of current match; arrow keys navigate matches while in search mode (ke‑style quirk acceptable).
+ - ESC/C-g exits search; Enter confirms and leaves cursor on match.
+
+Milestone 7 — Safety and polish for v0.1
+1. Safe writes
+ - Write to temp file then rename; preserve permissions where possible.
+2. Dirty/quit logic
+ - Confirm on quit when any buffer is dirty; `C-k C-q` bypasses confirmation.
+3. Resize/terminal quirks
+ - Handle small terminals gracefully; no crashes on narrow widths.
+4. Basic tests
+ - Unit tests for `GapBuffer`, Buffer open/save round‑trip, and command mapping.
+
+Out of scope for v0.1 (tracked, not blocking)
+- Undo/redo, regex search, kill ring, word motions, tabs/render width, syntax highlighting, piece table selection, GUI.
+
+Implementation notes (files to add)
+- Input: `InputHandler.h`, `TerminalInputHandler.cpp/h` (ncurses).
+- Rendering: `Renderer.h`, `TerminalRenderer.cpp/h` (ncurses).
+- Prompt helpers: minimal utility for line input in raw mode.
+- Platform: small termios wrapper; SIGWINCH handler.
+
+Acceptance checklist for v0.1
+- Start: `./kte [files]` opens files or an empty buffer.
+- Edit: insert text, backspace, newlines; move cursor; content scrolls.
+- Save: `C-k s` writes file atomically; dirty flag clears; status shows bytes written.
+- Quit: `C-k q` confirms if dirty; `C-k C-q` exits without confirm; `C-k x` save+exit.
+- Refresh: `C-l` redraws.
+- Search: `C-s` finds next while typing; ESC cancels.
+
+Next concrete step
+- Stabilize cursor placement and scrolling logic; add resize handling and begin minimal prompt for save‑as.
\ No newline at end of file
diff --git a/Renderer.h b/Renderer.h
new file mode 100644
index 0000000..9bb0668
--- /dev/null
+++ b/Renderer.h
@@ -0,0 +1,15 @@
+/*
+ * Renderer.h - rendering abstraction
+ */
+#ifndef KTE_RENDERER_H
+#define KTE_RENDERER_H
+
+class Editor;
+
+class Renderer {
+public:
+ virtual ~Renderer() = default;
+ virtual void Draw(const Editor &ed) = 0;
+};
+
+#endif // KTE_RENDERER_H
diff --git a/TerminalFrontend.cpp b/TerminalFrontend.cpp
new file mode 100644
index 0000000..22cf1f2
--- /dev/null
+++ b/TerminalFrontend.cpp
@@ -0,0 +1,56 @@
+#include "TerminalFrontend.h"
+
+#include
+#include
+
+#include "Editor.h"
+#include "Command.h"
+
+bool TerminalFrontend::Init(Editor &ed)
+{
+ initscr();
+ cbreak();
+ noecho();
+ keypad(stdscr, TRUE);
+ nodelay(stdscr, TRUE);
+ curs_set(1);
+
+ int r = 0, c = 0;
+ getmaxyx(stdscr, r, c);
+ prev_r_ = r; prev_c_ = c;
+ ed.SetDimensions(static_cast(r), static_cast(c));
+ return true;
+}
+
+void TerminalFrontend::Step(Editor &ed, bool &running)
+{
+ // Handle resize and keep editor dimensions synced
+ int r, c;
+ getmaxyx(stdscr, r, c);
+ if (r != prev_r_ || c != prev_c_) {
+ resizeterm(r, c);
+ clear();
+ prev_r_ = r; prev_c_ = c;
+ }
+ ed.SetDimensions(static_cast(r), static_cast(c));
+
+ MappedInput mi;
+ if (input_.Poll(mi)) {
+ if (mi.hasCommand) {
+ Execute(ed, mi.id, mi.arg, mi.count);
+ if (mi.id == CommandId::Quit || mi.id == CommandId::SaveAndQuit) {
+ running = false;
+ }
+ }
+ } else {
+ // Avoid busy loop
+ usleep(1000);
+ }
+
+ renderer_.Draw(ed);
+}
+
+void TerminalFrontend::Shutdown()
+{
+ endwin();
+}
diff --git a/TerminalFrontend.h b/TerminalFrontend.h
new file mode 100644
index 0000000..d59b77b
--- /dev/null
+++ b/TerminalFrontend.h
@@ -0,0 +1,27 @@
+/*
+ * TerminalFrontend - couples TerminalInputHandler + TerminalRenderer and owns ncurses lifecycle
+ */
+#ifndef KTE_TERMINAL_FRONTEND_H
+#define KTE_TERMINAL_FRONTEND_H
+
+#include "Frontend.h"
+#include "TerminalInputHandler.h"
+#include "TerminalRenderer.h"
+
+class TerminalFrontend : public Frontend {
+public:
+ TerminalFrontend() = default;
+ ~TerminalFrontend() override = default;
+
+ bool Init(Editor &ed) override;
+ void Step(Editor &ed, bool &running) override;
+ void Shutdown() override;
+
+private:
+ TerminalInputHandler input_{};
+ TerminalRenderer renderer_{};
+ int prev_r_ = 0;
+ int prev_c_ = 0;
+};
+
+#endif // KTE_TERMINAL_FRONTEND_H
diff --git a/TerminalInputHandler.cpp b/TerminalInputHandler.cpp
new file mode 100644
index 0000000..e6345d2
--- /dev/null
+++ b/TerminalInputHandler.cpp
@@ -0,0 +1,93 @@
+#include "TerminalInputHandler.h"
+
+#include
+
+namespace {
+constexpr int CTRL(char c) { return c & 0x1F; }
+}
+
+TerminalInputHandler::TerminalInputHandler() = default;
+
+TerminalInputHandler::~TerminalInputHandler() = default;
+
+static bool map_key_to_command(int ch, bool &k_prefix, MappedInput &out)
+{
+ // Handle special keys from ncurses
+ switch (ch) {
+ case KEY_LEFT: out = {true, CommandId::MoveLeft, "", 0}; return true;
+ case KEY_RIGHT: out = {true, CommandId::MoveRight, "", 0}; return true;
+ case KEY_UP: out = {true, CommandId::MoveUp, "", 0}; return true;
+ case KEY_DOWN: out = {true, CommandId::MoveDown, "", 0}; return true;
+ case KEY_HOME: out = {true, CommandId::MoveHome, "", 0}; return true;
+ case KEY_END: out = {true, CommandId::MoveEnd, "", 0}; return true;
+ case KEY_DC: out = {true, CommandId::DeleteChar,"", 0}; return true;
+ case KEY_RESIZE: out = {true, CommandId::Refresh, "", 0}; return true;
+ default: break;
+ }
+
+ // ESC as cancel of prefix; many terminals send meta sequences as ESC+...
+ if (ch == 27) { // ESC
+ k_prefix = false;
+ out = {true, CommandId::Refresh, "", 0};
+ return true;
+ }
+
+ // Control keys
+ if (ch == CTRL('K')) { // C-k prefix
+ k_prefix = true;
+ out = {true, CommandId::Refresh, "", 0};
+ return true;
+ }
+ if (ch == CTRL('G')) { // cancel
+ k_prefix = false;
+ out = {true, CommandId::Refresh, "", 0};
+ return true;
+ }
+ if (ch == CTRL('L')) { out = {true, CommandId::Refresh, "", 0}; return true; }
+ if (ch == CTRL('S')) { out = {true, CommandId::FindStart, "", 0}; return true; }
+
+ // Enter
+ if (ch == '\n' || ch == '\r') { k_prefix = false; out = {true, CommandId::Newline, "", 0}; return true; }
+ // Backspace in ncurses can be KEY_BACKSPACE or 127
+ if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) { k_prefix = false; out = {true, CommandId::Backspace, "", 0}; return true; }
+
+ 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;
+ }
+ if (ch == CTRL('Q')) { out = {true, CommandId::Quit, "", 0}; return true; }
+ out.hasCommand = false; // unknown chord
+ return true;
+ }
+
+ // Printable ASCII
+ if (ch >= 0x20 && ch <= 0x7E) {
+ out.hasCommand = true;
+ out.id = CommandId::InsertText;
+ out.arg.assign(1, static_cast(ch));
+ out.count = 0;
+ return true;
+ }
+
+ out.hasCommand = false;
+ return true; // consumed a key
+}
+
+bool TerminalInputHandler::decode_(MappedInput &out)
+{
+ int ch = getch();
+ if (ch == ERR) {
+ return false; // no input
+ }
+ return map_key_to_command(ch, k_prefix_, out);
+}
+
+bool TerminalInputHandler::Poll(MappedInput &out)
+{
+ out = {};
+ return decode_(out) && out.hasCommand;
+}
diff --git a/TerminalInputHandler.h b/TerminalInputHandler.h
new file mode 100644
index 0000000..c1af4d8
--- /dev/null
+++ b/TerminalInputHandler.h
@@ -0,0 +1,25 @@
+/*
+ * TerminalInputHandler - ncurses-based input handling for terminal mode
+ */
+#ifndef KTE_TERMINAL_INPUT_HANDLER_H
+#define KTE_TERMINAL_INPUT_HANDLER_H
+
+#include
+
+#include "InputHandler.h"
+
+class TerminalInputHandler : public InputHandler {
+public:
+ TerminalInputHandler();
+ ~TerminalInputHandler() override;
+
+ bool Poll(MappedInput &out) override;
+
+private:
+ bool decode_(MappedInput &out);
+
+ // ke-style prefix state
+ bool k_prefix_ = false; // true after C-k until next key or ESC
+};
+
+#endif // KTE_TERMINAL_INPUT_HANDLER_H
diff --git a/TerminalRenderer.cpp b/TerminalRenderer.cpp
new file mode 100644
index 0000000..c9524d1
--- /dev/null
+++ b/TerminalRenderer.cpp
@@ -0,0 +1,74 @@
+#include "TerminalRenderer.h"
+
+#include
+#include
+
+#include "Editor.h"
+#include "Buffer.h"
+
+TerminalRenderer::TerminalRenderer() = default;
+
+TerminalRenderer::~TerminalRenderer() = default;
+
+void TerminalRenderer::Draw(const Editor &ed)
+{
+ // Determine terminal size and keep editor dimensions in sync
+ int rows, cols;
+ getmaxyx(stdscr, rows, cols);
+
+ // Clear screen
+ erase();
+
+ const Buffer *buf = ed.CurrentBuffer();
+ int content_rows = rows - 1; // last line is status
+
+ if (buf) {
+ const auto &lines = buf->Rows();
+ std::size_t rowoffs = buf->Rowoffs();
+ std::size_t coloffs = buf->Coloffs();
+
+ for (int r = 0; r < content_rows; ++r) {
+ int y = r;
+ int x = 0;
+ move(y, x);
+ std::size_t li = rowoffs + static_cast(r);
+ if (li < lines.size()) {
+ const std::string &line = lines[li];
+ if (coloffs < line.size()) {
+ // Render a windowed slice of the line
+ std::size_t len = std::min(static_cast(cols), line.size() - coloffs);
+ addnstr(line.c_str() + static_cast(coloffs), static_cast(len));
+ }
+ }
+ clrtoeol();
+ }
+
+ // Place cursor (best-effort; tabs etc. not handled yet)
+ 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) {
+ move(cur_y, cur_x);
+ }
+ } else {
+ mvaddstr(0, 0, "[no buffer]");
+ }
+
+ // Status line (inverse)
+ move(rows - 1, 0);
+ attron(A_REVERSE);
+ char status[1024];
+ const char *fname = (buf && buf->IsFileBacked()) ? buf->Filename().c_str() : "(new)";
+ int dirty = (buf && buf->Dirty()) ? 1 : 0;
+ snprintf(status, sizeof(status), " %s%s %zux%zu %s ",
+ fname,
+ dirty ? "*" : "",
+ ed.Rows(), ed.Cols(),
+ ed.Status().c_str());
+ addnstr(status, cols);
+ clrtoeol();
+ attroff(A_REVERSE);
+
+ refresh();
+}
diff --git a/TerminalRenderer.h b/TerminalRenderer.h
new file mode 100644
index 0000000..42247b6
--- /dev/null
+++ b/TerminalRenderer.h
@@ -0,0 +1,17 @@
+/*
+ * TerminalRenderer - ncurses-based renderer for terminal mode
+ */
+#ifndef KTE_TERMINAL_RENDERER_H
+#define KTE_TERMINAL_RENDERER_H
+
+#include "Renderer.h"
+
+class TerminalRenderer : public Renderer {
+public:
+ TerminalRenderer();
+ ~TerminalRenderer() override;
+
+ void Draw(const Editor &ed) override;
+};
+
+#endif // KTE_TERMINAL_RENDERER_H
diff --git a/cmake/packaging.cmake b/cmake/packaging.cmake
index 0ee61a3..2525a46 100644
--- a/cmake/packaging.cmake
+++ b/cmake/packaging.cmake
@@ -28,12 +28,12 @@ set(CPACK_DEB_COMPONENT_INSTALL ON)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "K. Isom")
set(CPACK_PACKAGE_nox_DESCRIPTION_SUMMARY "kyle's editor")
set(CPACK_PACKAGE_nox_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION})
-set(CPACK_PACKAGE_nox_PACKAGE_NAME "kge")
+set(CPACK_PACKAGE_nox_PACKAGE_NAME "kte")
set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke")
if(BUILD_GUI)
- set(CPACK_PACKAGE_gui_PACKAGE_NAME "kge")
- set(CPACK_DEBIAN_gui_PACKAGE_NAME "kge")
+ set(CPACK_PACKAGE_gui_PACKAGE_NAME "kte")
+ set(CPACK_DEBIAN_gui_PACKAGE_NAME "kte")
set(CPACK_PACKAGE_gui_DESCRIPTION_SUMMARY " graphical front-end for kyle's editor")
set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
endif()
diff --git a/main.cpp b/main.cpp
index 56fd243..4789b5d 100644
--- a/main.cpp
+++ b/main.cpp
@@ -1,26 +1,141 @@
#include
+#include
+#include
+#include
-#include "Buffer.h"
+#include "Editor.h"
+#include "Command.h"
+#include "Frontend.h"
+#include "TerminalFrontend.h"
+#if defined(KTE_BUILD_GUI)
+#include "GUIFrontend.h"
+#endif
-// TIP To Run code, press or click the icon in the gutter.
-int main(const int argc, const char *argv[])
+
+#ifndef KGE_VERSION
+# define KTE_VERSION_STR "dev"
+#else
+# define KTE_STR_HELPER(x) #x
+# define KTE_STR(x) KTE_STR_HELPER(x)
+# define KTE_VERSION_STR KTE_STR(KGE_VERSION)
+#endif
+
+static void PrintUsage(const char* prog)
{
- const auto buffers = new std::vector();
+ std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
+ << "Options:\n"
+ << " -g, --gui Use GUI frontend (if built)\n"
+ << " -t, --term Use terminal (ncurses) frontend [default]\n"
+ << " -h, --help Show this help and exit\n"
+ << " -V, --version Show version and exit\n";
+}
- for (int i = 1; i < argc; i++) {
- auto buffer = Buffer(argv[i]);
- if (i % 2 == 0) {
- buffer.SetDirty(true);
- }
+int
+main(int argc, const char *argv[])
+{
+ Editor editor;
- buffers->emplace_back(buffer);
- }
+ // CLI parsing using getopt_long
+ bool req_gui = false;
+ bool req_term = false;
+ bool show_help = false;
+ bool show_version = false;
- std::cout << buffers->size() << " files loaded.\n";
- for (const auto &buffer : *buffers) {
- std::cout << buffer.AsString() << "\n";
- }
+ static struct option long_opts[] = {
+ {"gui", no_argument, nullptr, 'g'},
+ {"term", no_argument, nullptr, 't'},
+ {"help", no_argument, nullptr, 'h'},
+ {"version", no_argument, nullptr, 'V'},
+ {nullptr, 0, nullptr, 0 }
+ };
- delete buffers;
- return 0;
-}
\ No newline at end of file
+ int opt;
+ int long_index = 0;
+ while ((opt = getopt_long(argc, const_cast(argv), "gthV", long_opts, &long_index)) != -1) {
+ switch (opt) {
+ case 'g': req_gui = true; break;
+ case 't': req_term = true; break;
+ case 'h': show_help = true; break;
+ case 'V': show_version = true; break;
+ case '?':
+ default:
+ PrintUsage(argv[0]);
+ return 2;
+ }
+ }
+
+ if (show_help) {
+ PrintUsage(argv[0]);
+ return 0;
+ }
+ if (show_version) {
+ std::cout << "kte " << KTE_VERSION_STR << "\n";
+ return 0;
+ }
+
+#if !defined(KTE_BUILD_GUI)
+ (void)req_term; // suppress unused warning when GUI is not compiled in
+#endif
+
+ // Determine frontend
+#if !defined(KTE_BUILD_GUI)
+ if (req_gui) {
+ std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." << std::endl;
+ return 2;
+ }
+#else
+ bool use_gui = false;
+ if (req_gui) {
+ use_gui = true;
+ } else if (req_term) {
+ use_gui = false;
+ } else {
+ // Default to terminal
+ use_gui = false;
+ }
+#endif
+
+ // Open files passed on the CLI; if none, create an empty buffer
+ if (optind < argc) {
+ for (int i = optind; i < argc; ++i) {
+ std::string err;
+ const std::string path = argv[i];
+ if (!editor.OpenFile(path, err)) {
+ editor.SetStatus("open: " + err);
+ std::cerr << "kte: " << err << "\n";
+ }
+ }
+ } else {
+ // Create a single empty buffer
+ editor.AddBuffer(Buffer());
+ editor.SetStatus("new: empty buffer");
+ }
+
+ // Install built-in commands
+ InstallDefaultCommands();
+
+ // Select frontend
+ std::unique_ptr fe;
+#if defined(KTE_BUILD_GUI)
+ if (use_gui) {
+ fe.reset(new GUIFrontend());
+ } else
+#endif
+ {
+ fe.reset(new TerminalFrontend());
+ }
+
+ if (!fe->Init(editor)) {
+ std::cerr << "kte: failed to initialize frontend" << std::endl;
+ return 1;
+ }
+
+ bool running = true;
+ while (running) {
+ fe->Step(editor, running);
+ }
+
+ fe->Shutdown();
+
+ return 0;
+}