18 Commits

Author SHA1 Message Date
0cb7d36f2a bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-01 11:22:47 -08:00
09a6df0c33 Add regex search, search/replace, and buffer read-only mode functionality with help text 2025-12-01 02:54:40 -08:00
69457c424c add regex and search/replace functionality to editor 2025-11-30 23:33:17 -08:00
24c8040d8a trash flake-gui 2025-11-30 22:02:43 -08:00
e869249a7c thaaaaanks jeremy 2025-11-30 22:02:23 -08:00
35e957b326 Fix void crash in kge.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-11-30 21:51:03 -08:00
e7eb35626c NixOS build 2025-11-30 21:07:41 -08:00
f9128a336d better support for smaller screens
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
editor_prompt should replace the status line when active
2025-11-30 20:25:53 -08:00
f8d0e9213f bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
Also add some TODOs.
2025-11-30 19:57:05 -08:00
68286ecb7c Add icon to share/icons for Linux. 2025-11-30 19:55:03 -08:00
44807d0f40 clang-tidy was zealous on macOS. 2025-11-30 19:47:43 -08:00
41f37478c1 bump cmake version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-11-30 19:09:35 -08:00
d582979eb3 release maybe 2025-11-30 19:08:34 -08:00
2b194c7910 Actually add the screenshot. 2025-11-30 18:55:55 -08:00
6498213378 updating readme and roadmap 2025-11-30 18:54:24 -08:00
1a37a92534 add screenshot to README 2025-11-30 18:45:11 -08:00
fb5976f123 Add buffer position display and documentation improvements.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
- Display buffer position prefix "[x/N]" in GUI and terminal renderers.
- Improve `kte` and `kge` man pages with frontend usage details and project homepage.
- Update README with GUI invocation instructions.
- Bump version to 1.0.0.
2025-11-30 18:40:44 -08:00
e4cd4877cc Introduce file picker and GUI configuration with enhancements.
- Add visual file picker for GUI with toggle support.
- Introduce `GUIConfig` class for loading GUI settings from configuration file.
- Refactor window initialization to support dynamic sizing based on configuration.
- Add macOS-specific handling for fullscreen behavior.
- Improve header inclusion order and minor code cleanup.
2025-11-30 18:35:12 -08:00
67 changed files with 2350 additions and 876 deletions

View File

@@ -9,6 +9,9 @@ on:
permissions:
contents: write
env:
BUILD_TYPE: Release
jobs:
homebrew:
name: Bump Homebrew formula
@@ -35,3 +38,114 @@ jobs:
Created by https://github.com/mislav/bump-homebrew-formula-action
env:
COMMITTER_TOKEN: ${{ secrets.GH_CPAT }}
linux-build:
name: Build Linux ${{ matrix.arch }}
runs-on: ${{ matrix.runner }}
strategy:
fail-fast: false
matrix:
include:
- arch: amd64
runner: ubuntu-latest
- arch: arm64
runner: ubuntu-24.04-arm64
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install deps
run: |
sudo apt-get update
sudo apt-get install -y build-essential cmake pkg-config \
libncurses5-dev libncursesw5-dev \
libsdl2-dev libfreetype6-dev mesa-common-dev
- name: Configure (CMake, GUI ON)
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DBUILD_GUI=ON
- name: Build
run: |
cmake --build build --config ${BUILD_TYPE} -j
- name: Prepare dist
run: |
mkdir -p dist/linux-${{ matrix.arch }}
cp build/kte dist/linux-${{ matrix.arch }}/
cp build/kge dist/linux-${{ matrix.arch }}/
strip dist/linux-${{ matrix.arch }}/kte || true
strip dist/linux-${{ matrix.arch }}/kge || true
- name: Upload artifact (linux-${{ matrix.arch }})
uses: actions/upload-artifact@v4
with:
name: linux-${{ matrix.arch }}
path: dist/linux-${{ matrix.arch }}/*
macos-build:
name: Build macOS arm64 (.app)
runs-on: macos-14
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install deps (brew)
run: |
brew update
brew install cmake ncurses sdl2 freetype
- name: Configure (CMake, GUI ON, arm64)
run: |
cmake -S . -B build -DCMAKE_BUILD_TYPE=${BUILD_TYPE} -DBUILD_GUI=ON -DCMAKE_OSX_ARCHITECTURES=arm64
- name: Build
run: |
cmake --build build --config ${BUILD_TYPE} -j
- name: Zip kge.app
run: |
mkdir -p dist/macos-arm64
cd build
ditto -c -k --sequesterRsrc --keepParent kge.app ../dist/macos-arm64/kge.app.zip
- name: Upload artifact (macos-arm64)
uses: actions/upload-artifact@v4
with:
name: macos-arm64
path: dist/macos-arm64/kge.app.zip
release:
name: Create GitHub Release
needs: [ linux-build, macos-build ]
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Download artifacts
uses: actions/download-artifact@v4
with:
path: dist
- name: Reshape artifact layout
run: |
ls -R dist
# Actions download-artifact places each named artifact in a subfolder
# Move into the expected dist structure for GoReleaser
mkdir -p dist/linux-amd64 dist/linux-arm64 dist/macos-arm64
if [ -d dist/linux-amd64/linux-amd64 ]; then mv dist/linux-amd64/linux-amd64/* dist/linux-amd64/; fi
if [ -d dist/linux-arm64/linux-arm64 ]; then mv dist/linux-arm64/linux-arm64/* dist/linux-arm64/; fi
if [ -d dist/macos-arm64/macos-arm64 ]; then mv dist/macos-arm64/macos-arm64/* dist/macos-arm64/; fi
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.22.x'
- name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6
with:
version: latest
args: release --clean --config .goreleaser.yaml
env:
GITHUB_TOKEN: ${{ secrets.GH_CPAT }}

2
.gitignore vendored
View File

@@ -1,3 +1,5 @@
!.idea
cmake-build*
build
/imgui.ini
result

69
.goreleaser.yaml Normal file
View File

@@ -0,0 +1,69 @@
# GoReleaser configuration for kte/kge (C++ project)
# We use GoReleaser only for releasing: changelog, checksums, and uploading
# prebuilt artifacts that are produced by the CI workflow.
version: 2
project_name: kte
before:
hooks:
# No build here; artifacts are produced by the CI jobs and placed into dist/
- echo "GoReleaser: using prebuilt artifacts from dist/"
builds:
# No Go builds; this is a C++ project.
- id: noop
skip: true
checksum:
name_template: "checksums.txt"
algorithm: sha256
release:
# Rely on GITHUB_TOKEN from the workflow.
draft: false
prerelease: auto
mode: replace
footer: |
Built with CMake. See README for platform dependencies.
extra_files:
# Linux binaries (amd64, arm64)
- glob: dist/linux-amd64/kte
- glob: dist/linux-amd64/kge
- glob: dist/linux-arm64/kte
- glob: dist/linux-arm64/kge
# macOS Apple Silicon app bundle (zipped)
- glob: dist/macos-arm64/kge.app.zip
changelog:
sort: asc
use: github
filters:
exclude:
- '^docs: '
- '^chore: '
- '^ci: '
announce:
skip: true
signs:
# No signing by default.
- artifacts: none
archives:
# We are uploading raw binaries / zip created by CI, so no archives here.
- id: none
formats: [binary]
builds: [noop]
blobs: []
brews: []
snapcrafts: []
nfpm: []
publishers: []

96
.idea/workspace.xml generated
View File

@@ -33,12 +33,20 @@
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines.">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="add regex and search/replace functionality to editor">
<change afterPath="$PROJECT_DIR$/HelpText.cc" afterDir="false" />
<change afterPath="$PROJECT_DIR$/HelpText.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/main.cc" beforeDir="false" afterPath="$PROJECT_DIR$/main.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/ROADMAP.md" beforeDir="false" afterPath="$PROJECT_DIR$/ROADMAP.md" afterDir="false" />
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -53,6 +61,12 @@
<option name="myRunOnSave" value="true" />
</component>
<component name="Git.Settings">
<option name="PUSH_TAGS">
<GitPushTagMode>
<option name="argument" value="--tags" />
<option name="title" value="All" />
</GitPushTagMode>
</option>
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
<option name="UPDATE_TYPE" value="REBASE" />
</component>
@@ -60,18 +74,13 @@
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///dummy.cpp" root0="SKIP_HIGHLIGHTING" />
</component>
<component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" />
</component>
<component name="ProjectApplicationVersion">
<option name="ide" value="CLion" />
<option name="majorVersion" value="2025" />
@@ -134,12 +143,17 @@
</key>
</component>
<component name="RunManager" selected="CMake Application.kge">
<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="kge" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
@@ -170,7 +184,7 @@
<workItem from="1764539556448" duration="156000" />
<workItem from="1764539725338" duration="1075000" />
<workItem from="1764542392763" duration="3512000" />
<workItem from="1764548345516" duration="3453000" />
<workItem from="1764548345516" duration="28341000" />
</task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" />
@@ -252,7 +266,55 @@
<option name="project" value="LOCAL" />
<updated>1764550164829</updated>
</task>
<option name="localTasksCounter" value="11" />
<task id="LOCAL-00011" summary="Add horizontal scrolling support and refactor mouse click handling in GUI.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- Replace redundant variable declarations in `Buffer` for cleaner code.">
<option name="closed" value="true" />
<created>1764551986561</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1764551986561</updated>
</task>
<task id="LOCAL-00012" summary="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup.">
<option name="closed" value="true" />
<created>1764556512864</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1764556512864</updated>
</task>
<task id="LOCAL-00013" summary="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0.">
<option name="closed" value="true" />
<created>1764556854788</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1764556854788</updated>
</task>
<task id="LOCAL-00014" summary="Actually add the screenshot.">
<option name="closed" value="true" />
<created>1764557759844</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1764557759844</updated>
</task>
<task id="LOCAL-00015" summary="Fix void crash in kge.">
<option name="closed" value="true" />
<created>1764568264996</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1764568264996</updated>
</task>
<task id="LOCAL-00016" summary="add regex and search/replace functionality to editor">
<option name="closed" value="true" />
<created>1764574397967</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1764574397967</updated>
</task>
<option name="localTasksCounter" value="17" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -276,7 +338,13 @@
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." />
<MESSAGE value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." />
<MESSAGE value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
<option name="LAST_COMMIT_MESSAGE" value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- Replace redundant variable declarations in `Buffer` for cleaner code." />
<MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." />
<MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." />
<MESSAGE value="Actually add the screenshot." />
<MESSAGE value="Fix void crash in kge." />
<MESSAGE value="add regex and search/replace functionality to editor" />
<option name="LAST_COMMIT_MESSAGE" value="add regex and search/replace functionality to editor" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />

View File

@@ -1,12 +1,12 @@
#include "Buffer.h"
#include "UndoSystem.h"
#include "UndoTree.h"
#include <fstream>
#include <sstream>
#include <filesystem>
#include <cstdlib>
#include "Buffer.h"
#include "UndoSystem.h"
#include "UndoTree.h"
Buffer::Buffer()
{
@@ -36,6 +36,7 @@ Buffer::Buffer(const Buffer &other)
filename_ = other.filename_;
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -60,6 +61,7 @@ Buffer::operator=(const Buffer &other)
filename_ = other.filename_;
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -82,6 +84,7 @@ Buffer::Buffer(Buffer &&other) noexcept
filename_(std::move(other.filename_)),
is_file_backed_(other.is_file_backed_),
dirty_(other.dirty_),
read_only_(other.read_only_),
mark_set_(other.mark_set_),
mark_curx_(other.mark_curx_),
mark_cury_(other.mark_cury_),
@@ -112,6 +115,7 @@ Buffer::operator=(Buffer &&other) noexcept
filename_ = std::move(other.filename_);
is_file_backed_ = other.is_file_backed_;
dirty_ = other.dirty_;
read_only_ = other.read_only_;
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
@@ -366,7 +370,7 @@ Buffer::insert_text(int row, int col, std::string_view text)
// Split line at x
std::string tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
y += 1;
x = 0;
remain.erase(0, pos + 1);
@@ -427,7 +431,7 @@ Buffer::split_line(int row, const int col)
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
const auto tail = rows_[y].substr(x);
rows_[y].erase(x);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1), Line(tail));
}
@@ -455,7 +459,7 @@ Buffer::insert_row(int row, const std::string_view text)
row = 0;
if (static_cast<std::size_t>(row) > rows_.size())
row = static_cast<int>(rows_.size());
rows_.insert(rows_.begin() + row, std::string(text));
rows_.insert(rows_.begin() + row, Line(std::string(text)));
}

View File

@@ -5,6 +5,7 @@
#define KTE_BUFFER_H
#include <cstddef>
#include <memory>
#include <string>
#include <vector>
#include <string_view>
@@ -12,6 +13,7 @@
#include "AppendBuffer.h"
#include "UndoSystem.h"
class Buffer {
public:
Buffer();
@@ -75,13 +77,13 @@ public:
Line() = default;
Line(const char *s)
explicit Line(const char *s)
{
assign_from(s ? std::string(s) : std::string());
}
Line(const std::string &s)
explicit Line(const std::string &s)
{
assign_from(s);
}
@@ -137,29 +139,38 @@ public:
// conversions
operator std::string() const
explicit operator std::string() const
{
return std::string(buf_.Data() ? buf_.Data() : "", buf_.Size());
return {buf_.Data() ? buf_.Data() : "", buf_.Size()};
}
// string-like API used by command/renderer layers (implemented via materialization for now)
std::string substr(std::size_t pos) const
[[nodiscard]] std::string substr(std::size_t pos) const
{
const std::size_t n = buf_.Size();
if (pos >= n)
return std::string();
return std::string(buf_.Data() + pos, n - pos);
return {};
return {buf_.Data() + pos, n - pos};
}
std::string substr(std::size_t pos, std::size_t len) const
[[nodiscard]] std::string substr(std::size_t pos, std::size_t len) const
{
const std::size_t n = buf_.Size();
if (pos >= n)
return std::string();
return {};
const std::size_t take = (pos + len > n) ? (n - pos) : len;
return std::string(buf_.Data() + pos, take);
return {buf_.Data() + pos, take};
}
// minimal find() to support search within a line
[[nodiscard]] std::size_t find(const std::string &needle, const std::size_t pos = 0) const
{
// Materialize to std::string for now; Line is backed by AppendBuffer
const auto s = static_cast<std::string>(*this);
return s.find(needle, pos);
}
@@ -251,6 +262,14 @@ public:
return filename_;
}
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
// This does not mark the buffer as file-backed.
void SetVirtualName(const std::string &name)
{
filename_ = name;
is_file_backed_ = false;
}
[[nodiscard]] bool IsFileBacked() const
{
@@ -263,21 +282,37 @@ public:
return dirty_;
}
// Read-only flag
[[nodiscard]] bool IsReadOnly() const
{
return read_only_;
}
void SetCursor(std::size_t x, std::size_t y)
void SetReadOnly(bool ro)
{
read_only_ = ro;
}
void ToggleReadOnly()
{
read_only_ = !read_only_;
}
void SetCursor(const std::size_t x, const std::size_t y)
{
curx_ = x;
cury_ = y;
}
void SetRenderX(std::size_t rx)
void SetRenderX(const std::size_t rx)
{
rx_ = rx;
}
void SetOffsets(std::size_t row, std::size_t col)
void SetOffsets(const std::size_t row, const std::size_t col)
{
rowoffs_ = row;
coloffs_ = col;
@@ -297,7 +332,7 @@ public:
}
void SetMark(std::size_t x, std::size_t y)
void SetMark(const std::size_t x, const std::size_t y)
{
mark_set_ = true;
mark_curx_ = x;
@@ -342,7 +377,7 @@ public:
// Undo system accessors (created per-buffer)
UndoSystem *Undo();
const UndoSystem *Undo() const;
[[nodiscard]] const UndoSystem *Undo() const;
private:
// State mirroring original C struct (without undo_tree)
@@ -354,6 +389,7 @@ private:
std::string filename_;
bool is_file_backed_ = false;
bool dirty_ = false;
bool read_only_ = false;
bool mark_set_ = false;
std::size_t mark_curx_ = 0, mark_cury_ = 0;

View File

@@ -4,14 +4,15 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "0.9.2")
set(KTE_VERSION "1.0.5")
# 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.")
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.")
@@ -55,6 +56,7 @@ set(COMMON_SOURCES
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
KKeymap.cc
TerminalInputHandler.cc
TerminalRenderer.cc
@@ -74,6 +76,7 @@ set(COMMON_HEADERS
Editor.h
AppendBuffer.h
Command.h
HelpText.h
KKeymap.h
InputHandler.h
TerminalInputHandler.h
@@ -99,6 +102,9 @@ add_executable(kte
if (KTE_USE_PIECE_TABLE)
target_compile_definitions(kte PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(kte PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kte ${CURSES_LIBRARIES})
@@ -121,6 +127,10 @@ if (BUILD_TESTS)
target_compile_definitions(test_undo PRIVATE KTE_USE_PIECE_TABLE=1)
endif ()
if (KTE_UNDO_DEBUG)
target_compile_definitions(test_undo PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(test_undo ${CURSES_LIBRARIES})
endif ()
@@ -128,6 +138,8 @@ endif ()
if (${BUILD_GUI})
target_sources(kte PRIVATE
Font.h
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -142,6 +154,8 @@ if (${BUILD_GUI})
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -149,10 +163,22 @@ if (${BUILD_GUI})
GUIFrontend.cc
GUIFrontend.h)
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
if (KTE_UNDO_DEBUG)
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
endif ()
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
# On macOS, build kge as a proper .app bundle
if (APPLE)
# Define the icon file
set(MACOSX_BUNDLE_ICON_FILE kge.icns)
set(kge_ICON "${CMAKE_CURRENT_SOURCE_DIR}/${MACOSX_BUNDLE_ICON_FILE}")
# Add icon to the target sources and mark it as a resource
target_sources(kge PRIVATE ${kge_ICON})
set_source_files_properties(${kge_ICON} PROPERTIES
MACOSX_PACKAGE_LOCATION Resources)
# Configure Info.plist with version and identifiers
set(KGE_BUNDLE_ID "dev.wntrmute.kge")
configure_file(
@@ -164,11 +190,23 @@ if (${BUILD_GUI})
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_GUI_IDENTIFIER ${KGE_BUNDLE_ID}
MACOSX_BUNDLE_BUNDLE_NAME "kge"
MACOSX_BUNDLE_ICON_FILE ${MACOSX_BUNDLE_ICON_FILE}
MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/kge-Info.plist")
add_dependencies(kge kte)
add_custom_command(TARGET kge POST_BUILD
COMMAND ${CMAKE_COMMAND} -E copy
$<TARGET_FILE:kte>
$<TARGET_FILE_DIR:kge>/kte
COMMENT "Copying kte binary into kge.app bundle")
install(TARGETS kge
BUNDLE DESTINATION .
)
install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS
)
else ()
install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
@@ -176,4 +214,5 @@ if (${BUILD_GUI})
endif ()
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif ()

View File

@@ -1,19 +1,22 @@
#include <algorithm>
#include <filesystem>
#include <cstdlib>
#include <regex>
#include <fstream>
#include <sstream>
#include "Command.h"
#include "Editor.h"
#include "Buffer.h"
#include "UndoSystem.h"
// Note: Command layer must remain UI-agnostic. Do not include frontend/IO headers here.
#include "HelpText.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 std::size_t
compute_render_x(const std::string &line, std::size_t curx, std::size_t tabw)
compute_render_x(const std::string &line, const std::size_t curx, const std::size_t tabw)
{
std::size_t rx = 0;
for (std::size_t i = 0; i < curx && i < line.size(); ++i) {
@@ -62,7 +65,7 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
std::size_t rx = 0;
const auto &lines = buf.Rows();
if (cury < lines.size()) {
rx = compute_render_x(lines[cury], curx, 8);
rx = compute_render_x(static_cast<std::string>(lines[cury]), curx, 8);
}
if (rx < coloffs) {
coloffs = rx;
@@ -84,6 +87,31 @@ ensure_at_least_one_line(Buffer &buf)
}
}
// Determine if a command mutates the buffer contents (text edits)
static bool is_mutating_command(CommandId id)
{
switch (id) {
case CommandId::InsertText:
case CommandId::Newline:
case CommandId::Backspace:
case CommandId::DeleteChar:
case CommandId::KillToEOL:
case CommandId::KillLine:
case CommandId::Yank:
case CommandId::DeleteWordPrev:
case CommandId::DeleteWordNext:
case CommandId::IndentRegion:
case CommandId::UnindentRegion:
case CommandId::ReflowParagraph:
case CommandId::KillRegion:
case CommandId::Undo:
case CommandId::Redo:
return true;
default:
return false;
}
}
// --- UI/status helpers ---
static bool
@@ -149,7 +177,7 @@ extract_region_text(const Buffer &buf, std::size_t sx, std::size_t sy, std::size
}
// middle lines full
for (std::size_t y = sy + 1; y < ey; ++y) {
out += rows[y];
out += static_cast<std::string>(rows[y]);
out += '\n';
}
// last line head
@@ -239,7 +267,7 @@ insert_text_at_cursor(Buffer &buf, const std::string &text)
std::string after = rows[cur_y].substr(cur_x);
rows[cur_y].erase(cur_x);
// create new line after current with the 'after' tail
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), after);
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(cur_y + 1), Buffer::Line(after));
// move to start of next line
cur_y += 1;
cur_x = 0;
@@ -328,7 +356,7 @@ cmd_move_cursor_to(CommandContext &ctx)
}
if (by >= lines2.size())
by = lines2.size() - 1;
const std::string &line2 = lines2[by];
std::string line2 = static_cast<std::string>(lines2[by]);
std::size_t rx_target = bco + vx;
std::size_t sx = inverse_render_to_source_col(line2, rx_target, 8);
row = by;
@@ -348,7 +376,7 @@ cmd_move_cursor_to(CommandContext &ctx)
}
if (row >= lines.size())
row = lines.size() - 1;
const std::string &line = lines[row];
std::string line = static_cast<std::string>(lines[row]);
if (col > line.size())
col = line.size();
buf->SetCursor(col, row);
@@ -366,7 +394,7 @@ search_compute_matches(const Buffer &buf, const std::string &q)
return out;
const auto &rows = buf.Rows();
for (std::size_t y = 0; y < rows.size(); ++y) {
const std::string &line = rows[y];
std::string line = static_cast<std::string>(rows[y]);
std::size_t pos = 0;
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
out.emplace_back(y, pos);
@@ -377,6 +405,71 @@ search_compute_matches(const Buffer &buf, const std::string &q)
}
// Regex-based matches (per-line), capturing match length for highlighting
struct RegexMatch {
std::size_t y;
std::size_t x;
std::size_t len;
};
static std::vector<RegexMatch>
search_compute_matches_regex(const Buffer &buf, const std::string &pattern, std::string &err_out)
{
std::vector<RegexMatch> out;
err_out.clear();
if (pattern.empty())
return out;
try {
const std::regex rx(pattern);
const auto &rows = buf.Rows();
for (std::size_t y = 0; y < rows.size(); ++y) {
std::string line = static_cast<std::string>(rows[y]);
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
out.push_back(RegexMatch{
y, static_cast<std::size_t>(m.position()), static_cast<std::size_t>(m.length())
});
}
}
} catch (const std::regex_error &e) {
err_out = e.what();
// Return empty results on error
}
return out;
}
static void
search_apply_match_regex(Editor &ed, Buffer &buf, const std::vector<RegexMatch> &matches)
{
const std::string &q = ed.SearchQuery();
if (matches.empty()) {
ed.SetSearchMatch(0, 0, 0);
// Restore cursor to origin if present
if (ed.SearchOriginSet()) {
buf.SetCursor(ed.SearchOrigX(), ed.SearchOrigY());
buf.SetOffsets(ed.SearchOrigRowoffs(), ed.SearchOrigColoffs());
}
ed.SetSearchIndex(-1);
ed.SetStatus("Regex: " + q);
return;
}
int idx = ed.SearchIndex();
if (idx < 0 || idx >= static_cast<int>(matches.size()))
idx = 0;
const auto &m = matches[static_cast<std::size_t>(idx)];
ed.SetSearchMatch(m.y, m.x, m.len);
buf.SetCursor(m.x, m.y);
ensure_cursor_visible(ed, buf);
char tmp[64];
snprintf(tmp, sizeof(tmp), "%d/%zu", idx + 1, matches.size());
ed.SetStatus(std::string("Regex: ") + q + " " + tmp);
ed.SetSearchIndex(idx);
}
static void
search_apply_match(Editor &ed, Buffer &buf, const std::vector<std::pair<std::size_t, std::size_t> > &matches)
{
@@ -474,7 +567,7 @@ cmd_change_working_directory_start(CommandContext &ctx)
{
std::string initial;
try {
initial = std::filesystem::current_path().string();
initial = std::filesystem::current_path().string() + "/";
} catch (...) {
initial.clear();
}
@@ -667,6 +760,77 @@ cmd_find_start(CommandContext &ctx)
}
static bool
cmd_regex_find_start(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to search");
return false;
}
// Save origin for cancel
ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs());
// Enter regex search mode using the generic prompt system
ctx.editor.SetSearchActive(true);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.SetSearchIndex(-1);
ctx.editor.StartPrompt(Editor::PromptKind::RegexSearch, "Regex", "");
ctx.editor.SetStatus("Regex: ");
return true;
}
static bool
cmd_search_replace_start(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to search");
return false;
}
// Save original cursor/viewport to restore on cancel
ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs());
// Enter search-highlighting mode for the find step
ctx.editor.SetSearchActive(true);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.SetSearchIndex(-1);
// Two-step prompt: first collect find string, then replacement
ctx.editor.SetReplaceFindTmp("");
ctx.editor.SetReplaceWithTmp("");
ctx.editor.StartPrompt(Editor::PromptKind::ReplaceFind, "Replace: find", "");
ctx.editor.SetStatus("Replace: find: ");
return true;
}
static bool
cmd_regex_replace_start(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to search");
return false;
}
// Save original cursor/viewport to restore on cancel
ctx.editor.SetSearchOrigin(buf->Curx(), buf->Cury(), buf->Rowoffs(), buf->Coloffs());
// Enter search-highlighting mode for the find step (regex)
ctx.editor.SetSearchActive(true);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.SetSearchIndex(-1);
// Two-step prompt: first collect regex find pattern, then replacement
ctx.editor.SetReplaceFindTmp("");
ctx.editor.SetReplaceWithTmp("");
ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceFind, "Regex replace: find", "");
ctx.editor.SetStatus("Regex replace: find: ");
return true;
}
static bool
cmd_open_file_start(CommandContext &ctx)
{
@@ -677,6 +841,30 @@ cmd_open_file_start(CommandContext &ctx)
}
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
static bool
cmd_visual_file_picker_toggle(CommandContext &ctx)
{
// Toggle visibility
bool show = !ctx.editor.FilePickerVisible();
ctx.editor.SetFilePickerVisible(show);
if (show) {
// Initialize directory to current working directory if empty
if (ctx.editor.FilePickerDir().empty()) {
try {
ctx.editor.SetFilePickerDir(std::filesystem::current_path().string());
} catch (...) {
ctx.editor.SetFilePickerDir(".");
}
}
ctx.editor.SetStatus("Open File (visual)");
} else {
ctx.editor.SetStatus("Closed file picker");
}
return true;
}
static bool
cmd_jump_to_line_start(CommandContext &ctx)
{
@@ -932,13 +1120,30 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.AppendPromptText(ctx.arg);
// If it's a search prompt, mirror text to search state
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
std::string err;
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
if (!err.empty()) {
ctx.editor.SetStatus(
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err +
"]");
}
if (ctx.editor.SearchIndex() >= static_cast<int>(rmatches.size()))
ctx.editor.SetSearchIndex(rmatches.empty() ? -1 : 0);
search_apply_match_regex(ctx.editor, *buf, rmatches);
} else {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
// Keep index stable unless out of range
if (ctx.editor.SearchIndex() >= static_cast<int>(matches.size()))
ctx.editor.SetSearchIndex(matches.empty() ? -1 : 0);
search_apply_match(ctx.editor, *buf, matches);
}
} else {
// For other prompts, just echo label:text in status
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
@@ -956,7 +1161,7 @@ cmd_insert_text(CommandContext &ctx)
std::vector<std::pair<std::size_t, std::size_t> > matches;
if (!q.empty()) {
for (std::size_t y = 0; y < rows.size(); ++y) {
const std::string &line = rows[y];
std::string line = static_cast<std::string>(rows[y]);
std::size_t pos = 0;
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
matches.emplace_back(y, pos);
@@ -1018,6 +1223,204 @@ cmd_insert_text(CommandContext &ctx)
return true;
}
// Toggle read-only state of the current buffer
static bool
cmd_toggle_read_only(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer");
return false;
}
buf->ToggleReadOnly();
ctx.editor.SetStatus(std::string("Read-only: ") + (buf->IsReadOnly() ? "ON" : "OFF"));
return true;
}
// Open or refresh the +HELP+ buffer with content from docs/kte.1
static bool
cmd_show_help(CommandContext &ctx)
{
const std::string help_name = "+HELP+";
// Try to locate existing +HELP+ buffer
std::vector<Buffer> &bufs = ctx.editor.Buffers();
std::size_t help_index = static_cast<std::size_t>(-1);
for (std::size_t i = 0; i < bufs.size(); ++i) {
if (bufs[i].Filename() == help_name && !bufs[i].IsFileBacked()) {
help_index = i;
break;
}
}
auto roff_to_text = [](const std::string &in) -> std::string {
std::istringstream iss(in);
std::ostringstream out;
std::string line;
auto unquote = [](std::string s) {
if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin());
if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back();
return s;
};
while (std::getline(iss, line)) {
if (line.rfind("'", 0) == 0) {
continue; // comment line
}
if (line.rfind(".", 0) == 0) {
// Macro line
std::istringstream ls(line);
std::string dot, macro;
ls >> dot >> macro;
if (macro == "TH" || macro == "SH") {
std::string title;
std::getline(ls, title);
// trim leading spaces
while (!title.empty() && (title.front() == ' ' || title.front() == '\t')) title.erase(title.begin());
title = unquote(title);
out << "\n\n";
for (auto &c : title) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
out << title << "\n";
} else if (macro == "PP" || macro == "P" || macro == "TP") {
out << "\n";
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
std::string rest;
std::getline(ls, rest);
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) rest.erase(rest.begin());
out << unquote(rest) << "\n";
} else if (macro == "nf" || macro == "fi") {
// ignore fill mode toggles for now
} else {
// Unhandled macro: ignore
}
continue;
}
// Regular text; apply minimal escape replacements
for (std::size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\\') {
if (i + 1 < line.size() && line[i + 1] == '-') { out << '-'; ++i; continue; }
if (i + 3 < line.size() && line[i + 1] == '(') {
std::string esc = line.substr(i + 2, 2);
if (esc == "em") { out << ""; i += 3; continue; }
if (esc == "en") { out << "-"; i += 3; continue; }
}
}
out << line[i];
}
out << "\n";
}
return out.str();
};
auto load_help_text = [&](bool &used_man) -> std::string {
// 1) Prefer embedded/customizable help content
{
std::string embedded = HelpText::Text();
if (!embedded.empty()) { used_man = false; return embedded; }
}
// 2) Fall back to the manpage and convert roff to plain text
const char *man_candidates[] = {
"docs/kte.1",
"./docs/kte.1",
"/usr/local/share/man/man1/kte.1",
"/usr/share/man/man1/kte.1"
};
for (const char *p : man_candidates) {
std::ifstream in(p);
if (in.good()) {
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!s.empty()) { used_man = true; return roff_to_text(s); }
}
}
// Fallback minimal help text
used_man = false;
return std::string(
"KTE - Kyle's Text Editor\n\n"
"About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n\n"
"Core keybindings:\n"
" C-k h Show this help\n"
" C-k s Save buffer\n"
" C-k x Save and quit\n"
" C-k q Quit (confirm if dirty)\n"
" C-k C-q Quit now (no confirm)\n"
" C-k c Close current buffer\n"
" C-k b Switch buffer\n"
" C-k p Next buffer\n"
" C-k n Previous buffer\n"
" C-k e Open file (prompt)\n"
" C-k g Jump to line\n"
" C-k u Undo\n"
" C-k r Redo\n"
" C-k d Kill to end of line\n"
" C-k C-d Kill entire line\n"
" C-k = Indent region\n"
" C-k - Unindent region\n"
" C-k ' Toggle read-only\n"
" C-k l Reload buffer from disk\n"
" C-k a Mark all and jump to end\n"
" C-k v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n"
" C-k o Change working directory (prompt)\n\n"
"ESC/Alt commands:\n"
" ESC q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n"
" ESC d Delete next word\n"
" Alt-w Copy region to kill ring\n\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n");
};
auto populate_from_text = [](Buffer &b, const std::string &text) {
auto &rows = b.Rows();
rows.clear();
std::string line;
line.reserve(128);
for (char ch : text) {
if (ch == '\n') {
rows.emplace_back(line);
line.clear();
} else if (ch != '\r') {
line.push_back(ch);
}
}
// Add last line (even if empty)
rows.emplace_back(line);
b.SetDirty(false);
b.SetCursor(0, 0);
b.SetOffsets(0, 0);
b.SetRenderX(0);
};
if (help_index != static_cast<std::size_t>(-1)) {
Buffer &hb = bufs[help_index];
// If dirty, overwrite with original contents
if (hb.Dirty()) {
bool used_man = false;
std::string text = load_help_text(used_man);
populate_from_text(hb, text);
}
hb.SetReadOnly(true);
ctx.editor.SwitchTo(help_index);
ctx.editor.SetStatus("Help opened");
return true;
}
// Create a new help buffer
Buffer help;
help.SetVirtualName(help_name);
bool used_man = false;
std::string text = load_help_text(used_man);
populate_from_text(help, text);
help.SetReadOnly(true);
std::size_t idx = ctx.editor.AddBuffer(std::move(help));
ctx.editor.SwitchTo(idx);
ctx.editor.SetStatus("Help opened");
return true;
}
static bool
cmd_newline(CommandContext &ctx)
@@ -1027,15 +1430,113 @@ cmd_newline(CommandContext &ctx)
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
std::string value = ctx.editor.PromptText();
ctx.editor.AcceptPrompt();
if (kind == Editor::PromptKind::Search) {
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
// Finish search: keep cursor where it is, clear search UI prompt
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetStatus("Find done");
ctx.editor.SetStatus(kind == Editor::PromptKind::RegexSearch ? "Regex find done" : "Find done");
Buffer *b = ctx.editor.CurrentBuffer();
if (b)
ensure_cursor_visible(ctx.editor, *b);
} else if (kind == Editor::PromptKind::ReplaceFind) {
// Proceed to replacement text prompt
ctx.editor.SetReplaceFindTmp(value);
// Keep search highlights active using the collected find string
ctx.editor.SetSearchActive(true);
ctx.editor.SetSearchQuery(value);
if (Buffer *b = ctx.editor.CurrentBuffer()) {
auto matches = search_compute_matches(*b, ctx.editor.SearchQuery());
search_apply_match(ctx.editor, *b, matches);
}
ctx.editor.StartPrompt(Editor::PromptKind::ReplaceWith, "Replace: with", "");
ctx.editor.SetStatus("Replace: with: ");
return true;
} else if (kind == Editor::PromptKind::ReplaceWith) {
// Execute replace-all
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
if (buf->IsReadOnly()) {
ctx.editor.SetStatus("Read-only buffer");
// Clear search UI state
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
}
const std::string find = ctx.editor.ReplaceFindTmp();
const std::string with = value;
ctx.editor.SetReplaceWithTmp(with);
if (find.empty()) {
ctx.editor.SetStatus("Replace canceled (empty find)");
// Clear search UI state
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
}
// Save original cursor to restore after operations
std::size_t orig_x = buf->Curx();
std::size_t orig_y = buf->Cury();
auto &rows = buf->Rows();
std::size_t total = 0;
UndoSystem *u = buf->Undo();
if (u)
u->commit(); // end any pending batch
for (std::size_t y = 0; y < rows.size(); ++y) {
std::size_t pos = 0;
while (!find.empty()) {
pos = rows[y].find(find, pos);
if (pos == std::string::npos)
break;
// Perform delete of matched segment
rows[y].erase(pos, find.size());
if (u) {
buf->SetCursor(pos, y);
u->Begin(UndoType::Delete);
u->Append(std::string_view(find));
}
// Insert replacement
if (!with.empty()) {
rows[y].insert(pos, with);
if (u) {
buf->SetCursor(pos, y);
u->Begin(UndoType::Insert);
u->Append(std::string_view(with));
}
pos += with.size();
}
++total;
if (with.empty()) {
// Avoid infinite loop when replacing with empty
// pos remains the same; move forward by 1 to continue search
if (pos < rows[y].size())
++pos;
else
break;
}
}
}
buf->SetDirty(true);
// Restore original cursor
if (orig_y < rows.size())
buf->SetCursor(orig_x, orig_y);
ensure_cursor_visible(ctx.editor, *buf);
char msg[128];
std::snprintf(msg, sizeof(msg), "Replaced %zu occurrence%s", total, (total == 1 ? "" : "s"));
ctx.editor.SetStatus(msg);
// Clear search-highlighting state after replace completes
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
} else if (kind == Editor::PromptKind::OpenFile) {
std::string err;
// Expand "~" to the user's home directory
@@ -1226,6 +1727,84 @@ cmd_newline(CommandContext &ctx)
} catch (const std::exception &e) {
ctx.editor.SetStatus(std::string("chdir failed: ") + e.what());
}
} else if (kind == Editor::PromptKind::RegexReplaceFind) {
// Proceed to regex replacement text prompt
ctx.editor.SetReplaceFindTmp(value);
// Keep search highlights active using the collected regex pattern
ctx.editor.SetSearchActive(true);
ctx.editor.SetSearchQuery(value);
if (Buffer *b = ctx.editor.CurrentBuffer()) {
std::string err;
auto rm = search_compute_matches_regex(*b, ctx.editor.SearchQuery(), err);
if (!err.empty()) {
ctx.editor.SetStatus(std::string("Regex: ") + value + " [error: " + err + "]");
}
search_apply_match_regex(ctx.editor, *b, rm);
}
ctx.editor.StartPrompt(Editor::PromptKind::RegexReplaceWith, "Regex replace: with", "");
ctx.editor.SetStatus("Regex replace: with: ");
return true;
} else if (kind == Editor::PromptKind::RegexReplaceWith) {
// Execute regex replace-all
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
if (buf->IsReadOnly()) {
ctx.editor.SetStatus("Read-only buffer");
// Clear search UI state
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
}
const std::string patt = ctx.editor.ReplaceFindTmp();
const std::string repl = value;
ctx.editor.SetReplaceWithTmp(repl);
if (patt.empty()) {
ctx.editor.SetStatus("Regex replace canceled (empty pattern)");
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
}
std::regex rx;
try {
rx = std::regex(patt);
} catch (const std::regex_error &e) {
ctx.editor.SetStatus(std::string("Regex error: ") + e.what());
// Clear search UI state
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
return true;
}
auto &rows = buf->Rows();
std::size_t changed = 0;
for (auto &line : rows) {
std::string before = static_cast<std::string>(line);
std::string after = std::regex_replace(before, rx, repl);
if (after != before) {
line = after;
++changed;
}
}
buf->SetDirty(true);
ctx.editor.SetStatus("Regex replaced in " + std::to_string(changed) + " line(s)");
// Clear search UI state
ctx.editor.SetSearchActive(false);
ctx.editor.SetSearchQuery("");
ctx.editor.SetSearchMatch(0, 0, 0);
ctx.editor.ClearSearchOrigin();
ctx.editor.SetSearchIndex(-1);
if (auto *b = ctx.editor.CurrentBuffer())
ensure_cursor_visible(ctx.editor, *b);
return true;
}
return true;
}
@@ -1259,7 +1838,7 @@ cmd_newline(CommandContext &ctx)
tail = line.substr(x);
line.erase(x);
}
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), tail);
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(y + 1), Buffer::Line(tail));
y += 1;
x = 0;
}
@@ -1281,13 +1860,28 @@ cmd_backspace(CommandContext &ctx)
// If a prompt is active, backspace edits the prompt text
if (ctx.editor.PromptActive()) {
ctx.editor.BackspacePromptText();
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
Buffer *buf2 = ctx.editor.CurrentBuffer();
if (buf2) {
ctx.editor.SetSearchQuery(ctx.editor.PromptText());
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind) {
std::string err;
auto rm = search_compute_matches_regex(*buf2, ctx.editor.SearchQuery(), err);
if (!err.empty()) {
ctx.editor.SetStatus(
std::string("Regex: ") + ctx.editor.PromptText() + " [error: "
+ err + "]");
}
search_apply_match_regex(ctx.editor, *buf2, rm);
} else {
auto matches = search_compute_matches(*buf2, ctx.editor.SearchQuery());
search_apply_match(ctx.editor, *buf2, matches);
}
}
} else {
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
}
@@ -1500,12 +2094,12 @@ cmd_kill_line(CommandContext &ctx)
break;
if (rows.size() == 1) {
// last remaining line: clear its contents
killed_total += rows[0];
killed_total += static_cast<std::string>(rows[0]);
rows[0].Clear();
y = 0;
} else if (y < rows.size()) {
// erase current line; keep y pointing at the next line
killed_total += rows[y];
killed_total += static_cast<std::string>(rows[y]);
killed_total += "\n";
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(y));
if (y >= rows.size()) {
@@ -1700,7 +2294,28 @@ cmd_move_left(CommandContext &ctx)
if (auto *u = buf->Undo())
u->commit();
// If a prompt is active and it's search, go to previous match
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
if (ctx.editor.PromptActive() &&
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
std::string err;
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
if (!err.empty()) {
ctx.editor.SetStatus(
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
}
if (!rmatches.empty()) {
int idx = ctx.editor.SearchIndex();
if (idx < 0)
idx = 0;
idx = (idx - 1 + static_cast<int>(rmatches.size())) % static_cast<int>(rmatches.size());
ctx.editor.SetSearchIndex(idx);
search_apply_match_regex(ctx.editor, *buf, rmatches);
} else {
search_apply_match_regex(ctx.editor, *buf, rmatches);
}
} else {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
if (!matches.empty()) {
int idx = ctx.editor.SearchIndex();
@@ -1712,6 +2327,7 @@ cmd_move_left(CommandContext &ctx)
} else {
search_apply_match(ctx.editor, *buf, matches);
}
}
return true;
}
if (ctx.editor.SearchActive()) {
@@ -1755,7 +2371,28 @@ cmd_move_right(CommandContext &ctx)
return false;
if (auto *u = buf->Undo())
u->commit();
if (ctx.editor.PromptActive() && ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search) {
if (ctx.editor.PromptActive() &&
(ctx.editor.CurrentPromptKind() == Editor::PromptKind::Search ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
ctx.editor.CurrentPromptKind() == Editor::PromptKind::ReplaceFind)) {
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::RegexSearch) {
std::string err;
auto rmatches = search_compute_matches_regex(*buf, ctx.editor.SearchQuery(), err);
if (!err.empty()) {
ctx.editor.SetStatus(
std::string("Regex: ") + ctx.editor.PromptText() + " [error: " + err + "]");
}
if (!rmatches.empty()) {
int idx = ctx.editor.SearchIndex();
if (idx < 0)
idx = 0;
idx = (idx + 1) % static_cast<int>(rmatches.size());
ctx.editor.SetSearchIndex(idx);
search_apply_match_regex(ctx.editor, *buf, rmatches);
} else {
search_apply_match_regex(ctx.editor, *buf, rmatches);
}
} else {
auto matches = search_compute_matches(*buf, ctx.editor.SearchQuery());
if (!matches.empty()) {
int idx = ctx.editor.SearchIndex();
@@ -1767,6 +2404,7 @@ cmd_move_right(CommandContext &ctx)
} else {
search_apply_match(ctx.editor, *buf, matches);
}
}
return true;
}
if (ctx.editor.SearchActive()) {
@@ -2186,7 +2824,7 @@ cmd_delete_word_prev(CommandContext &ctx)
// Then collect complete lines between y and start_y
for (std::size_t ly = y + 1; ly < start_y; ++ly) {
deleted += "\n";
deleted += rows[ly];
deleted += static_cast<std::string>(rows[ly]);
}
// Finally, collect from beginning of start_y to start_x
if (start_y < rows.size()) {
@@ -2283,7 +2921,7 @@ cmd_delete_word_next(CommandContext &ctx)
// Then collect complete lines between start_y and y
for (std::size_t ly = start_y + 1; ly < y; ++ly) {
deleted += "\n";
deleted += rows[ly];
deleted += static_cast<std::string>(rows[ly]);
}
// Finally, collect from beginning of y to x
if (y < rows.size()) {
@@ -2553,6 +3191,15 @@ InstallDefaultCommands()
cmd_unknown_kcommand
});
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
CommandRegistry::Register({
CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start
});
CommandRegistry::Register({
CommandId::RegexpReplace, "regex-replace", "Begin regex search & replace", cmd_regex_replace_start
});
CommandRegistry::Register({
CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start
});
CommandRegistry::Register({
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
});
@@ -2622,14 +3269,25 @@ InstallDefaultCommands()
CommandRegistry::Register({
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
});
// Read-only
CommandRegistry::Register({CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only});
// Buffer operations
CommandRegistry::Register({
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
});
// Help
CommandRegistry::Register({
CommandId::ShowHelp, "help", "+HELP+ buffer with manual text", cmd_show_help
});
CommandRegistry::Register({
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
cmd_mark_all_and_jump_end
});
// GUI
CommandRegistry::Register({
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
cmd_visual_file_picker_toggle
});
// Working directory
CommandRegistry::Register({
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
@@ -2661,6 +3319,15 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
CommandId::CopyRegion && id != CommandId::DeleteWordPrev && id != CommandId::DeleteWordNext) {
ed.SetKillChain(false);
}
// If buffer is read-only, block mutating commands outside of prompts
if (!ed.PromptActive()) {
Buffer *b = ed.CurrentBuffer();
if (b && b->IsReadOnly() && is_mutating_command(id)) {
ed.SetStatus("Read-only buffer");
return true; // treated as handled, but no change
}
}
CommandContext ctx{ed, arg, count};
return cmd->handler ? cmd->handler(ctx) : false;
}

View File

@@ -23,7 +23,11 @@ enum class CommandId {
Refresh, // force redraw
KPrefix, // show "C-k _" prompt in status when entering k-command
FindStart, // begin incremental search (placeholder)
RegexFindStart, // begin regex search (C-r)
RegexpReplace, // begin regex search & replace (C-t)
SearchReplace, // begin search & replace (two-step prompt)
OpenFileStart, // begin open-file prompt
VisualFilePickerToggle,
// Buffers
BufferSwitchStart, // begin buffer switch prompt
BufferClose,
@@ -69,6 +73,8 @@ enum class CommandId {
IndentRegion, // indent region (C-k =)
UnindentRegion, // unindent region (C-k -)
ReflowParagraph, // reflow paragraph to column width (ESC q)
// Read-only buffers
ToggleReadOnly, // toggle current buffer read-only (C-k ')
// Buffer operations
ReloadBuffer, // reload buffer from disk (C-k l)
MarkAllAndJumpEnd, // set mark at beginning, jump to end (C-k a)
@@ -76,6 +82,8 @@ enum class CommandId {
JumpToLine, // prompt for line and jump (C-k g)
ShowWorkingDirectory, // Display the current working directory in the editor message.
ChangeWorkingDirectory, // Change the editor's current directory.
// Help
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
// Meta
UnknownKCommand, // arg: single character that was not recognized after C-k
};

View File

@@ -1,9 +1,9 @@
#include "Editor.h"
#include <algorithm>
#include <utility>
#include <filesystem>
#include "Editor.h"
Editor::Editor() = default;

View File

@@ -302,7 +302,21 @@ public:
// --- Generic Prompt subsystem (for search, open-file, save-as, etc.) ---
enum class PromptKind { None = 0, Search, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
enum class PromptKind {
None = 0,
Search,
RegexSearch,
RegexReplaceFind, // step 1 of Regex Search & Replace: find pattern
RegexReplaceWith, // step 2 of Regex Search & Replace: replacement text
OpenFile,
SaveAs,
Confirm,
BufferSwitch,
GotoLine,
Chdir,
ReplaceFind, // step 1 of Search & Replace: find what
ReplaceWith // step 2 of Search & Replace: replace with
};
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
@@ -441,6 +455,31 @@ public:
return buffers_;
}
// --- GUI: Visual File Picker state ---
void SetFilePickerVisible(bool on)
{
file_picker_visible_ = on;
}
[[nodiscard]] bool FilePickerVisible() const
{
return file_picker_visible_;
}
void SetFilePickerDir(const std::string &path)
{
file_picker_dir_ = path;
}
[[nodiscard]] const std::string &FilePickerDir() const
{
return file_picker_dir_;
}
private:
std::size_t rows_ = 0, cols_ = 0;
int mode_ = 0;
@@ -478,6 +517,21 @@ private:
std::string prompt_label_;
std::string prompt_text_;
std::string pending_overwrite_path_;
// GUI-only state (safe no-op in terminal builds)
bool file_picker_visible_ = false;
std::string file_picker_dir_;
// Temporary state for Search & Replace flow
public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
private:
std::string replace_find_tmp_;
std::string replace_with_tmp_;
};
#endif // KTE_EDITOR_H

View File

@@ -4,8 +4,6 @@
#ifndef KTE_FRONTEND_H
#define KTE_FRONTEND_H
#include <memory>
class Editor;
class InputHandler;

107
GUIConfig.cc Normal file
View File

@@ -0,0 +1,107 @@
#include <cstdlib>
#include <cstring>
#include <fstream>
#include <sstream>
#include <algorithm>
#include "GUIConfig.h"
static void
trim(std::string &s)
{
auto not_space = [](int ch) {
return !std::isspace(ch);
};
s.erase(s.begin(), std::find_if(s.begin(), s.end(), not_space));
s.erase(std::find_if(s.rbegin(), s.rend(), not_space).base(), s.end());
}
static std::string
default_config_path()
{
const char *home = std::getenv("HOME");
if (!home || !*home)
return std::string();
std::string path(home);
path += "/.config/kte/kge.ini";
return path;
}
GUIConfig
GUIConfig::Load()
{
GUIConfig cfg; // defaults already set
std::string path = default_config_path();
if (!path.empty()) {
cfg.LoadFromFile(path);
}
return cfg;
}
bool
GUIConfig::LoadFromFile(const std::string &path)
{
std::ifstream in(path);
if (!in.good())
return false;
std::string line;
while (std::getline(in, line)) {
// Remove comments starting with '#' or ';'
auto hash = line.find('#');
if (hash != std::string::npos)
line.erase(hash);
auto sc = line.find("//");
if (sc != std::string::npos)
line.erase(sc);
// Basic key=value
auto eq = line.find('=');
if (eq == std::string::npos)
continue;
std::string key = line.substr(0, eq);
std::string val = line.substr(eq + 1);
trim(key);
trim(val);
// Strip trailing semicolon
if (!val.empty() && val.back() == ';') {
val.pop_back();
trim(val);
}
// Lowercase key
std::transform(key.begin(), key.end(), key.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (key == "fullscreen") {
fullscreen = (val == "1" || val == "true" || val == "on" || val == "yes");
} else if (key == "columns" || key == "cols") {
int v = columns;
try {
v = std::stoi(val);
} catch (...) {}
if (v > 0)
columns = v;
} else if (key == "rows") {
int v = rows;
try {
v = std::stoi(val);
} catch (...) {}
if (v > 0)
rows = v;
} else if (key == "font_size" || key == "fontsize") {
float v = font_size;
try {
v = std::stof(val);
} catch (...) {}
if (v > 0.0f)
font_size = v;
}
}
return true;
}

27
GUIConfig.h Normal file
View File

@@ -0,0 +1,27 @@
/*
* GUIConfig - loads simple GUI configuration from $HOME/.config/kte/kge.ini
*/
#ifndef KTE_GUI_CONFIG_H
#define KTE_GUI_CONFIG_H
#include <string>
#ifndef KTE_FONT_SIZE
#define KTE_FONT_SIZE 16.0f
#endif
class GUIConfig {
public:
bool fullscreen = false;
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
// Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load();
// Load from explicit path. Returns true if file existed and was parsed.
bool LoadFromFile(const std::string &path);
};
#endif // KTE_GUI_CONFIG_H

View File

@@ -1,18 +1,25 @@
#include <SDL.h>
#include <SDL_opengl.h>
#include <imgui.h>
#include <backends/imgui_impl_sdl2.h>
#include <backends/imgui_impl_opengl3.h>
#include <cstdio>
#include <string>
#include <cstring>
#include <cstdlib>
#include <algorithm>
#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"
#include "GUIFrontend.h"
#include "Font.h" // embedded default font (DefaultFontRegular)
#include "GUIConfig.h"
#ifndef KTE_FONT_SIZE
#define KTE_FONT_SIZE 16.0f
#endif
static const char *kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
@@ -24,6 +31,9 @@ GUIFrontend::Init(Editor &ed)
return false;
}
// Load GUI configuration (fullscreen, columns/rows, font size)
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load();
// 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);
@@ -33,14 +43,56 @@ GUIFrontend::Init(Editor &ed)
SDL_GL_SetAttribute(SDL_GL_DEPTH_SIZE, 24);
SDL_GL_SetAttribute(SDL_GL_STENCIL_SIZE, 8);
// Compute desired window size from config
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
if (fullscreen) {
// "Fullscreen": fill the usable bounds of the primary display.
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
width_ = usable.w;
height_ = usable.h;
}
#if !defined(__APPLE__)
// Non-macOS: desktop fullscreen uses the current display resolution.
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
#endif
} else {
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
int w = static_cast<int>(columns * font_size);
int h = static_cast<int>((rows * 2) * font_size);
// As a safety, clamp to display usable bounds if retrievable
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
w = std::min(w, usable.w);
h = std::min(h, usable.h);
}
width_ = std::max(320, w);
height_ = std::max(200, h);
}
window_ = SDL_CreateWindow(
"kte",
"kge - kyle's text editor " KTE_VERSION_STR,
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width_, height_,
SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI);
win_flags);
if (!window_)
return false;
#if defined(__APPLE__)
// macOS: when "fullscreen" is requested, position the window at the
// top-left of the usable display area to mimic fullscreen while keeping
// the system menu bar visible.
if (fullscreen) {
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
SDL_SetWindowPosition(window_, usable.x, usable.y);
}
}
#endif
gl_ctx_ = SDL_GL_CreateContext(window_);
if (!gl_ctx_)
return false;
@@ -64,11 +116,23 @@ GUIFrontend::Init(Editor &ed)
width_ = w;
height_ = h;
// Initialize GUI font from embedded default
#ifndef KTE_FONT_SIZE
#define KTE_FONT_SIZE 16.0f
#if defined(__APPLE__)
// Workaround: On macOS Retina when starting maximized, we sometimes get a
// subtle input vs draw alignment mismatch until the first manual resize.
// Nudge the window size by 1px and back to trigger a proper internal
// recomputation, without visible impact.
if (w > 1 && h > 1) {
SDL_SetWindowSize(window_, w - 1, h - 1);
SDL_SetWindowSize(window_, w, h);
// Update cached size in case backend reports immediately
SDL_GetWindowSize(window_, &w, &h);
width_ = w;
height_ = h;
}
#endif
LoadGuiFont_(nullptr, (float) KTE_FONT_SIZE);
// Initialize GUI font from embedded default (use configured size or compiled default)
LoadGuiFont_(nullptr, (float) font_size);
return true;
}

View File

@@ -8,6 +8,7 @@
#include "GUIInputHandler.h"
#include "GUIRenderer.h"
struct SDL_Window;
typedef void *SDL_GLContext;

View File

@@ -1,7 +1,8 @@
#include <SDL.h>
#include <cstdio>
#include <ncurses.h>
#include <SDL.h>
#include "GUIInputHandler.h"
#include "KKeymap.h"

View File

@@ -4,11 +4,12 @@
#ifndef KTE_GUI_INPUT_HANDLER_H
#define KTE_GUI_INPUT_HANDLER_H
#include <queue>
#include <mutex>
#include <queue>
#include "InputHandler.h"
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
class GUIInputHandler final : public InputHandler {

View File

@@ -1,29 +1,47 @@
#include "GUIRenderer.h"
#include "Editor.h"
#include "Buffer.h"
#include "Command.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstdlib>
#include <filesystem>
#include <limits>
#include <string>
#include <imgui.h>
#include <cstdio>
#include <string>
#include <filesystem>
#include <cmath>
#include <limits>
#include <regex>
#include "GUIRenderer.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
# define KTE_VERSION_STR "dev"
#endif
// ImGui compatibility: some bundled ImGui versions (or builds without docking)
// don't define ImGuiWindowFlags_NoDocking. Treat it as 0 in that case.
#ifndef ImGuiWindowFlags_NoDocking
# define ImGuiWindowFlags_NoDocking 0
#endif
void
GUIRenderer::Draw(Editor &ed)
{
// Make the editor window occupy the entire GUI container/viewport
ImGuiViewport *vp = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(vp->Pos);
ImGui::SetNextWindowSize(vp->Size);
// On HiDPI/Retina, snap to integer pixels to prevent any draw vs hit-test
// mismatches that can appear on the very first maximized frame.
ImVec2 main_pos = vp->Pos;
ImVec2 main_sz = vp->Size;
main_pos.x = std::floor(main_pos.x + 0.5f);
main_pos.y = std::floor(main_pos.y + 0.5f);
main_sz.x = std::floor(main_sz.x + 0.5f);
main_sz.y = std::floor(main_sz.y + 0.5f);
ImGui::SetNextWindowPos(main_pos);
ImGui::SetNextWindowSize(main_sz);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoResize
@@ -167,9 +185,13 @@ GUIRenderer::Draw(Editor &ed)
if (px < 0.0f)
px = 0.0f;
// Empty buffer guard: if there are no lines yet, just move to 0:0
if (lines.empty()) {
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
} else {
// Convert pixel X to a render-column target including horizontal col offset
// Use our own tab expansion of width 8 to match command layer logic.
const std::string &line_clicked = lines[by];
std::string line_clicked = static_cast<std::string>(lines[by]);
const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
@@ -216,19 +238,81 @@ GUIRenderer::Draw(Editor &ed)
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, best_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
}
}
// Cache current horizontal offset in rendered columns
const std::size_t coloffs_now = buf->Coloffs();
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
// Capture the screen position before drawing the line
ImVec2 line_pos = ImGui::GetCursorScreenPos();
const std::string &line = lines[i];
std::string line = static_cast<std::string>(lines[i]);
// Expand tabs to spaces with width=8 and apply horizontal scroll offset
const std::size_t tabw = 8;
std::string expanded;
expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing
// Compute search highlight ranges for this line in source indices
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
if (search_mode) {
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.position());
std::size_t ex = sx + static_cast<std::size_t>(m.length());
hl_src_ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// ignore invalid patterns here; status line already shows the error
}
} else {
const std::string &q = ed.SearchQuery();
std::size_t pos = 0;
while (!q.empty() && (pos = line.find(q, pos)) != std::string::npos) {
hl_src_ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
auto src_to_rx = [&](std::size_t upto_src_exclusive) -> std::size_t {
std::size_t rx = 0;
std::size_t s = 0;
while (s < upto_src_exclusive && s < line.size()) {
if (line[s] == '\t')
rx += (tabw - (rx % tabw));
else
rx += 1;
++s;
}
return rx;
};
// Draw background highlights (under text)
if (search_mode && !hl_src_ranges.empty()) {
// Current match emphasis
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
for (const auto &rg : hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset
if (rx_end <= coloffs_now) continue; // fully left of view
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
// Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end;
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line (ImGui child scrolling will handle clipping)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
@@ -268,7 +352,6 @@ GUIRenderer::Draw(Editor &ed)
// Status bar spanning full width
ImGui::Separator();
// Build three segments: left (app/version/buffer/dirty), middle (message), right (cursor/mark)
// Compute full content width and draw a filled background rectangle
ImVec2 win_pos = ImGui::GetWindowPos();
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
@@ -281,6 +364,85 @@ GUIRenderer::Draw(Editor &ed)
ImVec2 p1(x1, cursor.y + bar_h);
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
// If a prompt is active, replace the entire status bar with the prompt text
if (ed.PromptActive()) {
std::string label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
if (home_c && *home_c) {
std::string home(home_c);
if (ptext.rfind(home, 0) == 0) {
std::string rest = ptext.substr(home.size());
if (rest.empty())
ptext = "~";
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
ptext = std::string("~") + rest;
}
}
}
float pad = 6.f;
float left_x = p0.x + pad;
float right_x = p1.x - pad;
float max_px = std::max(0.0f, right_x - left_x);
std::string prefix;
if (!label.empty()) prefix = label + ": ";
// Compose showing right-end of filename portion when too long for space
std::string final_msg;
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
// Trim from left until it fits by pixel width
std::string tail = ptext;
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
if (tail_sz.x > avail_px) {
// Remove leading chars until it fits
// Use a simple loop; text lengths are small here
size_t start = 0;
// To avoid O(n^2) worst-case, remove chunks
while (start < tail.size()) {
// Estimate how many chars to skip based on ratio
float ratio = tail_sz.x / avail_px;
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
start += skip;
std::string candidate = tail.substr(start);
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
if (cand_sz.x <= avail_px) {
tail = candidate;
tail_sz = cand_sz;
break;
}
}
if (ImGui::CalcTextSize(tail.c_str()).x > avail_px && !tail.empty()) {
// As a last resort, ensure fit by chopping exactly
// binary reduce
size_t lo = 0, hi = tail.size();
while (lo < hi) {
size_t mid = (lo + hi) / 2;
std::string cand = tail.substr(mid);
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
}
tail = tail.substr(lo);
}
}
final_msg = prefix + tail;
} else {
final_msg = prefix + ptext;
}
ImVec2 msg_sz = ImGui::CalcTextSize(final_msg.c_str());
ImGui::PushClipRect(ImVec2(p0.x, p0.y), ImVec2(p1.x, p1.y), true);
ImGui::SetCursorScreenPos(ImVec2(left_x, p0.y + (bar_h - msg_sz.y) * 0.5f));
ImGui::TextUnformatted(final_msg.c_str());
ImGui::PopClipRect();
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
} else {
// Build left text
std::string left;
left.reserve(256);
@@ -297,6 +459,18 @@ GUIRenderer::Draw(Editor &ed)
} catch (...) {}
}
left += " ";
// Insert buffer position prefix "[x/N] " before filename
{
std::size_t total = ed.BufferCount();
if (total > 0) {
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // 1-based for display
left += "[";
left += std::to_string(static_cast<unsigned long long>(idx1));
left += "/";
left += std::to_string(static_cast<unsigned long long>(total));
left += "] ";
}
}
left += fname;
if (buf->Dirty())
left += " *";
@@ -375,8 +549,158 @@ GUIRenderer::Draw(Editor &ed)
}
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
}
}
ImGui::End();
ImGui::PopStyleVar(3);
// --- Visual File Picker overlay (GUI only) ---
if (ed.FilePickerVisible()) {
// Centered popup-style window that always fits within the current viewport
ImGuiViewport *vp2 = ImGui::GetMainViewport();
// Desired size, min size, and margins
const ImVec2 want(800.0f, 500.0f);
const ImVec2 min_sz(240.0f, 160.0f);
const float margin = 20.0f; // space from viewport edges
// Compute the maximum allowed size (viewport minus margins) and make sure it's not negative
ImVec2 max_sz(std::max(32.0f, vp2->Size.x - 2.0f * margin),
std::max(32.0f, vp2->Size.y - 2.0f * margin));
// Clamp desired size to [min_sz, max_sz]
ImVec2 size(std::min(want.x, max_sz.x), std::min(want.y, max_sz.y));
size.x = std::max(size.x, std::min(min_sz.x, max_sz.x));
size.y = std::max(size.y, std::min(min_sz.y, max_sz.y));
// Center within the viewport using the final size
ImVec2 pos(vp2->Pos.x + std::max(margin, (vp2->Size.x - size.x) * 0.5f),
vp2->Pos.y + std::max(margin, (vp2->Size.y - size.y) * 0.5f));
// On HiDPI displays (macOS Retina), ensure integer pixel alignment to avoid
// potential hit-test vs draw mismatches from sub-pixel positions.
pos.x = std::floor(pos.x + 0.5f);
pos.y = std::floor(pos.y + 0.5f);
size.x = std::floor(size.x + 0.5f);
size.y = std::floor(size.y + 0.5f);
ImGui::SetNextWindowPos(pos, ImGuiCond_Always);
ImGui::SetNextWindowSize(size, ImGuiCond_Always);
ImGuiWindowFlags wflags = ImGuiWindowFlags_NoResize | ImGuiWindowFlags_NoCollapse |
ImGuiWindowFlags_NoDocking;
bool open = true;
if (ImGui::Begin("File Picker", &open, wflags)) {
// Current directory
std::string curdir = ed.FilePickerDir();
if (curdir.empty()) {
try {
curdir = std::filesystem::current_path().string();
} catch (...) {
curdir = ".";
}
ed.SetFilePickerDir(curdir);
}
ImGui::TextUnformatted(curdir.c_str());
ImGui::SameLine();
if (ImGui::Button("Up")) {
try {
std::filesystem::path p(curdir);
if (p.has_parent_path()) {
ed.SetFilePickerDir(p.parent_path().string());
}
} catch (...) {}
}
ImGui::SameLine();
if (ImGui::Button("Close")) {
ed.SetFilePickerVisible(false);
}
ImGui::Separator();
// Header
ImGui::TextUnformatted("Name");
ImGui::Separator();
// Scrollable list
ImGui::BeginChild("picker-list", ImVec2(0, 0), true);
// Build entries: directories first then files, alphabetical
struct Entry {
std::string name;
std::filesystem::path path;
bool is_dir;
};
std::vector<Entry> entries;
entries.reserve(256);
// Optional parent entry
try {
std::filesystem::path base(curdir);
std::error_code ec;
for (auto it = std::filesystem::directory_iterator(base, ec);
!ec && it != std::filesystem::directory_iterator(); it.increment(ec)) {
const auto &p = it->path();
std::string nm;
try {
nm = p.filename().string();
} catch (...) {
continue;
}
if (nm == "." || nm == "..")
continue;
bool is_dir = false;
std::error_code ec2;
is_dir = it->is_directory(ec2);
entries.push_back({nm, p, is_dir});
}
} catch (...) {
// ignore listing errors; show empty
}
std::sort(entries.begin(), entries.end(), [](const Entry &a, const Entry &b) {
if (a.is_dir != b.is_dir)
return a.is_dir && !b.is_dir;
return a.name < b.name;
});
// Draw rows
int idx = 0;
for (const auto &e: entries) {
ImGui::PushID(idx++); // ensure unique/stable IDs even if names repeat
std::string label;
label.reserve(e.name.size() + 4);
if (e.is_dir)
label += "[";
label += e.name;
if (e.is_dir)
label += "]";
// Render selectable row
ImGui::Selectable(label.c_str(), false, ImGuiSelectableFlags_AllowDoubleClick);
// Activate based strictly on hover + mouse, to avoid any off-by-one due to click routing
if (ImGui::IsItemHovered()) {
if (e.is_dir && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
// Enter directory on double-click
ed.SetFilePickerDir(e.path.string());
} else if (!e.is_dir && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
// Open file on single click
std::string err;
if (!ed.OpenFile(e.path.string(), err)) {
ed.SetStatus(std::string("open: ") + err);
} else {
ed.SetStatus(std::string("Opened: ") + e.name);
}
ed.SetFilePickerVisible(false);
}
}
ImGui::PopID();
}
ImGui::EndChild();
}
ImGui::End();
if (!open) {
ed.SetFilePickerVisible(false);
}
}
}

View File

@@ -6,7 +6,7 @@
#include "Renderer.h"
class GUIRenderer : public Renderer {
class GUIRenderer final : public Renderer {
public:
GUIRenderer() = default;

View File

@@ -1,9 +1,10 @@
#include "GapBuffer.h"
#include <algorithm>
#include <cassert>
#include <cstring>
#include "GapBuffer.h"
GapBuffer::GapBuffer() = default;

View File

@@ -6,6 +6,7 @@
#include <cstddef>
class GapBuffer {
public:
GapBuffer();

55
HelpText.cc Normal file
View File

@@ -0,0 +1,55 @@
/*
* HelpText.cc - embedded/customizable help content
*/
#include "HelpText.h"
std::string
HelpText::Text()
{
// Customize the help text here. This string will be used by C-k h first.
// You can keep it empty to fall back to the manpage or built-in defaults.
// Note: keep newline characters as-is; the renderer splits lines on '\n'.
return std::string(
"KTE - Kyle's Text Editor\n\n"
"About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
"\n"
"Core keybindings:\n"
" C-k ' Toggle read-only\n"
" C-k - Unindent region\n"
" C-k = Indent region\n"
" C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\n"
" C-k a Mark all and jump to end\n"
" C-k b Switch buffer\n"
" C-k c Close current buffer\n"
" C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n"
" C-k g Jump to line\n"
" C-k h Show this help\n"
" C-k l Reload buffer from disk\n"
" C-k n Previous buffer\n"
" C-k o Change working directory (prompt)\n"
" C-k p Next buffer\n"
" C-k q Quit (confirm if dirty)\n"
" C-k r Redo\n"
" C-k s Save buffer\n"
" C-k u Undo\n"
" C-k v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n"
" C-k x Save and quit\n"
"\n"
"ESC/Alt commands:\n"
" ESC q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n"
" ESC d Delete next word\n"
" Alt-w Copy region to kill ring\n\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"
);
}

17
HelpText.h Normal file
View File

@@ -0,0 +1,17 @@
/*
* HelpText.h - embedded/customizable help content
*/
#ifndef KTE_HELPTEXT_H
#define KTE_HELPTEXT_H
#include <string>
class HelpText {
public:
// Returns the embedded help text as a single string with newlines.
// Project maintainers can customize the returned string below
// (in HelpText.cc) without touching the help command logic.
static std::string Text();
};
#endif // KTE_HELPTEXT_H

View File

@@ -8,6 +8,7 @@
#include "Command.h"
// Result of translating raw input into an editor command.
struct MappedInput {
bool hasCommand = false;

View File

@@ -1,9 +1,9 @@
#include "KKeymap.h"
#include <iostream>
#include <ncurses.h>
#include <ostream>
#include "KKeymap.h"
auto
KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
@@ -33,6 +33,10 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
out = CommandId::Redo; // C-k r (redo)
return true;
}
if (ascii_key == '\'') {
out = CommandId::ToggleReadOnly; // C-k ' (toggle read-only)
return true;
}
switch (k_lower) {
case 'a':
@@ -59,6 +63,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'g':
out = CommandId::JumpToLine;
return true;
case 'h':
out = CommandId::ShowHelp;
return true;
case 'j':
out = CommandId::JumpToMark;
return true;
@@ -83,6 +90,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'u':
out = CommandId::Undo;
return true;
case 'v':
out = CommandId::VisualFilePickerToggle;
return true;
case 'w':
out = CommandId::ShowWorkingDirectory;
return true;
@@ -142,6 +152,15 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
case 's':
out = CommandId::FindStart;
return true;
case 'r':
out = CommandId::RegexFindStart; // C-r regex search
return true;
case 't':
out = CommandId::RegexpReplace; // C-t regex search & replace
return true;
case 'h':
out = CommandId::SearchReplace; // C-h: search & replace
return true;
case 'l':
out = CommandId::Refresh;
return true;

View File

@@ -6,6 +6,7 @@
#include "Command.h"
// Lookup the command to execute after a C-k prefix.
// Parameters:
// - ascii_key: ASCII code of the key, preferably lowercased if it's a letter.

View File

@@ -8,6 +8,7 @@
#include <string>
#include <vector>
class PieceTable {
public:
PieceTable();

133
README.md
View File

@@ -1,9 +1,11 @@
kte Kyle's Text Editor
kte - Kyle's Text Editor
![Editor screenshot](./docs/screenshot.jpg)
Vision
-------
kte will be a small, fast, and understandable text editor with a
terminal<EFBFBD>first UX and an optional ImGui GUI. It modernizes the
kte is a small, fast, and understandable text editor with a
terminal-first UX and an optional ImGui GUI. It modernizes the
original ke editor while preserving its familiar WordStar/VDEstyle
command model and Emacsinfluenced ergonomics. The focus is on
simplicity of design, excellent latency, and pragmatic features you
@@ -11,7 +13,9 @@ can learn and keep in your head.
I am experimenting with using Jetbrains Junie to assist in
development, largely as a way to learn the effective use of agentic
coding.
coding. I worked with the agent by feeding it notes that I've been
taking about text editors for the last few years, as well as the
sources from the original ke editor that is all handwritten C.
Project Goals
-------------
@@ -26,89 +30,6 @@ Project Goals
so a GUI can grow independently of the TUI.
- Minimize dependencies; the GUI layer remains optional and isolated.
User Experience (intended)
--------------------------
- Terminal first: instant startup, responsive editing, no surprises
over SSH.
- Optional GUI: an ImGuibased window with tabs, menus, and
palette—sharing the same editor core and command model.
- Discoverable command model: WordStar/VDE style with a `C-k` prefix,
Emacslike incremental search, and context help.
- Sensible defaults with a simple config file for remaps and theme
selection.
- Respect the file system: no magic project files; autosave and
crashrecovery journals are optin and visible.
Core Features (roadmapped)
--------------------------
- Buffers and windows
- Multiple file buffers; fast switching, closing, and reopening.
- Split views (horizontal/vertical) in TUI and tiled panels in
GUI.
- Editing primitives
- Gap buffer (primary) with an alternative piece table for
largeedit scenarios.
- Kill/yank ring, word/sentence/paragraph motions, and rectangle
ops.
- Undo/redo with grouped edits and timetravel scrubbing.
- Search and replace
- Incremental search (C-s) and regex search (C-r) with live
highlighting.
- Multifile grep with a quickfix list; replace with confirm.
- Files and projects
- Robust encoding/lineending detection; safe writes (atomic where
possible).
- File tree sidebar (GUI) and quickopen palette.
- Lightweight session restore.
- Language niceties (optin, no runtime servers required)
- Syntax highlighting via fast, tabledriven lexers.
- Basic indentation rules per language; trailing whitespace/EOF
newline helpers.
- Extensibility (later)
- Command palette actions backed by the core command model.
- Small C++ plugin ABI and a scripting shim for configtime
customization.
Interfaces
----------
- CLI: the primary interface. `kte [files]` starts in the terminal,
adopting your `$TERM` capabilities. Terminal mode is implemented
using ncurses.
- GUI: an optional ImGuibased frontend that embeds the same editor
core.
Architecture (intended)
-----------------------
- Core model
- Buffer: file I/O, cursor/mark, viewport state, and edit
operations.
- GapBuffer: fast inmemory text structure for typical edits.
- PieceTable: alternative representation for heavy insert/delete
workflows.
- Controller layer
- InputHandler interface with `TerminalInputHandler` and
`GUIInputHandler` implementations.
- Command: normalized operations (save, kill, yank, move, search,
etc.).
- View layer
- Renderer interface with `TerminalRenderer` and `GUIRenderer`
implementations.
- Editor: toplevel state managing buffers, messaging, and global
flags.
Performance and Reliability Targets
-----------------------------------
- Submillisecond keystroke to screen update on typical files in TUI.
- Sustain fluid editing on multimegabyte files; graceful degradation
on very large files.
- Atomic/safe writes; autosave and crashrecovery journals are
explicit and transparent.
Keybindings
-----------
kte maintains kes command model while internals evolve. Highlights (subject to refinement):
@@ -180,6 +101,15 @@ Run:
./cmake-build-debug/kte [files]
```
If you configured the GUI, you can also run the GUI-first target (when
built as `kge`) or request the GUI from `kte`:
```
./cmake-build-debug/kte --gui [files]
# or if built/installed as a separate GUI target
./cmake-build-debug/kge [files]
```
GUI build example
-----------------
@@ -194,30 +124,5 @@ cmake --build cmake-build-debug
Status
------
- The project is under active evolution toward the above architecture
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)
--------------------
1. Solidify core buffer model (gap buffer), file I/O, and
kecompatible commands.
2. Introduce structured undo/redo and search/replace with
highlighting.
3. Stabilize terminal renderer and input handling across common
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.
6. Language niceties (syntax highlighting, indentation rules) behind a
zerodeps, fast path.
7. Session restore, autosave/journaling, and safe write guarantees.
8. Extensibility hooks with a small, stable API.
References
----------
- [ke](https://git.wntrmute.dev/kyle/ke) manual and keybinding
reference: `ke.md`
- Inspirations: Antirez kilo, WordStar/VDE, Emacs, and `mg(1)`
- This project is a hobby text editor meant to be my personal editor. I
do not warrant its suitability for anyone else.

View File

@@ -1,116 +1,10 @@
kte ROADMAP — from skeleton to a working editor
ROADMAP / TODO:
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.cc/h` (ncurses).
- Rendering: `Renderer.h`, `TerminalRenderer.cc/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.
- [x] Search + Replace
- [x] Regex search + replace
- [ ] The undo system should actually work
- [x] Able to mark buffers as read-only
- [x] Built-in help text
- [x] Shorten paths in the homedir with ~
- [x] When the filename is longer than the message window, scoot left to
keep it in view

View File

@@ -1,10 +1,10 @@
#include <unistd.h>
#include <termios.h>
#include <ncurses.h>
#include <termios.h>
#include <unistd.h>
#include "Editor.h"
#include "Command.h"
#include "TerminalFrontend.h"
#include "Command.h"
#include "Editor.h"
bool

View File

@@ -4,10 +4,11 @@
#ifndef KTE_TERMINAL_FRONTEND_H
#define KTE_TERMINAL_FRONTEND_H
#include <termios.h>
#include "Frontend.h"
#include "TerminalInputHandler.h"
#include "TerminalRenderer.h"
#include <termios.h>
class TerminalFrontend final : public Frontend {

View File

@@ -1,8 +1,8 @@
#include <ncurses.h>
#include <cstdio>
#include <ncurses.h>
#include "KKeymap.h"
#include "TerminalInputHandler.h"
#include "KKeymap.h"
namespace {
constexpr int

View File

@@ -4,8 +4,6 @@
#ifndef KTE_TERMINAL_INPUT_HANDLER_H
#define KTE_TERMINAL_INPUT_HANDLER_H
#include <cstdint>
#include "InputHandler.h"

View File

@@ -1,19 +1,21 @@
#include "TerminalRenderer.h"
#include <ncurses.h>
#include <cstdio>
#include <string>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <cstdlib>
#include <ncurses.h>
#include <regex>
#include <string>
#include "Editor.h"
#include "TerminalRenderer.h"
#include "Buffer.h"
#include "Editor.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
# define KTE_VERSION_STR "dev"
#endif
TerminalRenderer::TerminalRenderer() = default;
TerminalRenderer::~TerminalRenderer() = default;
@@ -44,13 +46,54 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t li = rowoffs + static_cast<std::size_t>(r);
std::size_t render_col = 0;
std::size_t src_i = 0;
bool do_hl = ed.SearchActive() && li == ed.SearchMatchY() && ed.SearchMatchLen() > 0;
std::size_t mx = do_hl ? ed.SearchMatchX() : 0;
std::size_t mlen = do_hl ? ed.SearchMatchLen() : 0;
// Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
if (search_mode && li < lines.size()) {
std::string sline = static_cast<std::string>(lines[li]);
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
it != std::sregex_iterator(); ++it) {
const auto &m = *it;
std::size_t sx = static_cast<std::size_t>(m.position());
std::size_t ex = sx + static_cast<std::size_t>(m.length());
ranges.emplace_back(sx, ex);
}
} catch (const std::regex_error &) {
// ignore invalid patterns here; status shows error
}
} else {
const std::string &q = ed.SearchQuery();
std::size_t pos = 0;
while (!q.empty() && (pos = sline.find(q, pos)) != std::string::npos) {
ranges.emplace_back(pos, pos + q.size());
pos += q.size();
}
}
}
auto is_src_in_hl = [&](std::size_t si) -> bool {
if (ranges.empty()) return false;
// ranges are non-overlapping and ordered by construction
// linear scan is fine for now
for (const auto &rg : ranges) {
if (si < rg.first) break;
if (si >= rg.first && si < rg.second) return true;
}
return false;
};
// Track current-match to optionally emphasize
const bool has_current = ed.SearchActive() && ed.SearchMatchLen() > 0;
const std::size_t cur_mx = has_current ? ed.SearchMatchX() : 0;
const std::size_t cur_my = has_current ? ed.SearchMatchY() : 0;
const std::size_t cur_mend = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
bool hl_on = false;
bool cur_on = false;
int written = 0;
if (li < lines.size()) {
const std::string &line = lines[li];
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
while (written < cols) {
@@ -75,16 +118,16 @@ TerminalRenderer::Draw(Editor &ed)
}
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = do_hl && src_i >= mx && src_i < mx + mlen;
// highlight by source index
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl) attr |= A_STANDOUT;
if (in_cur) attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
addch(' ');
++written;
++render_col;
@@ -107,17 +150,12 @@ TerminalRenderer::Draw(Editor &ed)
ch = ' ';
from_src = false;
}
if (do_hl) {
bool in_hl = from_src && src_i >= mx && src_i < mx + mlen;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
@@ -131,6 +169,10 @@ TerminalRenderer::Draw(Editor &ed)
attroff(A_STANDOUT);
hl_on = false;
}
if (cur_on) {
attroff(A_BOLD);
cur_on = false;
}
clrtoeol();
}
@@ -149,7 +191,7 @@ TerminalRenderer::Draw(Editor &ed)
mvaddstr(0, 0, "[no buffer]");
}
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
// Status line (inverse)
move(rows - 1, 0);
attron(A_REVERSE);
@@ -157,6 +199,64 @@ TerminalRenderer::Draw(Editor &ed)
for (int i = 0; i < cols; ++i)
addch(' ');
// If a prompt is active, replace the status bar with the full prompt text
if (ed.PromptActive()) {
// Build prompt text: "Label: text" and shorten HOME path for file-related prompts
std::string label = ed.PromptLabel();
std::string ptext = ed.PromptText();
auto kind = ed.CurrentPromptKind();
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs ||
kind == Editor::PromptKind::Chdir) {
const char *home_c = std::getenv("HOME");
if (home_c && *home_c) {
std::string home(home_c);
// Ensure we match only at the start
if (ptext.rfind(home, 0) == 0) {
std::string rest = ptext.substr(home.size());
if (rest.empty())
ptext = "~";
else if (rest[0] == '/' || rest[0] == '\\')
ptext = std::string("~") + rest;
}
}
}
// Prefer keeping the tail of the filename visible when it exceeds the window
std::string msg;
if (!label.empty()) {
msg = label + ": ";
}
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
int avail = cols - static_cast<int>(msg.size());
if (avail <= 0) {
// No room for label; fall back to showing the rightmost portion of the whole string
std::string whole = msg + ptext;
if ((int)whole.size() > cols)
whole = whole.substr(whole.size() - cols);
msg = whole;
} else {
if ((int)ptext.size() > avail) {
ptext = ptext.substr(ptext.size() - avail);
}
msg += ptext;
}
} else {
// Non-file prompts: simple concatenation and clip by terminal
msg += ptext;
}
// Draw left-aligned, clipped to width
if (!msg.empty())
mvaddnstr(rows - 1, 0, msg.c_str(), std::max(0, cols));
// End status rendering for prompt mode
attroff(A_REVERSE);
// Restore logical cursor position in content area
if (saved_cur_y >= 0 && saved_cur_x >= 0)
move(saved_cur_y, saved_cur_x);
return;
}
// Build left segment
std::string left;
{
@@ -180,9 +280,24 @@ TerminalRenderer::Draw(Editor &ed)
fname = "[no name]";
}
left += " ";
// Insert buffer position prefix "[x/N] " before filename
{
std::size_t total = ed.BufferCount();
if (total > 0) {
std::size_t idx1 = ed.CurrentBufferIndex() + 1; // human-friendly 1-based
left += "[";
left += std::to_string(static_cast<unsigned long long>(idx1));
left += "/";
left += std::to_string(static_cast<unsigned long long>(total));
left += "] ";
}
}
left += fname;
if (b && b->Dirty())
left += " *";
// Append read-only indicator
if (b && b->IsReadOnly())
left += " [RO]";
// Append total line count as "<n>L"
if (b) {
unsigned long lcount = static_cast<unsigned long>(b->Rows().size());

View File

@@ -1,7 +1,6 @@
#include "TestFrontend.h"
#include "Editor.h"
#include "Command.h"
#include <iostream>
#include "Editor.h"
bool

View File

@@ -4,11 +4,12 @@
#ifndef KTE_TEST_INPUT_HANDLER_H
#define KTE_TEST_INPUT_HANDLER_H
#include "InputHandler.h"
#include <queue>
#include "InputHandler.h"
class TestInputHandler : public InputHandler {
class TestInputHandler final : public InputHandler {
public:
TestInputHandler() = default;
@@ -21,7 +22,7 @@ public:
void QueueText(const std::string &text);
bool IsEmpty() const
[[nodiscard]] bool IsEmpty() const
{
return queue_.empty();
}

View File

@@ -1,5 +1,4 @@
#include "TestRenderer.h"
#include "Editor.h"
void

View File

@@ -4,11 +4,12 @@
#ifndef KTE_TEST_RENDERER_H
#define KTE_TEST_RENDERER_H
#include "Renderer.h"
#include <cstddef>
#include "Renderer.h"
class TestRenderer : public Renderer {
class TestRenderer final : public Renderer {
public:
TestRenderer() = default;
@@ -17,7 +18,7 @@ public:
void Draw(Editor &ed) override;
std::size_t GetDrawCount() const
[[nodiscard]] std::size_t GetDrawCount() const
{
return draw_count_;
}

View File

@@ -1,12 +1,11 @@
#ifndef KTE_UNDONODE_H
#define KTE_UNDONODE_H
#include <cstddef>
#include <cstdint>
#include <string>
enum class UndoType : uint8_t {
enum class UndoType : std::uint8_t {
Insert,
Delete,
Paste,

View File

@@ -16,7 +16,7 @@ UndoSystem::Begin(UndoType type)
if (type == UndoType::Delete) {
// Support batching both forward deletes (DeleteChar) and backspace (prepend case)
// Forward delete: cursor stays at anchor col; expected == col
std::size_t anchor = static_cast<std::size_t>(tree_.pending->col);
const auto anchor = static_cast<std::size_t>(tree_.pending->col);
if (anchor + tree_.pending->text.size() == static_cast<std::size_t>(col)) {
pending_prepend_ = false;
return; // keep batching forward delete

View File

@@ -2,8 +2,10 @@
#define KTE_UNDOSYSTEM_H
#include <string_view>
#include "UndoTree.h"
class Buffer;
class UndoSystem {
@@ -39,7 +41,6 @@ private:
void update_dirty_flag();
private:
Buffer *buf_;
UndoTree &tree_;
// Internal hint for Delete batching: whether next Append() should prepend

View File

@@ -2,7 +2,7 @@
#define KTE_UNDOTREE_H
#include "UndoNode.h"
#include <memory>
struct UndoTree {
UndoNode *root = nullptr; // first edit ever

View File

@@ -6,6 +6,8 @@
<string>en</string>
<key>CFBundleExecutable</key>
<string>kge</string>
<key>CFBundleIconFile</key>
<string>@MACOSX_BUNDLE_ICON_FILE@</string>
<key>CFBundleIdentifier</key>
<string>@KGE_BUNDLE_ID@</string>
<key>CFBundleInfoDictionaryVersion</key>

View File

@@ -1,51 +0,0 @@
{
lib,
stdenv,
cmake,
ncurses,
SDL2,
libGL,
xorg,
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "kte";
inherit version;
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
ncurses
SDL2
libGL
xorg.libX11
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
cp kge $out/bin/
installManPage ../docs/kte.1
installManPage ../docs/kge.1
runHook postInstall
'';
}

View File

@@ -1,42 +0,0 @@
{
lib,
stdenv,
cmake,
ncurses,
installShellFiles,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
pname = "kte";
inherit version;
src = lib.cleanSource ./.;
nativeBuildInputs = [
cmake
ncurses
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=OFF"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
installManPage ../docs/kte.1
runHook postInstall
'';
}

View File

@@ -7,12 +7,16 @@
libGL,
xorg,
installShellFiles,
graphical ? false,
...
}:
let
cmakeContent = builtins.readFile ./CMakeLists.txt;
cmakeLines = lib.splitString "\n" cmakeContent;
versionLine = lib.findFirst (l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
versionLine = lib.findFirst (
l: builtins.match ".*set\\(KTE_VERSION \".+\"\\).*" l != null
) (throw "KTE_VERSION not found in CMakeLists.txt") cmakeLines;
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in
stdenv.mkDerivation {
@@ -24,14 +28,16 @@ stdenv.mkDerivation {
nativeBuildInputs = [
cmake
ncurses
installShellFiles
]
++ lib.optionals graphical [
SDL2
libGL
xorg.libX11
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
"-DCMAKE_BUILD_TYPE=Debug"
];
@@ -40,11 +46,17 @@ stdenv.mkDerivation {
mkdir -p $out/bin
cp kte $out/bin/
cp kge $out/bin/
installManPage ../docs/kte.1
installManPage ../docs/kge.1
''
+ lib.optionalString graphical ''
cp kge $out/bin/
installManPage ../docs/kge.1
mkdir -p $out/share/icons
cp ../kge.png $out/share/icons/
''
+ ''
runHook postInstall
'';
}

View File

@@ -17,8 +17,11 @@ kge \- Kyle's Graphical Editor (GUI-first)
is the GUI-first build target of Kyle's Text Editor. It shares the same
editor core and command model as
.BR kte (1),
but defaults to the graphical ImGui frontend when available. A terminal
(ncurses) frontend is also available and can be requested explicitly.
and defaults to the graphical ImGui frontend when available. A terminal
(ncurses) frontend is also available and can be requested explicitly with
.B --term
or by invoking
.BR kte (1).
If one or more
.I files
@@ -199,6 +202,8 @@ Open using the terminal frontend from kge:
.BR kte (1),
.I docs/ke.md
(project keybinding manual)
.br
Project homepage: https://github.com/wntrmute/kte
.SH BUGS
Report issues on the project tracker. Some behaviors are inherited from
ke and may evolve over time; see the manual for notes.

View File

@@ -16,8 +16,15 @@ kte \- Kyle's Text Editor (terminal-first)
.B kte
is a small, fast, and understandable text editor with a terminal-first
experience. It preserves ke's WordStar/VDE-style command model with
Emacs-influenced ergonomics. The core uses ncurses in the terminal and can
optionally run with a GUI frontend if built.
Emacs-influenced ergonomics. The core uses ncurses in the terminal.
By default, .B kte
runs in the terminal (ncurses) frontend. If the binary was built with GUI
support, the same editor core can be shown with an ImGui-based GUI by passing
.B --gui
or by invoking the GUI-first target
.BR kge (1)
when available.
If one or more
.I files
@@ -194,6 +201,8 @@ Force GUI frontend (if available):
.BR kge (1),
.I docs/ke.md
(project keybinding manual)
.br
Project homepage: https://github.com/wntrmute/kte
.SH BUGS
Incremental search currently restarts from the top on each invocation; see
\(lqKnown behavior\(rq in the ke manual. Report issues on the project tracker.

BIN
docs/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

View File

@@ -1,55 +0,0 @@
# flake.nix
{
description = "kte ImGui/SDL2 text editor";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.default = pkgs.stdenv.mkDerivation {
pname = "kte";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
buildInputs = with pkgs; [
ncurses
SDL2
libGL
xorg.libX11
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCURSES_NEED_NCURSES=TRUE"
"-DCURSES_NEED_WIDE=TRUE"
];
# Alternative (even stronger): completely hide the broken module
preConfigure = ''
# If the project ships its own FindSDL2.cmake in cmake/, hide it
if [ -f cmake/FindSDL2.cmake ]; then
mv cmake/FindSDL2.cmake cmake/FindSDL2.cmake.disabled
echo "Disabled bundled FindSDL2.cmake"
fi
'';
meta = with pkgs.lib; {
description = "kte ImGui/SDL2 GUI editor";
mainProgram = "kte";
platforms = platforms.linux;
};
};
devShells.default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.default ];
packages = with pkgs; [ gdb clang-tools ];
};
});
}

10
flake.lock generated
View File

@@ -2,15 +2,15 @@
"nodes": {
"nixpkgs": {
"locked": {
"lastModified": 1764242076,
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
"owner": "nixos",
"lastModified": 1764517877,
"narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c",
"type": "github"
},
"original": {
"owner": "nixos",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"

View File

@@ -1,21 +1,20 @@
{
description = "Kyle's Text Editor";
description = "kyle's text editor";
inputs = {
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs =
{ self, nixpkgs }:
inputs@{ self, nixpkgs, ... }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
pkgsFor = system: import nixpkgs { inherit system; };
in
{
packages.x86_64-linux = {
default = pkgs.callPackage ./default-nogui.nix { };
kge = pkgs.callPackage ./default-gui.nix { };
kte = pkgs.callPackage ./default-nogui.nix { };
full = pkgs.callPackage ./default.nix { };
};
packages = eachSystem (system: rec {
default = kte;
full = kge;
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
});
};
}

BIN
kge.icns Normal file

Binary file not shown.

Binary file not shown.

After

Width:  |  Height:  |  Size: 33 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

BIN
kge.iconset/icon_16x16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 911 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

BIN
kge.iconset/icon_32x32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 483 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

BIN
kge.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

13
main.cc
View File

@@ -1,15 +1,15 @@
#include <cctype>
#include <cstdio>
#include <getopt.h>
#include <iostream>
#include <memory>
#include <string>
#include <cctype>
#include <unistd.h>
#include <getopt.h>
#include <signal.h>
#include <cstdio>
#include <string>
#include <unistd.h>
#include <sys/stat.h>
#include "Editor.h"
#include "Command.h"
#include "Editor.h"
#include "Frontend.h"
#include "TerminalFrontend.h"
@@ -22,6 +22,7 @@
# define KTE_VERSION_STR "devel"
#endif
static void
PrintUsage(const char *prog)
{

View File

@@ -1,10 +1,11 @@
#include <iostream>
#include <cassert>
#include <fstream>
#include <iostream>
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
#include "TestFrontend.h"
#include "Command.h"
#include "Buffer.h"
int