Basic shell working.

This commit is contained in:
2025-11-29 17:54:55 -08:00
parent 46c7a4e8a2
commit 69e7959fa4
24 changed files with 1631 additions and 55 deletions

58
.idea/workspace.xml generated
View File

@@ -22,31 +22,40 @@
<component name="CMakeRunConfigurationManager">
<generated>
<config projectName="kte" targetName="kte" />
<config projectName="kte" targetName="imgui" />
</generated>
</component>
<component name="CMakeSettings">
<configurations>
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" />
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G &quot;Unix Makefiles&quot; -DKTE_USE_PIECE_TABLE:BOOL=ON" />
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
<change afterPath="$PROJECT_DIR$/.junie/guidelines.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/AppendBuffer.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Buffer.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Editor.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GapBuffer.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GapBuffer.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/PieceTable.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/PieceTable.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/cmake/imgui.cmake" afterDir="false" />
<change afterPath="$PROJECT_DIR$/cmake/packaging.cmake" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ke.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/main.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Command.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Frontend.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIFrontend.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIFrontend.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIInputHandler.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIInputHandler.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIRenderer.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/GUIRenderer.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/InputHandler.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
<change afterPath="$PROJECT_DIR$/Renderer.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalFrontend.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalFrontend.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalInputHandler.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalInputHandler.h" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalRenderer.cpp" afterDir="false" />
<change afterPath="$PROJECT_DIR$/TerminalRenderer.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.junie/guidelines.md" beforeDir="false" afterPath="$PROJECT_DIR$/.junie/guidelines.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/README.md" beforeDir="false" afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/cmake/packaging.cmake" beforeDir="false" afterPath="$PROJECT_DIR$/cmake/packaging.cmake" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.cpp" beforeDir="false" afterPath="$PROJECT_DIR$/main.cpp" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -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"
}
}]]></component>
<component name="RunManager">
<component name="RunManager" selected="CMake Application.kte">
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<list>
<item itemvalue="CMake Application.imgui" />
<item itemvalue="CMake Application.kte" />
</list>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
@@ -124,7 +142,7 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1764457173148</updated>
<workItem from="1764457174208" duration="5713000" />
<workItem from="1764457174208" duration="9070000" />
</task>
<servers />
</component>

View File

@@ -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`)

View File

@@ -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"
"$<$<CONFIG:DEBUG>:-g>"
"$<$<CONFIG:RELEASE>:-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 ()

466
Command.cpp Normal file
View File

@@ -0,0 +1,466 @@
#include "Command.h"
#include <algorithm>
#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<std::ptrdiff_t>(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<std::ptrdiff_t>(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<std::ptrdiff_t>(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<int>(y)) repeat = static_cast<int>(y);
y -= static_cast<std::size_t>(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<int>(max_down)) repeat = static_cast<int>(max_down);
y += static_cast<std::size_t>(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<Command> &
CommandRegistry::storage_()
{
static std::vector<Command> 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<Command> &
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;
}

86
Command.h Normal file
View File

@@ -0,0 +1,86 @@
/*
* Command.h - command model and registry for editor actions
*/
#ifndef KTE_COMMAND_H
#define KTE_COMMAND_H
#include <functional>
#include <string>
#include <unordered_map>
#include <vector>
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<bool(CommandContext &)>; // 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<Command> &All();
private:
static std::vector<Command> &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

27
Frontend.h Normal file
View File

@@ -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 <memory>
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

121
GUIFrontend.cpp Normal file
View File

@@ -0,0 +1,121 @@
#include "GUIFrontend.h"
#include <SDL.h>
#include <SDL_opengl.h>
#include <imgui.h>
#include <backends/imgui_impl_sdl2.h>
#include <backends/imgui_impl_opengl3.h>
#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<std::size_t>(height_), static_cast<std::size_t>(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<std::size_t>(height_), static_cast<std::size_t>(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();
}

32
GUIFrontend.h Normal file
View File

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

92
GUIInputHandler.cpp Normal file
View File

@@ -0,0 +1,92 @@
#include "GUIInputHandler.h"
#include <SDL.h>
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<std::mutex> lk(mu_);
q_.push(mi);
}
return produced;
}
bool GUIInputHandler::Poll(MappedInput &out)
{
std::lock_guard<std::mutex> lk(mu_);
if (q_.empty()) return false;
out = q_.front();
q_.pop();
return true;
}

31
GUIInputHandler.h Normal file
View File

@@ -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 <queue>
#include <mutex>
#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<MappedInput> q_;
bool k_prefix_ = false;
};
#endif // KTE_GUI_INPUT_HANDLER_H

37
GUIRenderer.cpp Normal file
View File

@@ -0,0 +1,37 @@
#include "GUIRenderer.h"
#include "Editor.h"
#include "Buffer.h"
#include <imgui.h>
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();
}

17
GUIRenderer.h Normal file
View File

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

28
InputHandler.h Normal file
View File

@@ -0,0 +1,28 @@
/*
* InputHandler.h - input abstraction and mapping to commands
*/
#ifndef KTE_INPUT_HANDLER_H
#define KTE_INPUT_HANDLER_H
#include <string>
#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

View File

@@ -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 ImGuibased 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.

97
ROADMAP.md Normal file
View File

@@ -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`, nonblocking 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 (ncursesbased).
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 (UTF8, 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 saveas/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 (kestyle 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 roundtrip, 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 saveas.

15
Renderer.h Normal file
View File

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

56
TerminalFrontend.cpp Normal file
View File

@@ -0,0 +1,56 @@
#include "TerminalFrontend.h"
#include <unistd.h>
#include <ncurses.h>
#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<std::size_t>(r), static_cast<std::size_t>(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<std::size_t>(r), static_cast<std::size_t>(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();
}

27
TerminalFrontend.h Normal file
View File

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

93
TerminalInputHandler.cpp Normal file
View File

@@ -0,0 +1,93 @@
#include "TerminalInputHandler.h"
#include <ncurses.h>
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<char>(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;
}

25
TerminalInputHandler.h Normal file
View File

@@ -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 <cstdint>
#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

74
TerminalRenderer.cpp Normal file
View File

@@ -0,0 +1,74 @@
#include "TerminalRenderer.h"
#include <ncurses.h>
#include <cstdio>
#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<std::size_t>(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<std::size_t>(static_cast<std::size_t>(cols), line.size() - coloffs);
addnstr(line.c_str() + static_cast<long>(coloffs), static_cast<int>(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<int>(cy - buf->Rowoffs());
int cur_x = static_cast<int>(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();
}

17
TerminalRenderer.h Normal file
View File

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

View File

@@ -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()

157
main.cpp
View File

@@ -1,26 +1,141 @@
#include <iostream>
#include <string>
#include <unistd.h>
#include <getopt.h>
#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 <b>Run</b> code, press <shortcut actionId="Run"/> or click the <icon src="AllIcons.Actions.Execute"/> 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<Buffer>();
for (int i = 1; i < argc; i++) {
auto buffer = Buffer(argv[i]);
if (i % 2 == 0) {
buffer.SetDirty(true);
}
buffers->emplace_back(buffer);
}
std::cout << buffers->size() << " files loaded.\n";
for (const auto &buffer : *buffers) {
std::cout << buffer.AsString() << "\n";
}
delete buffers;
return 0;
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";
}
int
main(int argc, const char *argv[])
{
Editor editor;
// CLI parsing using getopt_long
bool req_gui = false;
bool req_term = false;
bool show_help = false;
bool show_version = false;
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 }
};
int opt;
int long_index = 0;
while ((opt = getopt_long(argc, const_cast<char* const*>(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<Frontend> 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;
}