From 69e7959fa4851fbb5235f7d36ea4eb954285dd9d Mon Sep 17 00:00:00 2001 From: Kyle Isom Date: Sat, 29 Nov 2025 17:54:55 -0800 Subject: [PATCH] Basic shell working. --- .idea/workspace.xml | 58 +++-- .junie/guidelines.md | 11 +- CMakeLists.txt | 33 ++- Command.cpp | 466 +++++++++++++++++++++++++++++++++++++++ Command.h | 86 ++++++++ Frontend.h | 27 +++ GUIFrontend.cpp | 121 ++++++++++ GUIFrontend.h | 32 +++ GUIInputHandler.cpp | 92 ++++++++ GUIInputHandler.h | 31 +++ GUIRenderer.cpp | 37 ++++ GUIRenderer.h | 17 ++ InputHandler.h | 28 +++ README.md | 86 +++++++- ROADMAP.md | 97 ++++++++ Renderer.h | 15 ++ TerminalFrontend.cpp | 56 +++++ TerminalFrontend.h | 27 +++ TerminalInputHandler.cpp | 93 ++++++++ TerminalInputHandler.h | 25 +++ TerminalRenderer.cpp | 74 +++++++ TerminalRenderer.h | 17 ++ cmake/packaging.cmake | 6 +- main.cpp | 151 +++++++++++-- 24 files changed, 1631 insertions(+), 55 deletions(-) create mode 100644 Command.cpp create mode 100644 Command.h create mode 100644 Frontend.h create mode 100644 GUIFrontend.cpp create mode 100644 GUIFrontend.h create mode 100644 GUIInputHandler.cpp create mode 100644 GUIInputHandler.h create mode 100644 GUIRenderer.cpp create mode 100644 GUIRenderer.h create mode 100644 InputHandler.h create mode 100644 ROADMAP.md create mode 100644 Renderer.h create mode 100644 TerminalFrontend.cpp create mode 100644 TerminalFrontend.h create mode 100644 TerminalInputHandler.cpp create mode 100644 TerminalInputHandler.h create mode 100644 TerminalRenderer.cpp create mode 100644 TerminalRenderer.h 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 @@ + - + - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + + + @@ -124,7 +142,7 @@ 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; +}