22 Commits

Author SHA1 Message Date
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 2466 additions and 794 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 }}

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>

188
.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,9 @@
</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`.">
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Actually add the screenshot.">
<change beforePath="$PROJECT_DIR$/.github/workflows/release.yml" beforeDir="false" afterPath="$PROJECT_DIR$/.github/workflows/release.yml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -49,21 +50,18 @@
<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>
<component name="HighlightingSettingsPerFile">
<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" />
@@ -74,9 +72,9 @@
<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,51 +85,52 @@
<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">
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<component name="RunManager" selected="CMake Application.kge">
<configuration default="true" type="CLionExternalRunConfiguration" 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" />
<option name="CLION.EXTERNAL.BUILD" enabled="true" />
</method>
</configuration>
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
@@ -139,7 +138,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" 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 +161,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="9962000" />
</task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" />
@@ -220,7 +227,63 @@
<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>
<option name="localTasksCounter" value="15" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -241,7 +304,14 @@
<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." />
<option name="LAST_COMMIT_MESSAGE" value="Actually add the screenshot." />
</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.

111
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()
@@ -129,12 +130,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 +172,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 +219,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 +275,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 +314,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 +347,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();
@@ -340,13 +384,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()));
auto &line = rows_[y];
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,15 +414,18 @@ 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);
}
@@ -387,24 +434,28 @@ Buffer::split_line(int row, int col)
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, std::string(text));
}
@@ -415,7 +466,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();

View File

@@ -4,13 +4,13 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "0.1.0")
set(KTE_VERSION "1.0.3")
# 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_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")
if (CMAKE_HOST_UNIX)
@@ -44,6 +44,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})
@@ -104,6 +106,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
@@ -123,6 +128,8 @@ endif ()
if (${BUILD_GUI})
target_sources(kte PRIVATE
Font.h
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -137,6 +144,8 @@ if (${BUILD_GUI})
main.cc
${COMMON_SOURCES}
${COMMON_HEADERS}
GUIConfig.cc
GUIConfig.h
GUIRenderer.cc
GUIRenderer.h
GUIInputHandler.cc
@@ -148,6 +157,15 @@ if (${BUILD_GUI})
# 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 +177,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 .
)
else()
install(TARGETS kte
RUNTIME DESTINATION kge.app/Contents/MacOS
)
else ()
install(TARGETS kge
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
endif()
endif ()
# Install kge man page only when GUI is built
install(FILES docs/kge.1 DESTINATION ${CMAKE_INSTALL_MANDIR}/man1)
install(FILES kge.png DESTINATION ${CMAKE_INSTALL_PREFIX}/share/icons)
endif ()

View File

@@ -1,18 +1,18 @@
#include <algorithm>
#include <filesystem>
#include <cstdlib>
#include "Command.h"
#include "Editor.h"
#include "Buffer.h"
#include "UndoSystem.h"
// Note: Command layer must remain UI-agnostic. Do not include frontend/IO headers here.
// Keep buffer viewport offsets so that the cursor stays within the visible
// window based on the editor's current dimensions. The bottom row is reserved
// for the status line.
static std::size_t
compute_render_x(const std::string &line, std::size_t curx, std::size_t tabw)
compute_render_x(const std::string &line, const std::size_t curx, const std::size_t tabw)
{
std::size_t rx = 0;
for (std::size_t i = 0; i < curx && i < line.size(); ++i) {
@@ -453,6 +453,36 @@ cmd_save(CommandContext &ctx)
}
// --- Working directory commands ---
static bool
cmd_show_working_directory(CommandContext &ctx)
{
try {
std::filesystem::path cwd = std::filesystem::current_path();
ctx.editor.SetStatus(std::string("cwd: ") + cwd.string());
return true;
} catch (const std::exception &e) {
ctx.editor.SetStatus(std::string("cwd: <error> ") + e.what());
return false;
}
}
static bool
cmd_change_working_directory_start(CommandContext &ctx)
{
std::string initial;
try {
initial = std::filesystem::current_path().string() + "/";
} catch (...) {
initial.clear();
}
ctx.editor.StartPrompt(Editor::PromptKind::Chdir, "chdir", initial);
ctx.editor.SetStatus(std::string("chdir: ") + ctx.editor.PromptText());
return true;
}
static bool
cmd_save_as(CommandContext &ctx)
{
@@ -646,6 +676,40 @@ cmd_open_file_start(CommandContext &ctx)
}
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
static bool
cmd_visual_file_picker_toggle(CommandContext &ctx)
{
// Toggle visibility
bool show = !ctx.editor.FilePickerVisible();
ctx.editor.SetFilePickerVisible(show);
if (show) {
// Initialize directory to current working directory if empty
if (ctx.editor.FilePickerDir().empty()) {
try {
ctx.editor.SetFilePickerDir(std::filesystem::current_path().string());
} catch (...) {
ctx.editor.SetFilePickerDir(".");
}
}
ctx.editor.SetStatus("Open File (visual)");
} else {
ctx.editor.SetStatus("Closed file picker");
}
return true;
}
static bool
cmd_jump_to_line_start(CommandContext &ctx)
{
// Start a prompt to read a 1-based line number and jump there (clamped)
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
ctx.editor.SetStatus("Goto line: ");
return true;
}
// --- Buffers: switch/next/prev/close ---
static bool
cmd_buffer_switch_start(CommandContext &ctx)
@@ -752,43 +816,141 @@ cmd_insert_text(CommandContext &ctx)
}
// If a prompt is active, edit prompt text
if (ctx.editor.PromptActive()) {
// Special-case: buffer switch prompt supports Tab-completion
if (ctx.editor.CurrentPromptKind() == Editor::PromptKind::BufferSwitch && ctx.arg == "\t") {
// Complete against buffer names (path and basename)
const std::string prefix = ctx.editor.PromptText();
std::vector<std::pair<std::string, std::size_t> > cands; // name, index
const auto &bs = ctx.editor.Buffers();
for (std::size_t i = 0; i < bs.size(); ++i) {
std::string full = buffer_display_name(bs[i]);
std::string base = buffer_basename(bs[i]);
if (full.rfind(prefix, 0) == 0) {
cands.emplace_back(full, i);
// Special-case: Tab-completion for prompts
if (ctx.arg == "\t") {
auto kind = ctx.editor.CurrentPromptKind();
// Buffer switch prompt supports Tab-completion on buffer names
if (kind == Editor::PromptKind::BufferSwitch) {
// Complete against buffer names (path and basename)
const std::string prefix = ctx.editor.PromptText();
std::vector<std::pair<std::string, std::size_t> > cands; // name, index
const auto &bs = ctx.editor.Buffers();
for (std::size_t i = 0; i < bs.size(); ++i) {
std::string full = buffer_display_name(bs[i]);
std::string base = buffer_basename(bs[i]);
if (full.rfind(prefix, 0) == 0) {
cands.emplace_back(full, i);
}
if (base.rfind(prefix, 0) == 0 && base != full) {
cands.emplace_back(base, i);
}
}
if (base.rfind(prefix, 0) == 0 && base != full) {
cands.emplace_back(base, i);
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cands[0].first);
} else {
// extend to longest common prefix
std::string lcp = cands[0].first;
for (std::size_t i = 1; i < cands.size(); ++i) {
const std::string &s = cands[i].first;
std::size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
}
if (!lcp.empty() && lcp != ctx.editor.PromptText())
ctx.editor.SetPromptText(lcp);
}
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true;
}
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cands[0].first);
} else {
// extend to longest common prefix
std::string lcp = cands[0].first;
for (std::size_t i = 1; i < cands.size(); ++i) {
const std::string &s = cands[i].first;
std::size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
// File path completion for OpenFile/SaveAs/Chdir
if (kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs
|| kind == Editor::PromptKind::Chdir) {
auto expand_user_path = [](const std::string &in) -> std::string {
if (!in.empty() && in[0] == '~') {
const char *home = std::getenv("HOME");
if (home && in.size() == 1)
return std::string(home);
if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
std::string rest = in.substr(1); // keep leading slash
return std::string(home) + rest;
}
}
return in;
};
std::string text = ctx.editor.PromptText();
// Build a path and split dir + base prefix
std::string expanded = expand_user_path(text);
std::filesystem::path p(expanded);
std::filesystem::path dir;
std::string base;
if (expanded.empty()) {
dir = std::filesystem::current_path();
base.clear();
} else if (std::filesystem::is_directory(p)) {
dir = p;
base.clear();
} else {
dir = p.parent_path();
base = p.filename().string();
if (dir.empty())
dir = std::filesystem::current_path();
}
if (!lcp.empty() && lcp != ctx.editor.PromptText())
ctx.editor.SetPromptText(lcp);
std::error_code ec;
std::vector<std::filesystem::directory_entry> entries;
std::filesystem::directory_iterator it(dir, ec), end;
for (; !ec && it != end; it.increment(ec)) {
entries.push_back(*it);
}
// Filter by base prefix
std::vector<std::string> cands;
for (const auto &de: entries) {
std::string name = de.path().filename().string();
if (base.empty() || name.rfind(base, 0) == 0) {
std::string candidate = (dir / name).string();
// For dirs, add trailing slash hint
if (de.is_directory(ec))
candidate += "/";
cands.push_back(candidate);
}
}
// If no candidates, keep as-is
if (cands.empty()) {
// no-op
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cands[0]);
} else {
// Longest common prefix of display strings
auto lcp = cands[0];
for (size_t i = 1; i < cands.size(); ++i) {
const auto &s = cands[i];
size_t j = 0;
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
++j;
lcp.resize(j);
if (lcp.empty())
break;
}
if (!lcp.empty() && lcp != ctx.editor.PromptText()) {
ctx.editor.SetPromptText(lcp);
} else {
// Show some choices in status (trim to avoid spam)
std::string msg = ctx.editor.PromptLabel() + ": ";
size_t shown = 0;
for (const auto &s: cands) {
if (shown >= 10) {
msg += "";
break;
}
if (shown > 0)
msg += ' ';
msg += std::filesystem::path(s).filename().string();
++shown;
}
ctx.editor.SetStatus(msg);
return true;
}
}
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true;
}
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true;
}
ctx.editor.AppendPromptText(ctx.arg);
@@ -899,6 +1061,20 @@ cmd_newline(CommandContext &ctx)
ensure_cursor_visible(ctx.editor, *b);
} else if (kind == Editor::PromptKind::OpenFile) {
std::string err;
// Expand "~" to the user's home directory
auto expand_user_path = [](const std::string &in) -> std::string {
if (!in.empty() && in[0] == '~') {
const char *home = std::getenv("HOME");
if (home && in.size() == 1)
return std::string(home);
if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
std::string rest = in.substr(1);
return std::string(home) + rest;
}
}
return in;
};
value = expand_user_path(value);
if (value.empty()) {
ctx.editor.SetStatus("Open canceled (empty)");
} else if (!ctx.editor.OpenFile(value, err)) {
@@ -943,6 +1119,21 @@ cmd_newline(CommandContext &ctx)
if (!buf) {
ctx.editor.SetStatus("No buffer to save");
} else {
// Expand "~" for save path
auto expand_user_path = [](const std::string &in) -> std::string {
if (!in.empty() && in[0] == '~') {
const char *home = std::getenv("HOME");
if (home && in.size() == 1)
return std::string(home);
if (home && (in.size() > 1) && (
in[1] == '/' || in[1] == '\\')) {
std::string rest = in.substr(1);
return std::string(home) + rest;
}
}
return in;
};
value = expand_user_path(value);
// If this is a first-time save (unnamed/non-file-backed) and the
// target exists, ask for confirmation before overwriting.
if (!buf->IsFileBacked() && std::filesystem::exists(value)) {
@@ -990,6 +1181,74 @@ cmd_newline(CommandContext &ctx)
} else {
ctx.editor.SetStatus("Nothing to confirm");
}
} else if (kind == Editor::PromptKind::GotoLine) {
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer");
return true;
}
std::size_t nrows = buf->Nrows();
if (nrows == 0) {
buf->SetCursor(0, 0);
ensure_cursor_visible(ctx.editor, *buf);
ctx.editor.SetStatus("Empty buffer");
return true;
}
// Parse 1-based line number; on failure, keep cursor and show status
std::size_t line1 = 0;
try {
if (!value.empty())
line1 = static_cast<std::size_t>(std::stoull(value));
} catch (...) {
line1 = 0;
}
if (line1 == 0) {
ctx.editor.SetStatus("Goto canceled (invalid line)");
return true;
}
std::size_t y = line1 - 1; // convert to 0-based
if (y >= nrows)
y = nrows - 1; // clamp to last line
buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
ctx.editor.SetStatus("Goto line " + std::to_string(line1));
} else if (kind == Editor::PromptKind::Chdir) {
// Attempt to change the current working directory
if (value.empty()) {
ctx.editor.SetStatus("chdir canceled (empty)");
return true;
}
try {
// Expand "~" for chdir
auto expand_user_path = [](const std::string &in) -> std::string {
if (!in.empty() && in[0] == '~') {
const char *home = std::getenv("HOME");
if (home && in.size() == 1)
return std::string(home);
if (home && (in.size() > 1) && (in[1] == '/' || in[1] == '\\')) {
std::string rest = in.substr(1);
return std::string(home) + rest;
}
}
return in;
};
value = expand_user_path(value);
std::filesystem::path p(value);
std::error_code ec;
// Expand if value is relative: resolve against current_path implicitly
if (!std::filesystem::exists(p, ec)) {
ctx.editor.SetStatus(std::string("chdir: no such path: ") + value);
return true;
}
if (!std::filesystem::is_directory(p, ec)) {
ctx.editor.SetStatus(std::string("chdir: not a directory: ") + value);
return true;
}
std::filesystem::current_path(p);
ctx.editor.SetStatus(std::string("cwd: ") + std::filesystem::current_path().string());
} catch (const std::exception &e) {
ctx.editor.SetStatus(std::string("chdir failed: ") + e.what());
}
}
return true;
}
@@ -2372,6 +2631,10 @@ InstallDefaultCommands()
CommandRegistry::Register({
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
});
// Direct navigation by line number
CommandRegistry::Register({
CommandId::JumpToLine, "goto-line", "Prompt for line and jump", cmd_jump_to_line_start
});
// Undo/Redo
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
@@ -2390,6 +2653,20 @@ InstallDefaultCommands()
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
cmd_mark_all_and_jump_end
});
// GUI
CommandRegistry::Register({
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
cmd_visual_file_picker_toggle
});
// Working directory
CommandRegistry::Register({
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
cmd_show_working_directory
});
CommandRegistry::Register({
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
cmd_change_working_directory_start
});
// UI helpers
CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});

View File

@@ -24,6 +24,8 @@ enum class CommandId {
KPrefix, // show "C-k _" prompt in status when entering k-command
FindStart, // begin incremental search (placeholder)
OpenFileStart, // begin open-file prompt
// GUI: visual file picker
VisualFilePickerToggle,
// Buffers
BufferSwitchStart, // begin buffer switch prompt
BufferClose,
@@ -72,6 +74,10 @@ enum class CommandId {
// 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.
// 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,7 @@ 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, OpenFile, SaveAs, Confirm, BufferSwitch, GotoLine, Chdir };
void StartPrompt(PromptKind kind, const std::string &label, const std::string &initial)
@@ -409,6 +409,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 +441,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 +503,10 @@ 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_;
};
#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,45 @@
#include "GUIRenderer.h"
#include "Editor.h"
#include "Buffer.h"
#include "Command.h"
#include <cmath>
#include <cstdio>
#include <filesystem>
#include <limits>
#include <cstdlib>
#include <string>
#include <imgui.h>
#include <cstdio>
#include <string>
#include <filesystem>
#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 +63,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 +74,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 +133,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 +149,131 @@ 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;
} 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;
break;
}
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();
}
// 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;
// Convert pixel X to a render-column target including horizontal col offset
// Use our own tab expansion of width 8 to match command layer logic.
const std::string &line_clicked = lines[by];
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;
}
}
// Dispatch command to move cursor
// 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;
}
++i;
}
// 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());
// 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
// 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);
@@ -197,40 +281,91 @@ GUIRenderer::Draw(Editor &ed)
}
ImGui::EndChild();
// Status bar spanning full width
ImGui::Separator();
// 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();
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float x0 = win_pos.x + cr_min.x;
float x1 = win_pos.x + cr_max.x;
ImVec2 cursor = ImGui::GetCursorScreenPos();
float bar_h = ImGui::GetFrameHeight();
ImVec2 p0(x0, cursor.y);
ImVec2 p1(x1, cursor.y + bar_h);
ImU32 bg_col = ImGui::GetColorU32(ImGuiCol_HeaderActive);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, bg_col);
// 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()) {
// Compute full content width and draw a filled background rectangle
ImVec2 win_pos = ImGui::GetWindowPos();
ImVec2 cr_min = ImGui::GetWindowContentRegionMin();
ImVec2 cr_max = ImGui::GetWindowContentRegionMax();
float x0 = win_pos.x + cr_min.x;
float x1 = win_pos.x + cr_max.x;
ImVec2 cursor = ImGui::GetCursorScreenPos();
float bar_h = ImGui::GetFrameHeight();
ImVec2 p0(x0, cursor.y);
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 msg = ed.PromptLabel();
if (!msg.empty()) msg += ": ";
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;
}
}
}
msg += ptext;
float pad = 6.f;
ImVec2 msg_sz = ImGui::CalcTextSize(msg.c_str());
float left_x = p0.x + pad;
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(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;
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 +380,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());
@@ -289,10 +432,160 @@ GUIRenderer::Draw(Editor &ed)
ImGui::PopClipRect();
}
}
// Advance cursor to after the bar to keep layout consistent
ImGui::Dummy(ImVec2(x1 - x0, bar_h));
// 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();

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;
}
}
@@ -33,62 +34,78 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
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 '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;
}
@@ -134,9 +151,6 @@ KLookupCtrlCommand(const int ascii_key, CommandId &out) -> bool
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();

165
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):
@@ -137,26 +58,26 @@ Dependencies by platform
------------------------
- macOS (Homebrew)
- Terminal (default):
- `brew install ncurses`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- `brew install sdl2 freetype`
- OpenGL is provided by the system framework on macOS; no package needed.
- Terminal (default):
- `brew install ncurses`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- `brew install sdl2 freetype`
- OpenGL is provided by the system framework on macOS; no package needed.
- Debian/Ubuntu
- Terminal (default):
- `sudo apt-get install -y libncurses5-dev libncursesw5-dev`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- `sudo apt-get install -y libsdl2-dev libfreetype6-dev mesa-common-dev`
- The `mesa-common-dev` package provides OpenGL headers/libs (`libGL`).
- Terminal (default):
- `sudo apt-get install -y libncurses5-dev libncursesw5-dev`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- `sudo apt-get install -y libsdl2-dev libfreetype6-dev mesa-common-dev`
- The `mesa-common-dev` package provides OpenGL headers/libs (`libGL`).
- NixOS/Nix
- Terminal (default):
- Ad-hoc shell: `nix-shell -p cmake gcc ncurses`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- Ad-hoc shell: `nix-shell -p cmake gcc ncurses SDL2 freetype libGL`
- With flakes/devshell (example `flake.nix` inputs not provided): include
`ncurses` for TUI, and `SDL2`, `freetype`, `libGL` for GUI in your devShell.
- Terminal (default):
- Ad-hoc shell: `nix-shell -p cmake gcc ncurses`
- Optional GUI (enable with `-DBUILD_GUI=ON`):
- Ad-hoc shell: `nix-shell -p cmake gcc ncurses SDL2 freetype libGL`
- With flakes/devshell (example `flake.nix` inputs not provided): include
`ncurses` for TUI, and `SDL2`, `freetype`, `libGL` for GUI in your devShell.
Notes
-----
@@ -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.
- [ ] Search + Replace
- [ ] Regex search + replace
- [ ] The undo system should actually work
- [ ] Able to mark buffers as read-only
- [ ] Built-in help text
- [ ] Shorten paths in the homedir with ~
- [ ] When the filename is longer than the message window, scoot left to
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,20 @@
#include "TerminalRenderer.h"
#include <ncurses.h>
#include <cstdio>
#include <string>
#include <algorithm>
#include <cstdio>
#include <filesystem>
#include <cstdlib>
#include <ncurses.h>
#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;
@@ -149,13 +150,50 @@ TerminalRenderer::Draw(Editor &ed)
mvaddstr(0, 0, "[no buffer]");
}
// Status line (inverse) — left: app/version/buffer/dirty, middle: message, right: cursor/mark
move(rows - 1, 0);
attron(A_REVERSE);
// Status line (inverse)
move(rows - 1, 0);
attron(A_REVERSE);
// Fill the status line with spaces first
for (int i = 0; i < cols; ++i)
addch(' ');
// Fill the status line with spaces first
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 msg = ed.PromptLabel();
if (!msg.empty())
msg += ": ";
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;
}
}
}
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 +206,40 @@ TerminalRenderer::Draw(Editor &ed)
const Buffer *b = buf;
std::string fname;
if (b) {
fname = b->Filename();
}
if (!fname.empty()) {
try {
fname = std::filesystem::path(fname).filename().string();
fname = ed.DisplayNameFor(*b);
} catch (...) {
// keep original on any error
fname = b->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; // 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 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)
@@ -224,10 +281,10 @@ TerminalRenderer::Draw(Editor &ed)
if (llen > 0)
mvaddnstr(rows - 1, 0, left.c_str(), llen);
// Draw right, flush to end
int rstart = std::max(0, cols - rlen);
if (rlen > 0)
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
// Draw right, flush to end
int rstart = std::max(0, cols - rlen);
if (rlen > 0)
mvaddnstr(rows - 1, rstart, right.c_str(), rlen);
// Middle message
const std::string &msg = ed.Status();
@@ -243,7 +300,7 @@ TerminalRenderer::Draw(Editor &ed)
}
}
attroff(A_REVERSE);
attroff(A_REVERSE);
// Restore terminal cursor to the content position so a visible caret
// remains in the editing area (not on the status line).

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>

51
default-gui.nix Normal file
View File

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

42
default-nogui.nix Normal file
View File

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

View File

@@ -1,24 +1,50 @@
# default.nix
{
lib,
stdenv,
cmake,
ncurses,
SDL2,
libGL,
xorg,
installShellFiles,
...
}:
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
SDL2
libGL
xorg.libX11
installShellFiles
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCURSES_NEED_NCURSES=TRUE"
"-DCURSES_NEED_WIDE=TRUE"
"-DCMAKE_BUILD_TYPE=Debug"
];
installPhase = ''
runHook preInstall
mkdir -p $out/bin
cp kte $out/bin/
cp kge $out/bin/
installManPage ../docs/kte.1
installManPage ../docs/kge.1
runHook postInstall
'';
}

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

55
flake-gui.nix Normal file
View File

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

38
flake.lock generated
View File

@@ -1,34 +1,16 @@
{
"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=",
"owner": "NixOS",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "2fad6eac6077f03fe109c4d4eb171cf96791faa4",
"type": "github"
},
"original": {
"owner": "NixOS",
"owner": "nixos",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
@@ -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,21 @@
# 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";
nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
};
outputs = { self, nixpkgs, flake-utils }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = nixpkgs.legacyPackages.${system};
in {
packages.default = pkgs.stdenv.mkDerivation {
pname = "kte";
version = "0.1.0";
src = ./.;
nativeBuildInputs = [ pkgs.cmake pkgs.pkg-config ];
buildInputs = with pkgs; [
ncurses
SDL2
libGL
xorg.libX11
];
cmakeFlags = [
"-DBUILD_GUI=ON"
"-DCURSES_NEED_NCURSES=TRUE"
"-DCURSES_NEED_WIDE=TRUE"
];
# Alternative (even stronger): completely hide the broken module
preConfigure = ''
# If the project ships its own FindSDL2.cmake in cmake/, hide it
if [ -f cmake/FindSDL2.cmake ]; then
mv cmake/FindSDL2.cmake cmake/FindSDL2.cmake.disabled
echo "Disabled bundled FindSDL2.cmake"
fi
'';
meta = with pkgs.lib; {
description = "kte ImGui/SDL2 GUI editor";
mainProgram = "kte";
platforms = platforms.linux;
};
};
devShells.default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.default ];
packages = with pkgs; [ gdb clang-tools ];
};
});
outputs =
{ self, nixpkgs }:
let
pkgs = import nixpkgs { system = "x86_64-linux"; };
in
{
packages.x86_64-linux = {
default = pkgs.callPackage ./default-nogui.nix { };
kge = pkgs.callPackage ./default-gui.nix { };
kte = pkgs.callPackage ./default-nogui.nix { };
full = pkgs.callPackage ./default.nix { };
};
};
}

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

77
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)
{
@@ -49,7 +56,7 @@ main(int argc, const char *argv[])
int opt;
int long_index = 0;
while ((opt = getopt_long(argc, const_cast<char * const*>(argv), "gthV", long_opts, &long_index)) != -1) {
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
switch (opt) {
case 'g':
req_gui = true;
@@ -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