Basic shell working.
This commit is contained in:
58
.idea/workspace.xml
generated
58
.idea/workspace.xml
generated
@@ -22,31 +22,40 @@
|
|||||||
<component name="CMakeRunConfigurationManager">
|
<component name="CMakeRunConfigurationManager">
|
||||||
<generated>
|
<generated>
|
||||||
<config projectName="kte" targetName="kte" />
|
<config projectName="kte" targetName="kte" />
|
||||||
|
<config projectName="kte" targetName="imgui" />
|
||||||
</generated>
|
</generated>
|
||||||
</component>
|
</component>
|
||||||
<component name="CMakeSettings">
|
<component name="CMakeSettings">
|
||||||
<configurations>
|
<configurations>
|
||||||
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" />
|
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G "Unix Makefiles" -DKTE_USE_PIECE_TABLE:BOOL=ON" />
|
||||||
</configurations>
|
</configurations>
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="">
|
||||||
<change afterPath="$PROJECT_DIR$/.junie/guidelines.md" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/Command.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/AppendBuffer.h" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/Buffer.cpp" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/Frontend.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIFrontend.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIFrontend.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/Editor.cpp" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIInputHandler.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIInputHandler.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/GapBuffer.cpp" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIRenderer.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/GapBuffer.h" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/GUIRenderer.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/PieceTable.cpp" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/InputHandler.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/PieceTable.h" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/README.md" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/Renderer.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/cmake/imgui.cmake" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/TerminalFrontend.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/cmake/packaging.cmake" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/TerminalFrontend.h" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/ke.md" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/TerminalInputHandler.cpp" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/main.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>
|
</list>
|
||||||
<option name="SHOW_DIALOG" value="false" />
|
<option name="SHOW_DIALOG" value="false" />
|
||||||
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
<option name="HIGHLIGHT_CONFLICTS" value="true" />
|
||||||
@@ -100,22 +109,31 @@
|
|||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
"nodejs_package_manager_path": "npm",
|
"nodejs_package_manager_path": "npm",
|
||||||
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
|
"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",
|
"to.speed.mode.migration.done": "true",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></component>
|
}]]></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">
|
<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">
|
<method v="2">
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<configuration name="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">
|
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
|
||||||
<method v="2">
|
<method v="2">
|
||||||
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
|
||||||
</method>
|
</method>
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="CMake Application.imgui" />
|
||||||
|
<item itemvalue="CMake Application.kte" />
|
||||||
|
</list>
|
||||||
</component>
|
</component>
|
||||||
<component name="TaskManager">
|
<component name="TaskManager">
|
||||||
<task active="true" id="Default" summary="Default task">
|
<task active="true" id="Default" summary="Default task">
|
||||||
@@ -124,7 +142,7 @@
|
|||||||
<option name="number" value="Default" />
|
<option name="number" value="Default" />
|
||||||
<option name="presentableId" value="Default" />
|
<option name="presentableId" value="Default" />
|
||||||
<updated>1764457173148</updated>
|
<updated>1764457173148</updated>
|
||||||
<workItem from="1764457174208" duration="5713000" />
|
<workItem from="1764457174208" duration="9070000" />
|
||||||
</task>
|
</task>
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ development practices for kte.
|
|||||||
## Goals
|
## Goals
|
||||||
|
|
||||||
- Keep the core small, fast, and understandable.
|
- 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.
|
- Preserve familiar keybindings from ke while modernizing the internals.
|
||||||
- Favor simple data structures (e.g., gap buffer) and incremental evolution.
|
- Favor simple data structures (e.g., gap buffer) and incremental evolution.
|
||||||
|
|
||||||
@@ -22,7 +22,10 @@ development practices for kte.
|
|||||||
|
|
||||||
## Build and Run
|
## 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):
|
- Configure and build (example):
|
||||||
- `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
|
- `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`).
|
- GapBuffer: editable in-memory text representation (`GapBuffer.h/.cpp`).
|
||||||
- PieceTable: experimental/alternative representation (`PieceTable.h/.cpp`).
|
- PieceTable: experimental/alternative representation (`PieceTable.h/.cpp`).
|
||||||
- InputHandler: interface for handling text input (`InputHandler.h/`), along
|
- 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
|
- 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`).
|
- Editor: top-level editor state (`Editor.h/.cpp`).
|
||||||
- Command: command model (`Command.h/.cpp`).
|
- Command: command model (`Command.h/.cpp`).
|
||||||
- General purpose editor functionality (`Editing.h/.cpp`)
|
- General purpose editor functionality (`Editing.h/.cpp`)
|
||||||
|
|||||||
@@ -2,8 +2,11 @@ cmake_minimum_required(VERSION 3.15)
|
|||||||
project(kte)
|
project(kte)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
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)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" OFF)
|
||||||
|
|
||||||
|
|
||||||
@@ -20,7 +23,6 @@ else ()
|
|||||||
"-Wall"
|
"-Wall"
|
||||||
"-Wextra"
|
"-Wextra"
|
||||||
"-Werror"
|
"-Werror"
|
||||||
"-static"
|
|
||||||
"$<$<CONFIG:DEBUG>:-g>"
|
"$<$<CONFIG:DEBUG>:-g>"
|
||||||
"$<$<CONFIG:RELEASE>:-O2>")
|
"$<$<CONFIG:RELEASE>:-O2>")
|
||||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||||
@@ -38,11 +40,19 @@ if (${BUILD_GUI})
|
|||||||
include(cmake/imgui.cmake)
|
include(cmake/imgui.cmake)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# NCurses for terminal mode
|
||||||
|
find_package(Curses REQUIRED)
|
||||||
|
include_directories(${CURSES_INCLUDE_DIR})
|
||||||
|
|
||||||
set(SOURCES
|
set(SOURCES
|
||||||
GapBuffer.cpp
|
GapBuffer.cpp
|
||||||
PieceTable.cpp
|
PieceTable.cpp
|
||||||
Buffer.cpp
|
Buffer.cpp
|
||||||
Editor.cpp
|
Editor.cpp
|
||||||
|
Command.cpp
|
||||||
|
TerminalInputHandler.cpp
|
||||||
|
TerminalRenderer.cpp
|
||||||
|
TerminalFrontend.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
set(HEADERS
|
set(HEADERS
|
||||||
@@ -51,6 +61,13 @@ set(HEADERS
|
|||||||
Buffer.h
|
Buffer.h
|
||||||
Editor.h
|
Editor.h
|
||||||
AppendBuffer.h
|
AppendBuffer.h
|
||||||
|
Command.h
|
||||||
|
InputHandler.h
|
||||||
|
TerminalInputHandler.h
|
||||||
|
Renderer.h
|
||||||
|
TerminalRenderer.h
|
||||||
|
Frontend.h
|
||||||
|
TerminalFrontend.h
|
||||||
)
|
)
|
||||||
|
|
||||||
add_executable(kte
|
add_executable(kte
|
||||||
@@ -63,6 +80,16 @@ if (KTE_USE_PIECE_TABLE)
|
|||||||
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||||
|
|
||||||
if (${BUILD_GUI})
|
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 ()
|
endif ()
|
||||||
|
|||||||
466
Command.cpp
Normal file
466
Command.cpp
Normal 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
86
Command.h
Normal 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
27
Frontend.h
Normal 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
121
GUIFrontend.cpp
Normal 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
32
GUIFrontend.h
Normal 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
92
GUIInputHandler.cpp
Normal 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
31
GUIInputHandler.h
Normal 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
37
GUIRenderer.cpp
Normal 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
17
GUIRenderer.h
Normal 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
28
InputHandler.h
Normal 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
|
||||||
84
README.md
84
README.md
@@ -75,7 +75,8 @@ Interfaces
|
|||||||
----------
|
----------
|
||||||
|
|
||||||
- CLI: the primary interface. `kte [files]` starts in the terminal,
|
- 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
|
- GUI: an optional ImGui‑based frontend that embeds the same editor
|
||||||
core.
|
core.
|
||||||
|
|
||||||
@@ -130,7 +131,41 @@ See `ke.md` for the canonical ke reference retained for now.
|
|||||||
|
|
||||||
Build and Run
|
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:
|
Example build:
|
||||||
|
|
||||||
@@ -145,13 +180,50 @@ Run:
|
|||||||
./cmake-build-debug/kte [files]
|
./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
|
Status
|
||||||
------
|
------
|
||||||
|
|
||||||
- The project is under active evolution toward the above architecture
|
- The project is under active evolution toward the above architecture
|
||||||
and UX. The terminal interface is the leading target; GUI work will
|
and UX. The terminal interface now uses ncurses for input and
|
||||||
follow as a thin, optional layer. ke compatibility remains a primary
|
rendering. GUI work will follow as a thin, optional layer. ke
|
||||||
constraint while internals modernize.
|
compatibility remains a primary constraint while internals modernize.
|
||||||
|
|
||||||
Roadmap (high level)
|
Roadmap (high level)
|
||||||
--------------------
|
--------------------
|
||||||
@@ -161,7 +233,7 @@ Roadmap (high level)
|
|||||||
2. Introduce structured undo/redo and search/replace with
|
2. Introduce structured undo/redo and search/replace with
|
||||||
highlighting.
|
highlighting.
|
||||||
3. Stabilize terminal renderer and input handling across common
|
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
|
4. Add piece table as an alternative backend with runtime selection
|
||||||
per buffer.
|
per buffer.
|
||||||
5. Optional GUI frontend using ImGui; shared command palette.
|
5. Optional GUI frontend using ImGui; shared command palette.
|
||||||
|
|||||||
97
ROADMAP.md
Normal file
97
ROADMAP.md
Normal 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`, 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.
|
||||||
15
Renderer.h
Normal file
15
Renderer.h
Normal 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
56
TerminalFrontend.cpp
Normal 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
27
TerminalFrontend.h
Normal 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
93
TerminalInputHandler.cpp
Normal 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
25
TerminalInputHandler.h
Normal 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
74
TerminalRenderer.cpp
Normal 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
17
TerminalRenderer.h
Normal 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
|
||||||
@@ -28,12 +28,12 @@ set(CPACK_DEB_COMPONENT_INSTALL ON)
|
|||||||
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "K. Isom")
|
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "K. Isom")
|
||||||
set(CPACK_PACKAGE_nox_DESCRIPTION_SUMMARY "kyle's editor")
|
set(CPACK_PACKAGE_nox_DESCRIPTION_SUMMARY "kyle's editor")
|
||||||
set(CPACK_PACKAGE_nox_DESCRIPTION ${CPACK_PACKAGE_DESCRIPTION})
|
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")
|
set(CPACK_DEBIAN_nox_PACKAGE_NAME "ke")
|
||||||
|
|
||||||
if(BUILD_GUI)
|
if(BUILD_GUI)
|
||||||
set(CPACK_PACKAGE_gui_PACKAGE_NAME "kge")
|
set(CPACK_PACKAGE_gui_PACKAGE_NAME "kte")
|
||||||
set(CPACK_DEBIAN_gui_PACKAGE_NAME "kge")
|
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_SUMMARY " graphical front-end for kyle's editor")
|
||||||
set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
|
set(CPACK_PACKAGE_gui_DESCRIPTION "graphical front-end for ${CPACK_PACKAGE_DESCRIPTION} ")
|
||||||
endif()
|
endif()
|
||||||
|
|||||||
157
main.cpp
157
main.cpp
@@ -1,26 +1,141 @@
|
|||||||
#include <iostream>
|
#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>();
|
std::cerr << "Usage: " << prog << " [OPTIONS] [files]\n"
|
||||||
|
<< "Options:\n"
|
||||||
for (int i = 1; i < argc; i++) {
|
<< " -g, --gui Use GUI frontend (if built)\n"
|
||||||
auto buffer = Buffer(argv[i]);
|
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
||||||
if (i % 2 == 0) {
|
<< " -h, --help Show this help and exit\n"
|
||||||
buffer.SetDirty(true);
|
<< " -V, --version Show version and exit\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
buffers->emplace_back(buffer);
|
int
|
||||||
}
|
main(int argc, const char *argv[])
|
||||||
|
{
|
||||||
std::cout << buffers->size() << " files loaded.\n";
|
Editor editor;
|
||||||
for (const auto &buffer : *buffers) {
|
|
||||||
std::cout << buffer.AsString() << "\n";
|
// CLI parsing using getopt_long
|
||||||
}
|
bool req_gui = false;
|
||||||
|
bool req_term = false;
|
||||||
delete buffers;
|
bool show_help = false;
|
||||||
return 0;
|
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;
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user