29 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
ba9bd4a27d Bump version.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-30 17:22:54 -08:00
fcb2e9a7ed Add horizontal scrolling support and refactor mouse click handling in GUI.
- Introduce horizontal scrolling with column offset synchronization in GUI.
- Refactor mouse click handling for improved accuracy and viewport alignment.
- Enhance tab expansion and cursor rendering logic for better user experience.
- Replace redundant variable declarations in `Buffer` for cleaner code.
2025-11-30 17:19:46 -08:00
38ba8c9871 Refactor code for consistency and enhanced functionality.
- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.
- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.
- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.
- Refine keybindings and enhance existing commands for improved command flow.
- Adjust GUI and terminal renderers to display total line counts alongside filenames.
- Update coding style to align with project guidelines.
2025-11-30 16:49:24 -08:00
b91406860c update nix build
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-30 16:07:30 -08:00
8d1e9b2799 default to using the piece table 2025-11-30 10:35:28 -08:00
c91fe214d6 building nix 2025-11-30 04:46:10 -08:00
99042f5ef1 update nix build 2025-11-30 04:42:21 -08:00
96242154f7 build non-gui by default 2025-11-30 04:34:15 -08:00
f34e88c490 revert downwards
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
2025-11-30 04:29:09 -08:00
b8942b9804 Add GUI initialization updates and improve navigation commands.
- Implement terminal detachment for GUI mode to enable terminal closure post-launch.
- Add `+N` support for opening files at specific line numbers and refine cursor positioning.
- Introduce `JumpToLine` command for direct navigation by line number.
- Enhance mouse wheel handling for line-wise scrolling.
2025-11-30 04:28:40 -08:00
65869bd143 Add man pages for kge and kte with installation targets in CMake.
- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.
- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.
- Ensure `kge` man page installation is conditional on GUI being built.
2025-11-30 03:34:37 -08:00
66 changed files with 3599 additions and 1040 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: []

View File

@@ -17,7 +17,6 @@
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/KEEP_BLANK_LINES_IN_DECLARATIONS/@EntryValue" value="2" type="int" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/KEEP_BLANK_LINES_IN_CODE/@EntryValue" value="2" type="int" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/KEEP_USER_LINEBREAKS/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/INDENT_CASE_FROM_SWITCH/@EntryValue" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/INDENT_COMMENT/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/INT_ALIGN_EQ/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/SIMPLE_BLOCK_STYLE/@EntryValue" value="LINE_BREAK" type="string" />
@@ -115,33 +114,18 @@
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/TOPLEVEL_FUNCTION_DECLARATION_RETURN_TYPE_STYLE/@EntryValue" value="ON_SINGLE_LINE" type="string" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/FUNCTION_DECLARATION_RETURN_TYPE_STYLE/@EntryValue" value="ON_SINGLE_LINE" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=0B82708A1BA7774EB13D27F245698A56/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;1&quot; Title=&quot;Classes and structs&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;__interface&quot; /&gt;&lt;type Name=&quot;class&quot; /&gt;&lt;type Name=&quot;struct&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=0B82708A1BA7774EB13D27F245698A56/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=A7EBF16DA3BDCB42A0B710704BC8A053/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;3&quot; Title=&quot;Enums&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;enum&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=A7EBF16DA3BDCB42A0B710704BC8A053/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=0AFB7787612DF743B09AD9412E48D4CC/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;7&quot; Title=&quot;Local variables&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;local variable&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;aaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=0AFB7787612DF743B09AD9412E48D4CC/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=72514D5DF422D442B71A277F97B72887/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;8&quot; Title=&quot;Global variables&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;global variable&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AA_BB&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=72514D5DF422D442B71A277F97B72887/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=821F3C5CF47D5640AD3511BCBADE17C4/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;9&quot; Title=&quot;Lambdas&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;lambda&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=821F3C5CF47D5640AD3511BCBADE17C4/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=8F69F48E2532F54CBAA0039D4557825E/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;10&quot; Title=&quot;Global functions&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;global function&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=8F69F48E2532F54CBAA0039D4557825E/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=B6E900853D6D05429D8C57765B2E546A/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;11&quot; Title=&quot;Class and struct methods&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;member function&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=B6E900853D6D05429D8C57765B2E546A/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=B82A063F0DDD98498A70D8D7EBB97F8D/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;12&quot; Title=&quot;Class and struct fields&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;class field&quot; /&gt;&lt;type Name=&quot;struct field&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;_&quot; Style=&quot;aaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=B82A063F0DDD98498A70D8D7EBB97F8D/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=BBE8AA08E662BF409B2CB08EC597C493/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;13&quot; Title=&quot;Class and struct public fields&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;PUBLIC&quot;&gt;&lt;type Name=&quot;class field&quot; /&gt;&lt;type Name=&quot;struct field&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;aaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=BBE8AA08E662BF409B2CB08EC597C493/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=A4FAA2257682A94F8C2C93E123FAFC7A/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;19&quot; Title=&quot;Typedefs&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;Indeterminate&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;type alias&quot; /&gt;&lt;type Name=&quot;typedef&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=A4FAA2257682A94F8C2C93E123FAFC7A/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=BF0D1AE66D64FE4FAF613448A12051A0/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;17&quot; Title=&quot;Global constants&quot;&gt;&lt;Descriptor Static=&quot;Indeterminate&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;True&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;global variable&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=BF0D1AE66D64FE4FAF613448A12051A0/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=2B232F1067F0324F8FF4B9D63ACECDB2/@EntryIndexedValue" value="&lt;NamingElement Priority=&quot;16&quot; Title=&quot;Other constants&quot;&gt;&lt;Descriptor Static=&quot;True&quot; Constexpr=&quot;Indeterminate&quot; Const=&quot;True&quot; Volatile=&quot;Indeterminate&quot; Accessibility=&quot;NOT_APPLICABLE&quot;&gt;&lt;type Name=&quot;class field&quot; /&gt;&lt;type Name=&quot;local variable&quot; /&gt;&lt;type Name=&quot;struct field&quot; /&gt;&lt;/Descriptor&gt;&lt;Policy Inspect=&quot;True&quot; WarnAboutPrefixesAndSuffixes=&quot;False&quot; Prefix=&quot;&quot; Suffix=&quot;&quot; Style=&quot;AaBb&quot; /&gt;&lt;/NamingElement&gt;" type="string" />
<option name="/Default/CodeStyle/Naming/CppNamingOptions/Rules/=2B232F1067F0324F8FF4B9D63ACECDB2/@EntryIndexRemoved" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/REMOVE_BLANK_LINES_NEAR_BRACES_IN_CODE/@EntryValue" />
<option name="/Default/CodeStyle/CppIncludeDirective/SortIncludeDirectives/@EntryValue" value="true" type="bool" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/USE_CONTINUOUS_LINE_INDENT_IN_METHOD_PARS/@EntryValue" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/USE_CONTINUOUS_LINE_INDENT_IN_EXPRESSION_BRACES/@EntryValue" />
<option name="/Default/CodeStyle/CodeFormatting/CppFormatting/INDENT_GOTO_LABELS/@EntryValue" value="false" type="bool" />
</RiderCodeStyleSettings>
<files>

213
.idea/workspace.xml generated
View File

@@ -11,9 +11,9 @@
<option name="/Default/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
</component>
<component name="CMakePresetLoader"><![CDATA[{
"useNewFormat": true
}]]></component>
<component name="CMakePresetLoader">{
&quot;useNewFormat&quot;: true
}</component>
<component name="CMakeProjectFlavorService">
<option name="flavorId" value="CMakePlainProjectFlavor" />
</component>
@@ -33,8 +33,20 @@
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`.">
<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$/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" />
@@ -49,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>
@@ -56,27 +74,22 @@
<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" />
<option name="minorVersion" value="2.5" />
<option name="productBranch" value="Classic" />
</component>
<component name="ProjectColorInfo"><![CDATA[{
"associatedIndex": 3
}]]></component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
}</component>
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
<component name="ProjectLevelVcsManager">
<OptionsSetting value="false" id="Update" />
@@ -87,48 +100,49 @@
<option name="sortByType" value="true" />
<option name="sortKey" value="BY_TYPE" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"CMake Application.kge.executor": "Run",
"CMake Application.test_example.executor": "Run",
"CMake Application.test_undo.executor": "Run",
"ModuleVcsDetector.initialDetectionPerformed": "true",
"NIXITCH_NIXPKGS_CONFIG": "",
"NIXITCH_NIX_CONF_DIR": "",
"NIXITCH_NIX_OTHER_STORES": "",
"NIXITCH_NIX_PATH": "",
"NIXITCH_NIX_PROFILES": "",
"NIXITCH_NIX_REMOTE": "",
"NIXITCH_NIX_USER_PROFILE_DIR": "",
"RunOnceActivity.RadMigrateCodeStyle": "true",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.cidr.known.project.marker": "true",
"RunOnceActivity.readMode.enableVisualFormatting": "true",
"RunOnceActivity.west.config.association.type.startup.service": "true",
"cf.first.check.clang-format": "false",
"cidr.known.project.marker": "true",
"code.cleanup.on.save": "true",
"com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1": "true",
"git-widget-placeholder": "master",
"junie.onboarding.icon.badge.shown": "true",
"node.js.detected.package.eslint": "true",
"node.js.detected.package.tslint": "true",
"node.js.selected.package.eslint": "(autodetect)",
"node.js.selected.package.tslint": "(autodetect)",
"nodejs_package_manager_path": "npm",
"onboarding.tips.debug.path": "/Users/kyle/src/kte/main.cpp",
"rearrange.code.on.save": "true",
"settings.editor.selected.configurable": "junie.application.models",
"to.speed.mode.migration.done": "true",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;CMake Application.kge.executor&quot;: &quot;Run&quot;,
&quot;CMake Application.test_example.executor&quot;: &quot;Run&quot;,
&quot;CMake Application.test_undo.executor&quot;: &quot;Run&quot;,
&quot;ModuleVcsDetector.initialDetectionPerformed&quot;: &quot;true&quot;,
&quot;NIXITCH_NIXPKGS_CONFIG&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_CONF_DIR&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_OTHER_STORES&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_PATH&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_PROFILES&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_REMOTE&quot;: &quot;&quot;,
&quot;NIXITCH_NIX_USER_PROFILE_DIR&quot;: &quot;&quot;,
&quot;RunOnceActivity.RadMigrateCodeStyle&quot;: &quot;true&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;RunOnceActivity.readMode.enableVisualFormatting&quot;: &quot;true&quot;,
&quot;RunOnceActivity.west.config.association.type.startup.service&quot;: &quot;true&quot;,
&quot;cf.first.check.clang-format&quot;: &quot;false&quot;,
&quot;cidr.known.project.marker&quot;: &quot;true&quot;,
&quot;code.cleanup.on.save&quot;: &quot;true&quot;,
&quot;com.intellij.ml.llm.matterhorn.ej.ui.settings.DefaultModelSelectionForGA.v1&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;master&quot;,
&quot;junie.onboarding.icon.badge.shown&quot;: &quot;true&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;onboarding.tips.debug.path&quot;: &quot;/Users/kyle/src/kte/main.cpp&quot;,
&quot;rearrange.code.on.save&quot;: &quot;true&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;editor.preferences.fonts.default&quot;,
&quot;to.speed.mode.migration.done&quot;: &quot;true&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/docs" />
</key>
</component>
<component name="RunManager" selected="CMake Application.imgui">
<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" />
@@ -139,7 +153,7 @@
<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" 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>
@@ -162,7 +176,15 @@
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1764457173148</updated>
<workItem from="1764457174208" duration="42867000" />
<workItem from="1764457174208" duration="46950000" />
<workItem from="1764538560497" duration="215000" />
<workItem from="1764539255906" duration="196000" />
<workItem from="1764539459951" duration="64000" />
<workItem from="1764539535105" duration="10000" />
<workItem from="1764539556448" duration="156000" />
<workItem from="1764539725338" duration="1075000" />
<workItem from="1764542392763" duration="3512000" />
<workItem from="1764548345516" duration="28341000" />
</task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" />
@@ -220,7 +242,79 @@
<option name="project" value="LOCAL" />
<updated>1764501532446</updated>
</task>
<option name="localTasksCounter" value="8" />
<task id="LOCAL-00008" summary="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.">
<option name="closed" value="true" />
<created>1764502480274</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1764502480274</updated>
</task>
<task id="LOCAL-00009" summary="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.">
<option name="closed" value="true" />
<created>1764505723411</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1764505723411</updated>
</task>
<task id="LOCAL-00010" summary="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="closed" value="true" />
<created>1764550164829</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1764550164829</updated>
</task>
<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">
@@ -241,7 +335,16 @@
<MESSAGE value="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes." />
<MESSAGE value="Add `TestFrontend` documentation and `UndoSystem` buffer reference update.&#10;&#10;- Document `TestFrontend` for programmatic testing, including examples and usage details.&#10;- Add `UpdateBufferReference` to `UndoSystem` to support updating buffer associations." />
<MESSAGE value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." />
<option name="LAST_COMMIT_MESSAGE" value="Remove `packaging.cmake`, deprecate `test_undo` setup, and add new testing infrastructure.&#10;&#10;- Delete `packaging.cmake` to streamline build system.&#10;- Deprecate `test_undo` in CMake setup; condition builds on `BUILD_TESTS`.&#10;- Introduce `TestFrontend`, `TestRenderer`, and `TestInputHandler` for structured testing.&#10;- Update `GUIInputHandler` and `Command` for enhanced buffer save handling and overwrite confirmation.&#10;- Enhance kill ring operations and new prompt workflows in `Editor`." />
<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." />
<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,39 +1,19 @@
# Project Guidelines
kte is Kyle's Text Editor — a simple, fast text editor written in C++17. It
replaces the earlier C implementation, ke (see the ke manual in `ke.md`). The
replaces the earlier C implementation, ke (see the ke manual in `docs/ke.md`). The
design draws inspiration from Antirez' kilo, with keybindings rooted in the
WordStar/VDE family and emacs. The spiritual parent is `mg(1)`.
These guidelines summarize the goals, interfaces, key operations, and current
development practices for kte.
Style note: all code should be formatted with the current CLion C++ style.
## Goals
- Keep the core small, fast, and understandable.
- Provide an ncurses-based terminal-first editing experience, with an optional ImGui GUI.
- Provide an ncurses-based terminal-first editing experience, with an additional ImGui GUI.
- Preserve familiar keybindings from ke while modernizing the internals.
- Favor simple data structures (e.g., gap buffer) and incremental evolution.
## Interfaces
- Command-line interface: the primary interface today.
- GUI: planned ImGui-based interface.
## Build and Run
Prerequisites: a C++17 compiler, CMake, and ncurses development headers/libs.
- macOS (Homebrew): `brew install ncurses`
- Debian/Ubuntu: `sudo apt-get install libncurses5-dev libncursesw5-dev`
- Configure and build (example):
- `cmake -S . -B cmake-build-debug -DCMAKE_BUILD_TYPE=Debug`
- `cmake --build cmake-build-debug`
- Run:
- `./cmake-build-debug/kte [files]`
- Favor simple data structures (e.g., piece table) and incremental evolution.
Project entry point: `main.cpp`
@@ -52,31 +32,12 @@ Project entry point: `main.cpp`
## Keybindings (inherited from ke)
kte aims to maintain kes command model while internals evolve. See `ke.md` for
the full reference. Highlights:
- K-command prefix: `C-k` enters k-command mode; exit with `ESC` or `C-g`.
- Save/Exit: `C-k s` (save), `C-k x` or `C-k C-x` (save and exit), `C-k q` (quit
with confirm), `C-k C-q` (quit immediately).
- Editing: `C-k d` (kill to EOL), `C-k C-d` (kill line), `C-k BACKSPACE` (kill
to BOL), `C-w` (kill region), `C-y` (yank), `C-u` (universal argument).
- Navigation/Search: `C-s` (incremental find), `C-r` (regex search), `ESC f/b`
(word next/prev), `ESC BACKSPACE` (delete previous word).
- Buffers/Files: `C-k e` (open), `C-k b`/`C-k p` (switch), `C-k c` (close),
`C-k C-r` (reload).
- Misc: `C-l` (refresh), `C-g` (cancel), `C-k m` (run make), `C-k g` (goto line).
Known behavior from ke retained for now:
- Incremental search navigates results with arrow keys; search restarts from
the top on each invocation (known bug to be revisited).
The file `docs/ke.md` contains the canonical reference for keybindings.
## Contributing/Development Notes
- C++ standard: C++17.
- Style: match existing file formatting and minimal-comment style.
- Keep dependencies minimal; ImGui integration will be isolated behind a GUI
module.
- Keep dependencies minimal.
- Prefer small, focused changes that preserve kes UX unless explicitly changing
behavior.

117
Buffer.cc
View File

@@ -1,10 +1,11 @@
#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()
@@ -35,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_;
@@ -59,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_;
@@ -81,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_),
@@ -111,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_;
@@ -129,12 +134,36 @@ Buffer::operator=(Buffer &&other) noexcept
bool
Buffer::OpenFromFile(const std::string &path, std::string &err)
{
auto normalize_path = [](const std::string &in) -> std::string {
std::string expanded = in;
// Expand leading '~' to HOME
if (!expanded.empty() && expanded[0] == '~') {
const char *home = std::getenv("HOME");
if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\')) {
expanded = std::string(home) + expanded.substr(1);
} else if (home && expanded.size() == 1) {
expanded = std::string(home);
}
}
try {
std::filesystem::path p(expanded);
if (std::filesystem::exists(p)) {
return std::filesystem::canonical(p).string();
}
return std::filesystem::absolute(p).string();
} catch (...) {
// On any error, fall back to input
return expanded;
}
};
const std::string norm = normalize_path(path);
// If the file doesn't exist, initialize an empty, non-file-backed buffer
// with the provided filename. Do not touch the filesystem until Save/SaveAs.
if (!std::filesystem::exists(path)) {
if (!std::filesystem::exists(norm)) {
rows_.clear();
nrows_ = 0;
filename_ = path;
filename_ = norm;
is_file_backed_ = false;
dirty_ = false;
@@ -147,9 +176,9 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
return true;
}
std::ifstream in(path, std::ios::in | std::ios::binary);
std::ifstream in(norm, std::ios::in | std::ios::binary);
if (!in) {
err = "Failed to open file: " + path;
err = "Failed to open file: " + norm;
return false;
}
@@ -194,7 +223,7 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
}
nrows_ = rows_.size();
filename_ = path;
filename_ = norm;
is_file_backed_ = true;
dirty_ = false;
@@ -250,10 +279,29 @@ Buffer::Save(std::string &err) const
bool
Buffer::SaveAs(const std::string &path, std::string &err)
{
// Normalize output path first
std::string out_path;
try {
std::filesystem::path p(path);
// Do a light expansion of '~'
std::string expanded = path;
if (!expanded.empty() && expanded[0] == '~') {
const char *home = std::getenv("HOME");
if (home && expanded.size() >= 2 && (expanded[1] == '/' || expanded[1] == '\\'))
expanded = std::string(home) + expanded.substr(1);
else if (home && expanded.size() == 1)
expanded = std::string(home);
}
std::filesystem::path ep(expanded);
out_path = std::filesystem::absolute(ep).string();
} catch (...) {
out_path = path;
}
// Write to the given path
std::ofstream out(path, std::ios::out | std::ios::binary | std::ios::trunc);
std::ofstream out(out_path, std::ios::out | std::ios::binary | std::ios::trunc);
if (!out) {
err = "Failed to open for write: " + path;
err = "Failed to open for write: " + out_path;
return false;
}
for (std::size_t i = 0; i < rows_.size(); ++i) {
@@ -270,7 +318,7 @@ Buffer::SaveAs(const std::string &path, std::string &err)
return false;
}
filename_ = path;
filename_ = out_path;
is_file_backed_ = true;
dirty_ = false;
return true;
@@ -303,8 +351,8 @@ Buffer::insert_text(int row, int col, std::string_view text)
if (static_cast<std::size_t>(row) >= rows_.size())
rows_.emplace_back("");
std::size_t y = static_cast<std::size_t>(row);
std::size_t x = static_cast<std::size_t>(col);
auto y = static_cast<std::size_t>(row);
auto x = static_cast<std::size_t>(col);
if (x > rows_[y].size())
x = rows_[y].size();
@@ -322,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);
@@ -340,13 +388,13 @@ Buffer::delete_text(int row, int col, std::size_t len)
row = 0;
if (static_cast<std::size_t>(row) >= rows_.size())
return;
std::size_t y = static_cast<std::size_t>(row);
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
const auto y = static_cast<std::size_t>(row);
const auto x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
std::size_t remaining = len;
while (remaining > 0 && y < rows_.size()) {
auto &line = rows_[y];
std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
const std::size_t in_line = std::min<std::size_t>(remaining, line.size() - std::min(x, line.size()));
if (x < line.size() && in_line > 0) {
line.erase(x, in_line);
remaining -= in_line;
@@ -370,41 +418,48 @@ Buffer::delete_text(int row, int col, std::size_t len)
void
Buffer::split_line(int row, int col)
Buffer::split_line(int row, const int col)
{
if (row < 0)
if (row < 0) {
row = 0;
if (static_cast<std::size_t>(row) >= rows_.size())
}
if (static_cast<std::size_t>(row) >= rows_.size()) {
rows_.resize(static_cast<std::size_t>(row) + 1);
std::size_t y = static_cast<std::size_t>(row);
std::size_t x = std::min<std::size_t>(static_cast<std::size_t>(col), rows_[y].size());
std::string tail = rows_[y].substr(x);
}
const auto y = static_cast<std::size_t>(row);
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));
}
void
Buffer::join_lines(int row)
{
if (row < 0)
if (row < 0) {
row = 0;
std::size_t y = static_cast<std::size_t>(row);
if (y + 1 >= rows_.size())
}
const auto y = static_cast<std::size_t>(row);
if (y + 1 >= rows_.size()) {
return;
}
rows_[y] += rows_[y + 1];
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(y + 1));
}
void
Buffer::insert_row(int row, std::string_view text)
Buffer::insert_row(int row, const std::string_view text)
{
if (row < 0)
row = 0;
if (static_cast<std::size_t>(row) > rows_.size())
row = static_cast<int>(rows_.size());
rows_.insert(rows_.begin() + static_cast<std::ptrdiff_t>(row), std::string(text));
rows_.insert(rows_.begin() + row, Line(std::string(text)));
}
@@ -415,7 +470,7 @@ Buffer::delete_row(int row)
row = 0;
if (static_cast<std::size_t>(row) >= rows_.size())
return;
rows_.erase(rows_.begin() + static_cast<std::ptrdiff_t>(row));
rows_.erase(rows_.begin() + row);
}

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.1.0")
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" OFF)
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.")
@@ -44,6 +45,8 @@ if (${BUILD_GUI})
endif ()
# NCurses for terminal mode
set(CURSES_NEED_NCURSES)
set(CURSES_NEED_WIDE)
find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR})
@@ -53,6 +56,7 @@ set(COMMON_SOURCES
Buffer.cc
Editor.cc
Command.cc
HelpText.cc
KKeymap.cc
TerminalInputHandler.cc
TerminalRenderer.cc
@@ -72,6 +76,7 @@ set(COMMON_HEADERS
Editor.h
AppendBuffer.h
Command.h
HelpText.h
KKeymap.h
InputHandler.h
TerminalInputHandler.h
@@ -97,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})
@@ -104,6 +112,9 @@ install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
# Man pages
install(FILES docs/kte.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
if (BUILD_TESTS)
# test_undo executable for testing undo/redo system
add_executable(test_undo
@@ -116,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 ()
@@ -123,6 +138,8 @@ endif ()
if (${BUILD_GUI})
target_sources(kte PRIVATE
Font.h
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -137,6 +154,8 @@ if (${BUILD_GUI})
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -144,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(
@@ -159,14 +190,29 @@ 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}
)
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 ()

File diff suppressed because it is too large Load Diff

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,9 +73,17 @@ 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)
// Direct navigation by line number
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,7 +1,8 @@
#include "Editor.h"
#include <algorithm>
#include <utility>
#include <filesystem>
#include "Editor.h"
Editor::Editor() = default;
@@ -43,6 +44,78 @@ Editor::CurrentBuffer() const
}
static std::vector<std::filesystem::path>
split_reverse(const std::filesystem::path &p)
{
std::vector<std::filesystem::path> parts;
for (auto it = p; !it.empty(); it = it.parent_path()) {
if (it == it.parent_path()) {
// root or single element
if (!it.empty())
parts.push_back(it);
break;
}
parts.push_back(it.filename());
}
return parts; // from leaf toward root
}
std::string
Editor::DisplayNameFor(const Buffer &buf) const
{
std::string full = buf.Filename();
if (full.empty())
return std::string("[no name]");
std::filesystem::path target(full);
auto target_parts = split_reverse(target);
if (target_parts.empty())
return target.filename().string();
// Prepare list of other buffer paths
std::vector<std::vector<std::filesystem::path> > others;
others.reserve(buffers_.size());
for (const auto &b: buffers_) {
if (&b == &buf)
continue;
if (b.Filename().empty())
continue;
others.push_back(split_reverse(std::filesystem::path(b.Filename())));
}
// Increase suffix length until unique among others
std::size_t need = 1; // at least basename
for (;;) {
// Build candidate suffix for target
std::filesystem::path cand;
for (std::size_t i = 0; i < need && i < target_parts.size(); ++i) {
cand = std::filesystem::path(target_parts[i]) / cand;
}
// Compare against others
bool clash = false;
for (const auto &o_parts: others) {
std::filesystem::path ocand;
for (std::size_t i = 0; i < need && i < o_parts.size(); ++i) {
ocand = std::filesystem::path(o_parts[i]) / ocand;
}
if (ocand == cand) {
clash = true;
break;
}
}
if (!clash || need >= target_parts.size()) {
std::string s = cand.string();
// Remove any trailing slash that may appear from root joining
if (!s.empty() && (s.back() == '/' || s.back() == '\\'))
s.pop_back();
return s;
}
++need;
}
}
std::size_t
Editor::AddBuffer(const Buffer &buf)
{

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 };
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)
@@ -409,6 +423,11 @@ public:
const Buffer *CurrentBuffer() const;
// Compute a display-friendly short name for a buffer path that is the
// shortest unique suffix among all open buffers. If buffer has no name,
// returns "[no name]".
[[nodiscard]] std::string DisplayNameFor(const Buffer &buf) const;
// Add an existing buffer (copy/move) or open from file path
std::size_t AddBuffer(const Buffer &buf);
@@ -436,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;
@@ -473,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"
@@ -279,6 +280,24 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
MappedInput mi;
bool produced = false;
switch (e.type) {
case SDL_MOUSEWHEEL: {
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
dy = -dy;
#endif
if (dy != 0) {
int repeat = dy > 0 ? dy : -dy;
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown;
std::lock_guard<std::mutex> lk(mu_);
for (int i = 0; i < repeat; ++i) {
q_.push(MappedInput{true, id, std::string(), 0});
}
return true; // consumed
}
return false;
}
case SDL_KEYDOWN: {
// Remember state before mapping; used for TEXTINPUT suppression heuristics
const bool was_k_prefix = k_prefix_;

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,27 +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 <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
@@ -45,7 +65,7 @@ GUIRenderer::Draw(Editor &ed)
const auto &lines = buf->Rows();
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar);
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Detect click-to-move inside this scroll region
ImVec2 list_origin = ImGui::GetCursorScreenPos();
float scroll_y = ImGui::GetScrollY();
@@ -56,24 +76,57 @@ GUIRenderer::Draw(Editor &ed)
const float line_h = ImGui::GetTextLineHeight();
const float row_h = ImGui::GetTextLineHeightWithSpacing();
const float space_w = ImGui::CalcTextSize(" ").x;
// If the command layer requested a specific top-of-screen (via Buffer::Rowoffs),
// force the ImGui scroll to match so paging aligns the first visible row.
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
// This prevents clicks/wheel from being immediately overridden by stale offsets.
bool forced_scroll = false;
{
std::size_t desired_top = buf->Rowoffs();
long current_top = static_cast<long>(scroll_y / row_h);
if (static_cast<long>(desired_top) != current_top) {
ImGui::SetScrollY(static_cast<float>(desired_top) * row_h);
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
const long buf_coloffs = static_cast<long>(buf->Coloffs());
const long scroll_top = static_cast<long>(scroll_y / row_h);
const long scroll_left = static_cast<long>(scroll_x / space_w);
// Detect programmatic change (e.g., keyboard navigation ensured visibility)
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
ImGui::SetScrollY(static_cast<float>(buf_rowoffs) * row_h);
scroll_y = ImGui::GetScrollY();
forced_scroll = true;
}
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
ImGui::SetScrollX(static_cast<float>(buf_coloffs) * space_w);
scroll_x = ImGui::GetScrollX();
forced_scroll = true;
}
// If user scrolled, update buffer offsets accordingly
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
mbuf->Coloffs());
}
}
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left)));
}
}
// Update trackers for next frame
prev_buf_rowoffs = static_cast<long>(buf->Rowoffs());
prev_buf_coloffs = static_cast<long>(buf->Coloffs());
prev_scroll_y = ImGui::GetScrollY();
prev_scroll_x = ImGui::GetScrollX();
}
// Synchronize cursor and scrolling.
// A) When the user scrolls and the cursor goes off-screen, move the cursor to the nearest visible row.
// B) When the cursor moves (via keyboard commands), scroll it back into view.
// Ensure the cursor is visible even on the first frame or when it didn't move,
// unless we already forced scrolling from Buffer::Rowoffs this frame.
{
static float prev_scroll_y = -1.0f;
static long prev_cursor_y = -1;
// Compute visible row range using the child window height
float child_h = ImGui::GetWindowHeight();
long first_row = static_cast<long>(scroll_y / row_h);
@@ -82,37 +135,7 @@ GUIRenderer::Draw(Editor &ed)
vis_rows = 1;
long last_row = first_row + vis_rows - 1;
// A) If user scrolled (scroll_y changed), and cursor outside, move cursor to nearest visible row
// Skip this when we just forced a scroll alignment this frame (programmatic change).
if (!forced_scroll && prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
long new_row = (cyr < first_row) ? first_row : last_row;
if (new_row < 0)
new_row = 0;
if (new_row >= static_cast<long>(lines.size()))
new_row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
// Clamp column to line length
std::size_t new_col = 0;
if (!lines.empty()) {
const std::string &l = lines[static_cast<std::size_t>(new_row)];
new_col = std::min<std::size_t>(cx, l.size());
}
char tmp2[64];
std::snprintf(tmp2, sizeof(tmp2), "%ld:%zu", new_row, new_col);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp2));
cy = buf->Cury();
cx = buf->Curx();
cyr = static_cast<long>(cy);
// Update visible range again in case content changed
first_row = static_cast<long>(ImGui::GetScrollY() / row_h);
last_row = first_row + vis_rows - 1;
}
}
// B) If cursor moved since last frame and is outside the visible region, scroll to reveal it
// Skip this when we just forced a top-of-screen alignment this frame.
if (!forced_scroll && prev_cursor_y >= 0 && static_cast<long>(cy) != prev_cursor_y) {
if (!forced_scroll) {
long cyr = static_cast<long>(cy);
if (cyr < first_row || cyr > last_row) {
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
@@ -128,68 +151,197 @@ GUIRenderer::Draw(Editor &ed)
last_row = first_row + vis_rows - 1;
}
}
prev_scroll_y = ImGui::GetScrollY();
prev_cursor_y = static_cast<long>(cy);
}
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
// Map Y to row
float rel_y = scroll_y + (mp.y - list_origin.y);
long row = static_cast<long>(rel_y / row_h);
if (row < 0)
row = 0;
if (row >= static_cast<long>(lines.size()))
row = static_cast<long>(lines.empty() ? 0 : (lines.size() - 1));
// Map X to column by measuring text width
std::size_t col = 0;
if (!lines.empty()) {
const std::string &line = lines[static_cast<std::size_t>(row)];
float rel_x = scroll_x + (mp.x - list_origin.x);
if (rel_x <= 0.0f) {
col = 0;
// Compute viewport-relative row so (0) is top row of the visible area
float vy_f = (mp.y - list_origin.y - scroll_y) / row_h;
long vy = static_cast<long>(vy_f);
if (vy < 0)
vy = 0;
// Clamp vy within visible content height to avoid huge jumps
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float child_h = (cr_max.y - cr_min.y);
long vis_rows = static_cast<long>(child_h / row_h);
if (vis_rows < 1)
vis_rows = 1;
if (vy >= vis_rows)
vy = vis_rows - 1;
// Translate viewport row to buffer row using Buffer::Rowoffs
std::size_t by = buf->Rowoffs() + static_cast<std::size_t>(vy);
if (by >= lines.size()) {
if (!lines.empty())
by = lines.size() - 1;
else
by = 0;
}
// Compute desired pixel X inside the viewport content (subtract horizontal scroll)
float px = (mp.x - list_origin.x - scroll_x);
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 {
float prev_w = 0.0f;
for (std::size_t i = 1; i <= line.size(); ++i) {
ImVec2 sz = ImGui::CalcTextSize(
line.c_str(), line.c_str() + static_cast<long>(i));
if (sz.x >= rel_x) {
// Pick closer between i-1 and i
float d_prev = rel_x - prev_w;
float d_curr = sz.x - rel_x;
col = (d_prev <= d_curr) ? (i - 1) : i;
// 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.
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.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
if (!line_clicked.empty() && coloffs > 0) {
while (i < line_clicked.size() && rx_abs < coloffs) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
}
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
float best_dist = std::numeric_limits<float>::infinity();
while (true) {
// For i in [current..size], evaluate candidate including the implicit end position
std::size_t rx_view = (rx_abs >= coloffs) ? (rx_abs - coloffs) : 0;
float rx_px = static_cast<float>(rx_view) * space_w;
float dist = std::fabs(px - rx_px);
if (dist <= best_dist) {
best_dist = dist;
best_col = i;
}
if (i == line_clicked.size())
break;
// advance to next source column
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
prev_w = sz.x;
if (i == line.size()) {
// clicked beyond EOL
float eol_w = sz.x;
col = (rel_x > eol_w + space_w * 0.5f)
? line.size()
: line.size();
++i;
}
}
}
}
// Dispatch command to move cursor
// Dispatch absolute buffer coordinates (row:col)
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%ld:%zu", row, col);
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];
ImGui::TextUnformatted(line.c_str());
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];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
// Emit spaces for the tab
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
ImGui::TextUnformatted(expanded.c_str());
// Draw a visible cursor indicator on the current line
if (i == cy) {
// Compute X offset by measuring text width up to cursor column
std::size_t px_count = std::min(cx, line.size());
ImVec2 pre_sz = ImGui::CalcTextSize(line.c_str(),
line.c_str() + static_cast<long>(px_count));
ImVec2 p0 = ImVec2(line_pos.x + pre_sz.x, line_pos.y);
// Compute rendered X (rx) from source column with tab expansion
std::size_t rx_abs = 0;
for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) {
if (line[k] == '\t')
rx_abs += (tabw - (rx_abs % tabw));
else
rx_abs += 1;
}
// Convert to viewport x by subtracting horizontal col offset
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(rx_viewport) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
@@ -200,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();
@@ -213,24 +364,123 @@ 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);
left += "kge"; // GUI app name
left += " ";
left += KTE_VERSION_STR;
std::string fname = buf->Filename();
if (!fname.empty()) {
std::string fname;
try {
fname = ed.DisplayNameFor(*buf);
} catch (...) {
fname = buf->Filename();
try {
fname = std::filesystem::path(fname).filename().string();
} catch (...) {}
} else {
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; // 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 += " *";
// Append total line count as "<n>L"
{
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
left += " ";
left += std::to_string(lcount);
left += "L";
}
// Build right text (cursor/mark)
int row1 = static_cast<int>(buf->Cury()) + 1;
@@ -245,8 +495,16 @@ GUIRenderer::Draw(Editor &ed)
std::snprintf(rbuf, sizeof(rbuf), "%d,%d | M: not set", row1, col1);
std::string right = rbuf;
// Middle message
const std::string &msg = ed.Status();
// Middle message: if a prompt is active, show "Label: text"; otherwise show status
std::string msg;
if (ed.PromptActive()) {
msg = ed.PromptLabel();
if (!msg.empty())
msg += ": ";
msg += ed.PromptText();
} else {
msg = ed.Status();
}
// Measurements
ImVec2 left_sz = ImGui::CalcTextSize(left.c_str());
@@ -291,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,5 +1,8 @@
#include "KKeymap.h"
#include <iostream>
#include <ncurses.h>
#include <ostream>
#include "KKeymap.h"
auto
@@ -13,17 +16,15 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
switch (k_lower) {
case 'd':
out = CommandId::KillLine;
return true; // C-k C-d
case 'x':
out = CommandId::SaveAndQuit;
return true; // C-k C-x
return true;
case 'q':
out = CommandId::QuitNow;
return true; // C-k C-q (quit immediately)
return true;
case 'x':
out = CommandId::SaveAndQuit;
return true;
default:
// Important: do not return here — fall through to non-ctrl table
// so that C-k u/U still work even if Ctrl is (incorrectly) held
break;
return false;
}
}
@@ -32,63 +33,86 @@ 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;
}
// 3) Non-control k-table (lowercased)
switch (k_lower) {
case 'j':
out = CommandId::JumpToMark;
return true; // C-k j
case 'f':
out = CommandId::FlushKillRing;
return true; // C-k f
case 'd':
out = CommandId::KillToEOL;
return true; // C-k d
case 'y':
out = CommandId::Yank;
return true; // C-k y
case 's':
out = CommandId::Save;
return true; // C-k s
case 'e':
out = CommandId::OpenFileStart;
return true; // C-k e (open file)
case 'b':
out = CommandId::BufferSwitchStart;
return true; // C-k b (switch buffer by name)
case 'c':
out = CommandId::BufferClose;
return true; // C-k c (close current buffer)
case 'n':
out = CommandId::BufferPrev;
return true; // C-k n (switch to previous buffer)
case 'x':
out = CommandId::SaveAndQuit;
return true; // C-k x
case 'q':
out = CommandId::Quit;
return true; // C-k q
case 'p':
out = CommandId::BufferNext;
return true; // C-k p (switch to next buffer)
case 'u':
out = CommandId::Undo;
return true; // C-k u (undo)
case '-':
out = CommandId::UnindentRegion;
return true; // C-k - (unindent region)
case '=':
out = CommandId::IndentRegion;
return true; // C-k = (indent region)
case 'l':
out = CommandId::ReloadBuffer;
return true; // C-k l (reload buffer)
case 'a':
out = CommandId::MarkAllAndJumpEnd;
return true; // C-k a (mark all and jump to end)
return true;
case 'b':
out = CommandId::BufferSwitchStart;
return true;
case 'c':
out = CommandId::BufferClose;
return true;
case 'd':
out = CommandId::KillToEOL;
return true;
case 'e':
out = CommandId::OpenFileStart;
return true;
case 'E':
std::cerr << "E is not a valid command" << std::endl;
return false;
case 'f':
out = CommandId::FlushKillRing;
return true;
case 'g':
out = CommandId::JumpToLine;
return true;
case 'h':
out = CommandId::ShowHelp;
return true;
case 'j':
out = CommandId::JumpToMark;
return true;
case 'l':
out = CommandId::ReloadBuffer;
return true;
case 'n':
out = CommandId::BufferPrev;
return true;
case 'o':
out = CommandId::ChangeWorkingDirectory;
return true;
case 'p':
out = CommandId::BufferNext;
return true;
case 'q':
out = CommandId::Quit;
return true;
case 's':
out = CommandId::Save;
return true;
case 'u':
out = CommandId::Undo;
return true;
case 'v':
out = CommandId::VisualFilePickerToggle;
return true;
case 'w':
out = CommandId::ShowWorkingDirectory;
return true;
case 'x':
out = CommandId::SaveAndQuit;
return true;
case 'y':
out = CommandId::Yank;
return true;
case '-':
out = CommandId::UnindentRegion;
return true;
case '=':
out = CommandId::IndentRegion;
return true;
default:
break;
}
// 3) Non-control k-table (lowercased)
return false;
}
@@ -128,15 +152,21 @@ 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;
case 'g':
out = CommandId::Refresh;
return true;
case 'x':
out = CommandId::SaveAndQuit; // direct C-x mapping (GUI had this)
return true;
default:
break;
}

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
@@ -35,6 +35,19 @@ map_key_to_command(const int ch,
case KEY_MOUSE: {
MEVENT ev{};
if (getmouse(&ev) == OK) {
// Mouse wheel → map to MoveUp/MoveDown one line per wheel notch
#ifdef BUTTON4_PRESSED
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
out = {true, CommandId::MoveUp, "", 0};
return true;
}
#endif
#ifdef BUTTON5_PRESSED
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
out = {true, CommandId::MoveDown, "", 0};
return true;
}
#endif
// React to left button click/press
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
char buf[64];

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;
{
@@ -168,21 +268,43 @@ TerminalRenderer::Draw(Editor &ed)
const Buffer *b = buf;
std::string fname;
if (b) {
try {
fname = ed.DisplayNameFor(*b);
} catch (...) {
fname = b->Filename();
}
if (!fname.empty()) {
try {
fname = std::filesystem::path(fname).filename().string();
} catch (...) {
// keep original on any error
} catch (...) {}
}
} else {
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());
left += " ";
left += std::to_string(lcount);
left += "L";
}
}
// Build right segment (cursor and mark)

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,24 +1,62 @@
# default.nix
{
lib,
stdenv,
cmake,
ncurses,
SDL2,
libGL,
xorg,
installShellFiles,
graphical ? false,
...
}:
let
pkgs = import <nixpkgs> {};
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
pkgs.stdenv.mkDerivation {
stdenv.mkDerivation {
pname = "kte";
version = "0.1.0";
inherit version;
src = ./.;
src = lib.cleanSource ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
buildInputs = with pkgs; [
nativeBuildInputs = [
cmake
ncurses
installShellFiles
]
++ lib.optionals graphical [
SDL2
libGL
xorg.libX11
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCURSES_NEED_NCURSES=TRUE"
"-DCURSES_NEED_WIDE=TRUE"
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
installManPage ../docs/kte.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
'';
}

215
docs/kge.1 Normal file
View File

@@ -0,0 +1,215 @@
.\" kge(1) — Kyle's Graphical Editor (GUI-first)
.\"
.\" Project homepage: https://github.com/wntrmute/kte
.TH KGE 1 "2025-11-30" "kte 0.1.0" "User Commands"
.SH NAME
kge \- Kyle's Graphical Editor (GUI-first)
.SH SYNOPSIS
.B kge
[
.I options
]
[
.I files ...
]
.SH DESCRIPTION
.B kge
is the GUI-first build target of Kyle's Text Editor. It shares the same
editor core and command model as
.BR kte (1),
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
are provided, they are opened on startup; otherwise, an empty buffer is
created.
.SH OPTIONS
.TP
.B -g, --gui
Use the GUI frontend (default for
.B kge
when built).
.TP
.B -t, --term
Use the terminal (ncurses) frontend instead of the GUI.
.TP
.B -h, --help
Display a brief usage summary and exit.
.TP
.B -V, --version
Print version information and exit.
.SH KEYBINDINGS
The GUI shares the same commands and keybindings as the terminal editor.
They are summarized here for convenience. See the ke manual in the source
tree for the canonical reference and notes:
.I docs/ke.md
.
.SS K-commands (prefix Ctrl-K)
.PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP
.B C-k BACKSPACE
Delete from the cursor to the beginning of the line.
.TP
.B C-k SPACE
Toggle the mark.
.TP
.B C-k -
If the mark is set, unindent the region.
.TP
.B C-k =
If the mark is set, indent the region.
.TP
.B C-k a
Set the mark at the beginning of the file, then jump to the end of the file.
.TP
.B C-k b
Switch to a buffer.
.TP
.B C-k c
Close the current buffer. If no other buffers are open, an empty buffer will be opened. To exit, use C-k q.
.TP
.B C-k d
Delete from the cursor to the end of the line.
.TP
.B C-k C-d
Delete the entire line.
.TP
.B C-k e
Edit a new file.
.TP
.B C-k f
Flush the kill ring.
.TP
.B C-k g
Go to a specific line.
.TP
.B C-k j
Jump to the mark.
.TP
.B C-k l
Reload the current buffer from disk.
.TP
.B C-k m
Run make(1), reporting success or failure.
.TP
.B C-k p
Switch to the next buffer.
.TP
.B C-k q
Exit the editor. If the file has unsaved changes, a warning will be printed; a second C-k q will exit.
.TP
.B C-k C-q
Immediately exit the editor.
.TP
.B C-k s
Save the file, prompting for a filename if needed.
.TP
.B C-k u
Undo.
.TP
.B C-k r
Redo changes.
.TP
.B C-k x
Save the file and exit. Also C-k C-x.
.TP
.B C-k y
Yank the kill ring.
.TP
.B C-k \e
Dump core.
.SS Other keybindings
.TP
.B C-g
Cancel the current operation.
.TP
.B C-l
Refresh the display.
.TP
.B C-r
Regex search.
.TP
.B C-s
Incremental find.
.TP
.B C-u
Universal argument. C-u followed by numbers will repeat an operation n times.
.TP
.B C-w
Kill the region if the mark is set.
.TP
.B C-y
Yank the kill ring.
.TP
.B ESC BACKSPACE
Delete the previous word.
.TP
.B ESC b
Move to the previous word.
.TP
.B ESC d
Delete the next word.
.TP
.B ESC f
Move to the next word.
.TP
.B ESC q
Reflow the paragraph to 72 columns or the value of the universal argument.
.TP
.B ESC w
Save the region (if the mark is set) to the kill ring.
.SH ENVIRONMENT
.TP
.B TERM
Used if the terminal frontend is selected.
.TP
.B LANG, LC_ALL, LC_CTYPE
Determine locale and character encoding.
.SH FILES
.TP
.I ~/.kte/
Future configuration directory (not yet stabilized).
.TP
.I kge.app
On macOS, the GUI is built and installed as an app bundle. The command-line
wrapper
.B kge
may still be available for launching with files.
.SH EXIT STATUS
Returns 0 on success, non-zero on failure.
.SH EXAMPLES
.TP
Launch GUI (default) with multiple files:
.RS
.nf
kge main.cc Buffer.cc
.fi
.RE
.TP
Open using the terminal frontend from kge:
.RS
.nf
kge --term README.md
.fi
.RE
.SH SEE ALSO
.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.
.SH AUTHORS
Kyle (wntrmute) and contributors.
.SH COPYRIGHT
Copyright \(co 2025 Kyle. License as per project repository.
.SH NOTES
This page documents kte/kge version 0.1.0.

214
docs/kte.1 Normal file
View File

@@ -0,0 +1,214 @@
.\" kte(1) — Kyle's Text Editor (terminal-first)
.\"
.\" Project homepage: https://github.com/wntrmute/kte
.TH KTE 1 "2025-11-30" "kte 0.1.0" "User Commands"
.SH NAME
kte \- Kyle's Text Editor (terminal-first)
.SH SYNOPSIS
.B kte
[
.I options
]
[
.I files ...
]
.SH DESCRIPTION
.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.
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
are provided, they are opened on startup; otherwise, an empty buffer is
created.
.SH OPTIONS
.TP
.B -g, --gui
Use the GUI frontend (if the binary was built with GUI support). If GUI was
not built, the editor exits with an error.
.TP
.B -t, --term
Use the terminal (ncurses) frontend. This is the default for
.B kte
.
.TP
.B -h, --help
Display a brief usage summary and exit.
.TP
.B -V, --version
Print version information and exit.
.SH KEYBINDINGS
The command model and keybindings are inherited from
.I ke
and are summarized here for convenience. See
.I docs/ke.md
in the source tree for the canonical reference and notes.
.SS K-commands (prefix Ctrl-K)
.PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP
.B C-k BACKSPACE
Delete from the cursor to the beginning of the line.
.TP
.B C-k SPACE
Toggle the mark.
.TP
.B C-k -
If the mark is set, unindent the region.
.TP
.B C-k =
If the mark is set, indent the region.
.TP
.B C-k a
Set the mark at the beginning of the file, then jump to the end of the file.
.TP
.B C-k b
Switch to a buffer.
.TP
.B C-k c
Close the current buffer. If no other buffers are open, an empty buffer will be opened. To exit, use C-k q.
.TP
.B C-k d
Delete from the cursor to the end of the line.
.TP
.B C-k C-d
Delete the entire line.
.TP
.B C-k e
Edit a new file.
.TP
.B C-k f
Flush the kill ring.
.TP
.B C-k g
Go to a specific line.
.TP
.B C-k j
Jump to the mark.
.TP
.B C-k l
Reload the current buffer from disk.
.TP
.B C-k m
Run make(1), reporting success or failure.
.TP
.B C-k p
Switch to the next buffer.
.TP
.B C-k q
Exit the editor. If the file has unsaved changes, a warning will be printed; a second C-k q will exit.
.TP
.B C-k C-q
Immediately exit the editor.
.TP
.B C-k s
Save the file, prompting for a filename if needed.
.TP
.B C-k u
Undo.
.TP
.B C-k r
Redo changes.
.TP
.B C-k x
Save the file and exit. Also C-k C-x.
.TP
.B C-k y
Yank the kill ring.
.TP
.B C-k \e
Dump core.
.SS Other keybindings
.TP
.B C-g
Cancel the current operation.
.TP
.B C-l
Refresh the display.
.TP
.B C-r
Regex search.
.TP
.B C-s
Incremental find.
.TP
.B C-u
Universal argument. C-u followed by numbers will repeat an operation n times.
.TP
.B C-w
Kill the region if the mark is set.
.TP
.B C-y
Yank the kill ring.
.TP
.B ESC BACKSPACE
Delete the previous word.
.TP
.B ESC b
Move to the previous word.
.TP
.B ESC d
Delete the next word.
.TP
.B ESC f
Move to the next word.
.TP
.B ESC q
Reflow the paragraph to 72 columns or the value of the universal argument.
.TP
.B ESC w
Save the region (if the mark is set) to the kill ring.
.SH ENVIRONMENT
.TP
.B TERM
Specifies terminal type and capabilities for ncurses.
.TP
.B LANG, LC_ALL, LC_CTYPE
Determine locale and character encoding.
.SH FILES
.TP
.I ~/.kte/
Future configuration directory (not yet stabilized).
.SH EXIT STATUS
Returns 0 on success, non-zero on failure.
.SH EXAMPLES
.TP
Edit a file in the terminal (default):
.RS
.nf
kte README.md
.fi
.RE
.TP
Force GUI frontend (if available):
.RS
.nf
kte --gui main.cc
.fi
.RE
.SH SEE ALSO
.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.
.SH AUTHORS
Kyle (wntrmute) and contributors.
.SH COPYRIGHT
Copyright \(co 2025 Kyle. License as per project repository.
.SH NOTES
This page documents kte version 0.1.0.

BIN
docs/screenshot.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 196 KiB

40
flake.lock generated
View File

@@ -1,30 +1,12 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764242076,
"narHash": "sha256-sKoIWfnijJ0+9e4wRvIgm/HgE27bzwQxcEmo2J/gNpI=",
"lastModified": 1764517877,
"narHash": "sha256-pp3uT4hHijIC8JUK5MEqeAWmParJrgBVzHLNfJDZxg4=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"rev": "2d293cbfa5a793b4c50d17c05ef9e385b90edf6c",
"type": "github"
},
"original": {
@@ -36,24 +18,8 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

View File

@@ -1,55 +1,20 @@
# flake.nix
{
description = "kte ImGui/SDL2 text editor";
description = "kyle's text editor";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
outputs =
inputs@{ self, nixpkgs, ... }:
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 ];
};
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
pkgsFor = system: import nixpkgs { inherit system; };
in
{
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

75
main.cc
View File

@@ -1,12 +1,18 @@
#include <cctype>
#include <cstdio>
#include <getopt.h>
#include <iostream>
#include <memory>
#include <signal.h>
#include <string>
#include <unistd.h>
#include <getopt.h>
#include <sys/stat.h>
#include "Editor.h"
#include "Command.h"
#include "Editor.h"
#include "Frontend.h"
#include "TerminalFrontend.h"
#if defined(KTE_BUILD_GUI)
#include "GUIFrontend.h"
#endif
@@ -16,6 +22,7 @@
# define KTE_VERSION_STR "devel"
#endif
static void
PrintUsage(const char *prog)
{
@@ -106,16 +113,63 @@ main(int argc, const char *argv[])
}
#endif
// Open files passed on the CLI; if none, create an empty buffer
// Open files passed on the CLI; support +N to jump to line N in the next file.
// If no files are provided, create an empty buffer.
if (optind < argc) {
std::size_t pending_line = 0; // 0 = no pending line
for (int i = optind; i < argc; ++i) {
const char *arg = argv[i];
if (arg && arg[0] == '+') {
// Parse +<digits>
const char *p = arg + 1;
if (*p != '\0') {
bool all_digits = true;
for (const char *q = p; *q; ++q) {
if (!std::isdigit(static_cast<unsigned char>(*q))) {
all_digits = false;
break;
}
}
if (all_digits) {
// Clamp to >=1 later; 0 disables.
try {
unsigned long v = std::stoul(p);
pending_line = static_cast<std::size_t>(v);
} catch (...) {
// Ignore malformed huge numbers
pending_line = 0;
}
continue; // look for the next file arg
}
}
// Fall through: not a +number, treat as filename starting with '+'
}
std::string err;
const std::string path = argv[i];
const std::string path = arg;
if (!editor.OpenFile(path, err)) {
editor.SetStatus("open: " + err);
std::cerr << "kte: " << err << "\n";
} else if (pending_line > 0) {
// Apply pending +N to the just-opened (current) buffer
if (Buffer *b = editor.CurrentBuffer()) {
std::size_t nrows = b->Nrows();
std::size_t line = pending_line > 0 ? pending_line - 1 : 0;
// 1-based to 0-based
if (nrows > 0) {
if (line >= nrows)
line = nrows - 1;
} else {
line = 0;
}
b->SetCursor(0, line);
// Do not force viewport offsets here; the frontend/renderer
// will establish dimensions and normalize visibility on first draw.
}
pending_line = 0; // consumed
}
}
// If we ended with a pending +N but no subsequent file, ignore it.
} else {
// Create a single empty buffer
editor.AddBuffer(Buffer());
@@ -129,13 +183,22 @@ main(int argc, const char *argv[])
std::unique_ptr<Frontend> fe;
#if defined(KTE_BUILD_GUI)
if (use_gui) {
fe.reset(new GUIFrontend());
fe = std::make_unique<GUIFrontend>();
} else
#endif
{
fe.reset(new TerminalFrontend());
fe = std::make_unique<TerminalFrontend>();
}
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
if (use_gui) {
/* likely using the .app, so need to cd */
if (chdir(getenv("HOME")) != 0) {
std::cerr << "kge.app: failed to chdir to HOME" << std::endl;
}
}
#endif
if (!fe->Init(editor)) {
std::cerr << "kte: failed to initialize frontend" << std::endl;
return 1;

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