11 Commits

Author SHA1 Message Date
483ff18b0d Add ScrollUp and ScrollDown commands for viewport scrolling, refine mouse wheel handling in GUI and terminal, and bump version to 1.2.2.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-02 02:53:02 -08:00
cd33e8feb1 Refactor scrolling logic for GUIRenderer and terminal to improve synchronization and cursor visibility. 2025-12-02 02:43:05 -08:00
0bfe75fbf0 Refactor indentation for consistent style across codebase.
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-02 01:37:44 -08:00
d15b241140 Refactor syntax highlighting infrastructure and related classes.
- Moved all language highlighter implementations (`CppHighlighter`, `GoHighlighter`, `JsonHighlighter`, etc.), the engine, and registry to `syntax/`.
2025-12-02 01:36:26 -08:00
ceef6af3ae Add extensible highlighter registration and Tree-sitter support.
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
- Implemented runtime API for registering custom highlighters.
- Added optional Tree-sitter integration for advanced syntax parsing (disabled by default).
- Updated buffer initialization and copying to support dynamic highlighter configuration.
- Introduced `NullHighlighter` as a fallback for unsupported filetypes.
- Enhanced CMake configuration with `KTE_ENABLE_TREESITTER` option.
2025-12-01 19:04:37 -08:00
e62cf3ee28 Add viewport-aware syntax prefetching and background warming.
- Added prefetching in both terminal and GUI renderers to optimize visible row highlights.
- Introduced background worker for offscreen highlight warming to improve scrolling performance.
- Refactored `HighlighterEngine` to manage thread-safety, caching, and stateful re-computation.
- Integrated changes into `HighlighterEngine`, `TerminalRenderer`, and `GUIRenderer`.
- Bumped version to 1.2.0 in preparation for the release.
2025-12-01 18:37:01 -08:00
1a77f28ce4 Add syntax highlighting infrastructure
- Introduced `HighlighterRegistry` with support for multiple language highlighters (e.g., JSON, Markdown, Python).
- Added `JsonHighlighter` implementation for basic JSON syntax highlighting.
2025-12-01 18:20:36 -08:00
4d84b352eb format style 2025-12-01 16:47:16 -08:00
1892075d82 bump version
Some checks failed
Release / Bump Homebrew formula (push) Has been cancelled
Release / Build Linux amd64 (push) Has been cancelled
Release / Build Linux arm64 (push) Has been cancelled
Release / Build macOS arm64 (.app) (push) Has been cancelled
Release / Create GitHub Release (push) Has been cancelled
2025-12-01 16:38:10 -08:00
719862c842 Fix double scroll issue. 2025-12-01 16:37:51 -08:00
655cc40162 Refine help text, keybindings, GUI themes, and undo system.
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
- Expanded help text and command documentation with detailed keybinding descriptions.
- Added theme customization support to GUIConfig (Nord default, light/dark variants).
- Adjusted for consistent indentation and debug instrumentation in undo system.
- Enhanced test cases for multi-line, UTF-8, and branching scenarios.
2025-12-01 15:21:52 -08:00
68 changed files with 7330 additions and 2054 deletions

369
.idea/workspace.xml generated
View File

@@ -1,369 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="AutoImportSettings">
<option name="autoReloadType" value="SELECTIVE" />
</component>
<component name="BackendCodeEditorMiscSettings">
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" />
<option name="/Default/Housekeeping/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" />
<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">{
&quot;useNewFormat&quot;: true
}</component>
<component name="CMakeProjectFlavorService">
<option name="flavorId" value="CMakePlainProjectFlavor" />
</component>
<component name="CMakeReloadState">
<option name="reloaded" value="true" />
</component>
<component name="CMakeRunConfigurationManager">
<generated>
<config projectName="kte" targetName="kte" />
<config projectName="kte" targetName="imgui" />
<config projectName="kte" targetName="kge" />
</generated>
</component>
<component name="CMakeSettings" AUTO_RELOAD="true">
<configurations>
<configuration PROFILE_NAME="Debug" ENABLED="true" CONFIG_NAME="Debug" GENERATION_OPTIONS="-G &quot;Unix Makefiles&quot; -DKTE_USE_PIECE_TABLE:BOOL=ON -DBUILD_GUI:BOOL=ON" />
</configurations>
</component>
<component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real">
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
<option name="LAST_RESOLUTION" value="IGNORE" />
</component>
<component name="ClangdSettings">
<option name="formatViaClangd" value="false" />
</component>
<component name="ExecutionTargetManager" SELECTED_TARGET="CMakeBuildProfile:Debug" />
<component name="FormatOnSaveOptions">
<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:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
</component>
<component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" />
</component>
<component name="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" />
</component>
<component name="ProjectApplicationVersion">
<option name="ide" value="CLion" />
<option name="majorVersion" value="2025" />
<option name="minorVersion" value="2.5" />
<option name="productBranch" value="Classic" />
</component>
<component name="ProjectColorInfo">{
&quot;associatedIndex&quot;: 3
}</component>
<component name="ProjectId" id="36AlI8oyQOzOwSuZg6WxXf5LbHb" />
<component name="ProjectLevelVcsManager">
<OptionsSetting value="false" id="Update" />
</component>
<component name="ProjectViewState">
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
<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.git.unshallow": "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": "CMakeSettings",
"to.speed.mode.migration.done": "true",
"vue.rearranger.settings.migration": "true"
}
}]]></component>
<component name="RecentsManager">
<key name="MoveFile.RECENT_KEYS">
<recent name="$PROJECT_DIR$/docs" />
</key>
</component>
<component name="RunManager" selected="CMake Application.kge">
<configuration default="true" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="imgui" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="imgui" CONFIG_NAME="Debug">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="kge" type="CMakeRunConfiguration" factoryName="Application" PROGRAM_PARAMS="$PROJECT_DIR$/cmake-build-debug/test.txt" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" WORKING_DIR="file://$PROJECT_DIR$" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kge" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kge">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<configuration name="kte" type="CMakeRunConfiguration" factoryName="Application" REDIRECT_INPUT="false" ELEVATE="false" USE_EXTERNAL_CONSOLE="false" EMULATE_TERMINAL="false" PASS_PARENT_ENVS_2="true" PROJECT_NAME="kte" TARGET_NAME="kte" CONFIG_NAME="Debug" RUN_TARGET_PROJECT_NAME="kte" RUN_TARGET_NAME="kte">
<method v="2">
<option name="com.jetbrains.cidr.execution.CidrBuildBeforeRunTaskProvider$BuildBeforeRunTask" enabled="true" />
</method>
</configuration>
<list>
<item itemvalue="CMake Application.imgui" />
<item itemvalue="CMake Application.kge" />
<item itemvalue="CMake Application.kte" />
</list>
</component>
<component name="TaskManager">
<task active="true" id="Default" summary="Default task">
<changelist id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="" />
<created>1764457173148</created>
<option name="number" value="Default" />
<option name="presentableId" value="Default" />
<updated>1764457173148</updated>
<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="39312000" />
</task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" />
<created>1764485311566</created>
<option name="number" value="00001" />
<option name="presentableId" value="LOCAL-00001" />
<option name="project" value="LOCAL" />
<updated>1764485311566</updated>
</task>
<task id="LOCAL-00002" summary="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations.&#10;&#10;This uses either a GapBuffer or PieceTable depending on the compilation.">
<option name="closed" value="true" />
<created>1764486011231</created>
<option name="number" value="00002" />
<option name="presentableId" value="LOCAL-00002" />
<option name="project" value="LOCAL" />
<updated>1764486011231</updated>
</task>
<task id="LOCAL-00003" summary="Handle end-of-file newline semantics and improve scroll alignment logic.">
<option name="closed" value="true" />
<created>1764486876984</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1764486876984</updated>
</task>
<task id="LOCAL-00004" summary="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity.">
<option name="closed" value="true" />
<created>1764489870957</created>
<option name="number" value="00004" />
<option name="presentableId" value="LOCAL-00004" />
<option name="project" value="LOCAL" />
<updated>1764489870957</updated>
</task>
<task id="LOCAL-00005" summary="Add non-linear undo/redo design documentation and improve `UndoSystem` with backspace batching and GUI integration fixes.">
<option name="closed" value="true" />
<created>1764496151303</created>
<option name="number" value="00005" />
<option name="presentableId" value="LOCAL-00005" />
<option name="project" value="LOCAL" />
<updated>1764496151303</updated>
</task>
<task id="LOCAL-00006" summary="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.">
<option name="closed" value="true" />
<created>1764500200942</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1764500200942</updated>
</task>
<task id="LOCAL-00007" summary="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="closed" value="true" />
<created>1764501532446</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1764501532446</updated>
</task>
<task id="LOCAL-00008" summary="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built.">
<option name="closed" value="true" />
<created>1764502480274</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1764502480274</updated>
</task>
<task id="LOCAL-00009" summary="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling.">
<option name="closed" value="true" />
<created>1764505723411</created>
<option name="number" value="00009" />
<option name="presentableId" value="LOCAL-00009" />
<option name="project" value="LOCAL" />
<updated>1764505723411</updated>
</task>
<task id="LOCAL-00010" summary="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines.">
<option name="closed" value="true" />
<created>1764550164829</created>
<option name="number" value="00010" />
<option name="presentableId" value="LOCAL-00010" />
<option name="project" value="LOCAL" />
<updated>1764550164829</updated>
</task>
<task id="LOCAL-00011" summary="Add horizontal scrolling support and refactor mouse click handling in GUI.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- Replace redundant variable declarations in `Buffer` for cleaner code.">
<option name="closed" value="true" />
<created>1764551986561</created>
<option name="number" value="00011" />
<option name="presentableId" value="LOCAL-00011" />
<option name="project" value="LOCAL" />
<updated>1764551986561</updated>
</task>
<task id="LOCAL-00012" summary="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup.">
<option name="closed" value="true" />
<created>1764556512864</created>
<option name="number" value="00012" />
<option name="presentableId" value="LOCAL-00012" />
<option name="project" value="LOCAL" />
<updated>1764556512864</updated>
</task>
<task id="LOCAL-00013" summary="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0.">
<option name="closed" value="true" />
<created>1764556854788</created>
<option name="number" value="00013" />
<option name="presentableId" value="LOCAL-00013" />
<option name="project" value="LOCAL" />
<updated>1764556854788</updated>
</task>
<task id="LOCAL-00014" summary="Actually add the screenshot.">
<option name="closed" value="true" />
<created>1764557759844</created>
<option name="number" value="00014" />
<option name="presentableId" value="LOCAL-00014" />
<option name="project" value="LOCAL" />
<updated>1764557759844</updated>
</task>
<task id="LOCAL-00015" summary="Fix void crash in kge.">
<option name="closed" value="true" />
<created>1764568264996</created>
<option name="number" value="00015" />
<option name="presentableId" value="LOCAL-00015" />
<option name="project" value="LOCAL" />
<updated>1764568264996</updated>
</task>
<task id="LOCAL-00016" summary="add regex and search/replace functionality to editor">
<option name="closed" value="true" />
<created>1764574397967</created>
<option name="number" value="00016" />
<option name="presentableId" value="LOCAL-00016" />
<option name="project" value="LOCAL" />
<updated>1764574397967</updated>
</task>
<task id="LOCAL-00017" summary="Add regex search, search/replace, and buffer read-only mode functionality with help text">
<option name="closed" value="true" />
<created>1764586480092</created>
<option name="number" value="00017" />
<option name="presentableId" value="LOCAL-00017" />
<option name="project" value="LOCAL" />
<updated>1764586480092</updated>
</task>
<task id="LOCAL-00018" summary="Nord theme and undo system refinements&#10;&#10;- Improve text input/event batching&#10;- Enhance debugging with optional instrumentation&#10;- Begin implementation of non-linear undo tree structure.">
<option name="closed" value="true" />
<created>1764619193516</created>
<option name="number" value="00018" />
<option name="presentableId" value="LOCAL-00018" />
<option name="project" value="LOCAL" />
<updated>1764619193517</updated>
</task>
<task id="LOCAL-00019" summary="Add Nord theme for real">
<option name="closed" value="true" />
<created>1764619210817</created>
<option name="number" value="00019" />
<option name="presentableId" value="LOCAL-00019" />
<option name="project" value="LOCAL" />
<updated>1764619210817</updated>
</task>
<option name="localTasksCounter" value="20" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
<option name="version" value="3" />
</component>
<component name="VCPKGProject">
<isAutomaticCheckingOnLaunch value="false" />
<isAutomaticFoundErrors value="true" />
<isAutomaticReloadCMake value="true" />
</component>
<component name="VcsManagerConfiguration">
<MESSAGE value="Refactoring" />
<MESSAGE value="Add undo/redo infrastructure and buffer management additions." />
<MESSAGE value="Refactor `Buffer` to use `Line` abstraction and improve handling of row operations.&#10;&#10;This uses either a GapBuffer or PieceTable depending on the compilation." />
<MESSAGE value="Handle end-of-file newline semantics and improve scroll alignment logic." />
<MESSAGE value="Enable installation targets." />
<MESSAGE value="Add `UndoSystem` implementation and refactor `UndoNode` for simplicity." />
<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`." />
<MESSAGE value="Add man pages for `kge` and `kte` with installation targets in CMake.&#10;&#10;- Introduce `docs/kge.1` and `docs/kte.1` man pages covering usage, options, keybindings, and examples.&#10;- Update `CMakeLists.txt` to install man pages under `${CMAKE_INSTALL_MANDIR}/man1`.&#10;- Ensure `kge` man page installation is conditional on GUI being built." />
<MESSAGE value="Add GUI initialization updates and improve navigation commands.&#10;&#10;- Implement terminal detachment for GUI mode to enable terminal closure post-launch.&#10;- Add `+N` support for opening files at specific line numbers and refine cursor positioning.&#10;- Introduce `JumpToLine` command for direct navigation by line number.&#10;- Enhance mouse wheel handling for line-wise scrolling." />
<MESSAGE value="Refactor code for consistency and enhanced functionality.&#10;&#10;- Normalize path handling for buffer operations, supporting tilde expansion and absolute paths.&#10;- Introduce `DisplayNameFor` to uniquely resolve buffer display names, minimizing filename clashes.&#10;- Add new commands: `ShowWorkingDirectory` and `ChangeWorkingDirectory`.&#10;- Refine keybindings and enhance existing commands for improved command flow.&#10;- Adjust GUI and terminal renderers to display total line counts alongside filenames.&#10;- Update coding style to align with project guidelines." />
<MESSAGE value="Add horizontal scrolling support and refactor mouse click handling in GUI.&#10;&#10;- Introduce horizontal scrolling with column offset synchronization in GUI.&#10;- Refactor mouse click handling for improved accuracy and viewport alignment.&#10;- Enhance tab expansion and cursor rendering logic for better user experience.&#10;- Replace redundant variable declarations in `Buffer` for cleaner code." />
<MESSAGE value="Introduce file picker and GUI configuration with enhancements.&#10;&#10;- Add visual file picker for GUI with toggle support.&#10;- Introduce `GUIConfig` class for loading GUI settings from configuration file.&#10;- Refactor window initialization to support dynamic sizing based on configuration.&#10;- Add macOS-specific handling for fullscreen behavior.&#10;- Improve header inclusion order and minor code cleanup." />
<MESSAGE value="Add buffer position display and documentation improvements.&#10;&#10;- Display buffer position prefix &quot;[x/N]&quot; in GUI and terminal renderers.&#10;- Improve `kte` and `kge` man pages with frontend usage details and project homepage.&#10;- Update README with GUI invocation instructions.&#10;- Bump version to 1.0.0." />
<MESSAGE value="Actually add the screenshot." />
<MESSAGE value="Fix void crash in kge." />
<MESSAGE value="add regex and search/replace functionality to editor" />
<MESSAGE value="Add regex search, search/replace, and buffer read-only mode functionality with help text" />
<MESSAGE value="Nord theme and undo system refinements&#10;&#10;- Improve text input/event batching&#10;- Enhance debugging with optional instrumentation&#10;- Begin implementation of non-linear undo tree structure." />
<MESSAGE value="Add Nord theme for real" />
<option name="LAST_COMMIT_MESSAGE" value="Add Nord theme for real" />
</component>
<component name="XSLT-Support.FileAssociations.UIState">
<expand />
<select />
</component>
</project>

View File

@@ -6,6 +6,9 @@
#include "Buffer.h"
#include "UndoSystem.h"
#include "UndoTree.h"
// For reconstructing highlighter state on copies
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
Buffer::Buffer()
@@ -40,9 +43,32 @@ Buffer::Buffer(const Buffer &other)
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
// Copy syntax/highlighting flags
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Fresh undo system for the copy
undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate a highlighter engine for this copy based on filetype/syntax state
if (syntax_enabled_) {
// Allocate engine and install an appropriate highlighter
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
// Unsupported filetype -> NullHighlighter keeps syntax pipeline active
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
// No filetype -> keep syntax enabled but use NullHighlighter
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
// Fresh engine has empty caches; nothing to invalidate
}
}
@@ -65,9 +91,28 @@ Buffer::operator=(const Buffer &other)
mark_set_ = other.mark_set_;
mark_curx_ = other.mark_curx_;
mark_cury_ = other.mark_cury_;
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = other.filetype_;
// Recreate undo system for this instance
undo_tree_ = std::make_unique<UndoTree>();
undo_sys_ = std::make_unique<UndoSystem>(*this, *undo_tree_);
// Recreate highlighter engine consistent with syntax settings
highlighter_.reset();
if (syntax_enabled_) {
highlighter_ = std::make_unique<kte::HighlighterEngine>();
if (!filetype_.empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(filetype_);
if (hl) {
highlighter_->SetHighlighter(std::move(hl));
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
highlighter_->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
}
return *this;
}
@@ -91,6 +136,11 @@ Buffer::Buffer(Buffer &&other) noexcept
undo_tree_(std::move(other.undo_tree_)),
undo_sys_(std::move(other.undo_sys_))
{
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object
if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this);
@@ -122,6 +172,12 @@ Buffer::operator=(Buffer &&other) noexcept
undo_tree_ = std::move(other.undo_tree_);
undo_sys_ = std::move(other.undo_sys_);
// Move syntax/highlighting state
version_ = other.version_;
syntax_enabled_ = other.syntax_enabled_;
filetype_ = std::move(other.filetype_);
highlighter_ = std::move(other.highlighter_);
// Update UndoSystem's buffer reference to point to this object
if (undo_sys_) {
undo_sys_->UpdateBufferReference(*this);

View File

@@ -12,6 +12,10 @@
#include "AppendBuffer.h"
#include "UndoSystem.h"
#include <cstdint>
#include <memory>
#include "syntax/HighlighterEngine.h"
#include "Highlight.h"
class Buffer {
@@ -262,6 +266,7 @@ public:
return filename_;
}
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
// This does not mark the buffer as file-backed.
void SetVirtualName(const std::string &name)
@@ -282,17 +287,20 @@ public:
return dirty_;
}
// Read-only flag
[[nodiscard]] bool IsReadOnly() const
{
return read_only_;
}
void SetReadOnly(bool ro)
{
read_only_ = ro;
}
void ToggleReadOnly()
{
read_only_ = !read_only_;
@@ -322,6 +330,12 @@ public:
void SetDirty(bool d)
{
dirty_ = d;
if (d) {
++version_;
if (highlighter_) {
highlighter_->InvalidateFrom(0);
}
}
}
@@ -360,6 +374,56 @@ public:
[[nodiscard]] std::string AsString() const;
// Syntax highlighting integration (per-buffer)
[[nodiscard]] std::uint64_t Version() const
{
return version_;
}
void SetSyntaxEnabled(bool on)
{
syntax_enabled_ = on;
}
[[nodiscard]] bool SyntaxEnabled() const
{
return syntax_enabled_;
}
void SetFiletype(const std::string &ft)
{
filetype_ = ft;
}
[[nodiscard]] const std::string &Filetype() const
{
return filetype_;
}
kte::HighlighterEngine *Highlighter()
{
return highlighter_.get();
}
const kte::HighlighterEngine *Highlighter() const
{
return highlighter_.get();
}
void EnsureHighlighter()
{
if (!highlighter_)
highlighter_ = std::make_unique<kte::HighlighterEngine>();
}
// Raw, low-level editing APIs used by UndoSystem apply().
// These must NOT trigger undo recording. They also do not move the cursor.
void insert_text(int row, int col, std::string_view text);
@@ -396,6 +460,12 @@ private:
// Per-buffer undo state
std::unique_ptr<struct UndoTree> undo_tree_;
std::unique_ptr<UndoSystem> undo_sys_;
// Syntax/highlighting state
std::uint64_t version_ = 0; // increment on edits
bool syntax_enabled_ = true;
std::string filetype_;
std::unique_ptr<kte::HighlighterEngine> highlighter_;
};
#endif // KTE_BUFFER_H

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.1.0")
set(KTE_VERSION "1.2.2")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -13,6 +13,7 @@ set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
if (CMAKE_HOST_UNIX)
message(STATUS "Build system is POSIX.")
@@ -37,6 +38,9 @@ else ()
endif ()
add_compile_definitions(KGE_PLATFORM=${CMAKE_HOST_SYSTEM_NAME})
add_compile_definitions(KTE_VERSION_STR="v${KTE_VERSION}")
if (KTE_ENABLE_TREESITTER)
add_compile_definitions(KTE_ENABLE_TREESITTER)
endif ()
message(STATUS "Build system: ${CMAKE_HOST_SYSTEM_NAME}")
@@ -50,6 +54,29 @@ set(CURSES_NEED_WIDE)
find_package(Curses REQUIRED)
include_directories(${CURSES_INCLUDE_DIR})
set(SYNTAX_SOURCES
syntax/GoHighlighter.cc
syntax/CppHighlighter.cc
syntax/JsonHighlighter.cc
syntax/ErlangHighlighter.cc
syntax/MarkdownHighlighter.cc
syntax/TreeSitterHighlighter.cc
syntax/LispHighlighter.cc
syntax/HighlighterEngine.cc
syntax/RustHighlighter.cc
syntax/HighlighterRegistry.cc
syntax/SqlHighlighter.cc
syntax/NullHighlighter.cc
syntax/ForthHighlighter.cc
syntax/PythonHighlighter.cc
syntax/ShellHighlighter.cc
)
if (KTE_ENABLE_TREESITTER)
list(APPEND SYNTAX_SOURCES
TreeSitterHighlighter.cc)
endif ()
set(COMMON_SOURCES
GapBuffer.cc
PieceTable.cc
@@ -67,6 +94,41 @@ set(COMMON_SOURCES
UndoNode.cc
UndoTree.cc
UndoSystem.cc
${SYNTAX_SOURCES}
)
set(SYNTAX_HEADERS
syntax/GoHighlighter.h
syntax/HighlighterEngine.h
syntax/ShellHighlighter.h
syntax/MarkdownHighlighter.h
syntax/LispHighlighter.h
syntax/SqlHighlighter.h
syntax/ForthHighlighter.h
syntax/JsonHighlighter.h
syntax/TreeSitterHighlighter.h
syntax/NullHighlighter.h
syntax/CppHighlighter.h
syntax/ErlangHighlighter.h
syntax/LanguageHighlighter.h
syntax/RustHighlighter.h
syntax/PythonHighlighter.h
)
if (KTE_ENABLE_TREESITTER)
list(APPEND THEME_HEADERS
TreeSitterHighlighter.h)
endif ()
set(THEME_HEADERS
themes/ThemeHelpers.h
themes/EInk.h
themes/Gruvbox.h
themes/Solarized.h
themes/Plan9.h
themes/Nord.h
)
set(COMMON_HEADERS
@@ -90,6 +152,10 @@ set(COMMON_HEADERS
UndoNode.h
UndoTree.h
UndoSystem.h
Highlight.h
${SYNTAX_HEADERS}
${THEME_HEADERS}
)
# kte (terminal-first) executable
@@ -108,6 +174,18 @@ endif ()
target_link_libraries(kte ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER)
# Users can provide their own tree-sitter include/lib via cache variables
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
set(TREESITTER_LIBRARY "" CACHE FILEPATH "Path to tree-sitter library (.a/.dylib)")
if (TREESITTER_INCLUDE_DIR)
target_include_directories(kte PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(kte ${TREESITTER_LIBRARY})
endif ()
endif ()
install(TARGETS kte
RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)
@@ -133,6 +211,14 @@ if (BUILD_TESTS)
target_link_libraries(test_undo ${CURSES_LIBRARIES})
if (KTE_ENABLE_TREESITTER)
if (TREESITTER_INCLUDE_DIR)
target_include_directories(test_undo PRIVATE ${TREESITTER_INCLUDE_DIR})
endif ()
if (TREESITTER_LIBRARY)
target_link_libraries(test_undo ${TREESITTER_LIBRARY})
endif ()
endif ()
endif ()
if (${BUILD_GUI})

View File

@@ -4,12 +4,21 @@
#include <regex>
#include <fstream>
#include <sstream>
#include <cctype>
#include "Command.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#include "Editor.h"
#include "Buffer.h"
#include "UndoSystem.h"
#include "HelpText.h"
#include "syntax/LanguageHighlighter.h"
#include "syntax/HighlighterEngine.h"
#include "syntax/CppHighlighter.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#endif
// Keep buffer viewport offsets so that the cursor stays within the visible
@@ -33,12 +42,11 @@ compute_render_x(const std::string &line, const std::size_t curx, const std::siz
static void
ensure_cursor_visible(const Editor &ed, Buffer &buf)
{
const std::size_t rows = ed.Rows();
const std::size_t cols = ed.Cols();
if (rows == 0 || cols == 0)
if (cols == 0)
return;
const std::size_t content_rows = rows > 0 ? rows - 1 : 0; // last row = status
const std::size_t content_rows = ed.ContentRows();
const std::size_t cury = buf.Cury();
const std::size_t curx = buf.Curx();
std::size_t rowoffs = buf.Rowoffs();
@@ -87,8 +95,10 @@ ensure_at_least_one_line(Buffer &buf)
}
}
// Determine if a command mutates the buffer contents (text edits)
static bool is_mutating_command(CommandId id)
static bool
is_mutating_command(CommandId id)
{
switch (id) {
case CommandId::InsertText:
@@ -723,6 +733,21 @@ cmd_kprefix(CommandContext &ctx)
}
// Start generic command prompt (": ")
static bool
cmd_command_prompt_start(const CommandContext &ctx)
{
// Close any pending edit batch before entering prompt
if (Buffer *b = ctx.editor.CurrentBuffer()) {
if (auto *u = b->Undo())
u->commit();
}
ctx.editor.StartPrompt(Editor::PromptKind::Command, "", "");
ctx.editor.SetStatus(": ");
return true;
}
static bool
cmd_unknown_kcommand(CommandContext &ctx)
{
@@ -737,8 +762,272 @@ cmd_unknown_kcommand(CommandContext &ctx)
}
// --- Syntax highlighting commands ---
static void
apply_filetype(Buffer &buf, const std::string &ft)
{
buf.EnsureHighlighter();
auto *eng = buf.Highlighter();
if (!eng)
return;
std::string val = ft;
// trim + lower
auto trim = [](const std::string &s) {
std::string r = s;
auto notsp = [](int ch) {
return !std::isspace(ch);
};
r.erase(r.begin(), std::find_if(r.begin(), r.end(), notsp));
r.erase(std::find_if(r.rbegin(), r.rend(), notsp).base(), r.end());
return r;
};
val = trim(val);
for (auto &ch: val)
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (val == "off") {
eng->SetHighlighter(nullptr);
buf.SetFiletype("");
buf.SetSyntaxEnabled(false);
return;
}
if (val.empty()) {
// Empty means unknown/unspecified -> use NullHighlighter but keep syntax enabled
buf.SetFiletype("");
buf.SetSyntaxEnabled(true);
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
return;
}
// Normalize and create via registry
std::string norm = kte::HighlighterRegistry::Normalize(val);
auto hl = kte::HighlighterRegistry::CreateFor(norm);
if (hl) {
eng->SetHighlighter(std::move(hl));
buf.SetFiletype(norm);
buf.SetSyntaxEnabled(true);
eng->InvalidateFrom(0);
} else {
// Unknown -> install NullHighlighter and keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
buf.SetFiletype(val); // record what user asked even if unsupported
buf.SetSyntaxEnabled(true);
eng->InvalidateFrom(0);
}
}
static bool
cmd_find_start(CommandContext &ctx)
cmd_syntax(CommandContext &ctx)
{
Buffer *b = ctx.editor.CurrentBuffer();
if (!b) {
ctx.editor.SetStatus("No buffer");
return true;
}
std::string arg = ctx.arg;
// trim
auto trim = [](std::string &s) {
auto notsp = [](int ch) {
return !std::isspace(ch);
};
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
};
trim(arg);
if (arg == "on") {
b->SetSyntaxEnabled(true);
// If no highlighter but filetype is cpp by extension, set it
if (!b->Highlighter() || !b->Highlighter()->HasHighlighter()) {
apply_filetype(*b, b->Filetype().empty() ? std::string("cpp") : b->Filetype());
}
ctx.editor.SetStatus("syntax: on");
} else if (arg == "off") {
b->SetSyntaxEnabled(false);
ctx.editor.SetStatus("syntax: off");
} else if (arg == "reload") {
if (auto *eng = b->Highlighter())
eng->InvalidateFrom(0);
ctx.editor.SetStatus("syntax: reloaded");
} else {
ctx.editor.SetStatus("usage: :syntax on|off|reload");
}
return true;
}
static bool
cmd_set_option(CommandContext &ctx)
{
Buffer *b = ctx.editor.CurrentBuffer();
if (!b) {
ctx.editor.SetStatus("No buffer");
return true;
}
// Expect key=value
auto eq = ctx.arg.find('=');
if (eq == std::string::npos) {
ctx.editor.SetStatus("usage: :set key=value");
return true;
}
std::string key = ctx.arg.substr(0, eq);
std::string val = ctx.arg.substr(eq + 1);
// trim
auto trim = [](std::string &s) {
auto notsp = [](int ch) {
return !std::isspace(ch);
};
s.erase(s.begin(), std::find_if(s.begin(), s.end(), notsp));
s.erase(std::find_if(s.rbegin(), s.rend(), notsp).base(), s.end());
};
trim(key);
trim(val);
// lower-case value for filetype
for (auto &ch: val)
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (key == "filetype") {
apply_filetype(*b, val);
if (b->SyntaxEnabled())
ctx.editor.SetStatus(
std::string("filetype: ") + (b->Filetype().empty() ? "off" : b->Filetype()));
else
ctx.editor.SetStatus("filetype: off");
return true;
}
ctx.editor.SetStatus("unknown option: " + key);
return true;
}
// GUI theme cycling commands (available in GUI build; show message otherwise)
#ifdef KTE_BUILD_GUI
static bool
cmd_theme_next(CommandContext &ctx)
{
auto id = kte::NextTheme();
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
return true;
}
static bool
cmd_theme_prev(CommandContext &ctx)
{
auto id = kte::PrevTheme();
ctx.editor.SetStatus(std::string("Theme: ") + kte::ThemeName(id));
return true;
}
#else
static bool
cmd_theme_next(CommandContext &ctx)
{
ctx.editor.SetStatus("Theme switching only available in GUI build");
return true;
}
static bool
cmd_theme_prev(CommandContext &ctx)
{
ctx.editor.SetStatus("Theme switching only available in GUI build");
return true;
}
#endif
// Theme set by name command
#ifdef KTE_BUILD_GUI
static bool
cmd_theme_set_by_name(const CommandContext &ctx)
{
std::string name = ctx.arg;
// trim spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(name);
rtrim(name);
if (name.empty()) {
ctx.editor.SetStatus("theme: missing name");
return true;
}
if (kte::ApplyThemeByName(name)) {
ctx.editor.SetStatus(
std::string("Theme: ") + name + std::string(" (bg: ") + kte::BackgroundModeName() + ")");
} else {
// Build list of available themes
const auto &reg = kte::ThemeRegistry();
std::string avail;
for (size_t i = 0; i < reg.size(); ++i) {
if (i)
avail += ", ";
avail += reg[i]->Name();
}
ctx.editor.SetStatus(std::string("Unknown theme; available: ") + avail);
}
return true;
}
#else
static bool
cmd_theme_set_by_name(CommandContext &ctx)
{
(void) ctx;
// No-op in terminal build
return true;
}
#endif
// Background set command (GUI)
#ifdef KTE_BUILD_GUI
static bool
cmd_background_set(const CommandContext &ctx)
{
std::string mode = ctx.arg;
// trim
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(mode);
rtrim(mode);
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (mode != "light" && mode != "dark") {
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
return true;
}
kte::SetBackgroundMode(mode == "light" ? kte::BackgroundMode::Light : kte::BackgroundMode::Dark);
// Re-apply current theme to reflect background change
kte::ApplyThemeByName(kte::CurrentThemeName());
ctx.editor.SetStatus(std::string("Background: ") + mode + std::string("; Theme: ") + kte::CurrentThemeName());
return true;
}
#else
static bool
cmd_background_set(CommandContext &ctx)
{
(void) ctx;
return true;
}
#endif
static bool
cmd_find_start(const CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
@@ -761,7 +1050,7 @@ cmd_find_start(CommandContext &ctx)
static bool
cmd_regex_find_start(CommandContext &ctx)
cmd_regex_find_start(const CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
@@ -784,7 +1073,7 @@ cmd_regex_find_start(CommandContext &ctx)
static bool
cmd_search_replace_start(CommandContext &ctx)
cmd_search_replace_start(const CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
@@ -808,7 +1097,7 @@ cmd_search_replace_start(CommandContext &ctx)
static bool
cmd_regex_replace_start(CommandContext &ctx)
cmd_regex_replace_start(const CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
@@ -832,7 +1121,7 @@ cmd_regex_replace_start(CommandContext &ctx)
static bool
cmd_open_file_start(CommandContext &ctx)
cmd_open_file_start(const CommandContext &ctx)
{
// Start a generic prompt to read a path
ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", "");
@@ -843,7 +1132,7 @@ 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)
cmd_visual_file_picker_toggle(const CommandContext &ctx)
{
// Toggle visibility
bool show = !ctx.editor.FilePickerVisible();
@@ -866,7 +1155,7 @@ cmd_visual_file_picker_toggle(CommandContext &ctx)
static bool
cmd_jump_to_line_start(CommandContext &ctx)
cmd_jump_to_line_start(const CommandContext &ctx)
{
// Start a prompt to read a 1-based line number and jump there (clamped)
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
@@ -877,7 +1166,7 @@ cmd_jump_to_line_start(CommandContext &ctx)
// --- Buffers: switch/next/prev/close ---
static bool
cmd_buffer_switch_start(CommandContext &ctx)
cmd_buffer_switch_start(const CommandContext &ctx)
{
// If only one (or zero) buffer is open, do nothing per spec
if (ctx.editor.BufferCount() <= 1) {
@@ -895,7 +1184,7 @@ buffer_display_name(const Buffer &b)
{
if (!b.Filename().empty())
return b.Filename();
return std::string("<untitled>");
return {"<untitled>"};
}
@@ -904,7 +1193,7 @@ buffer_basename(const Buffer &b)
{
const std::string &p = b.Filename();
if (p.empty())
return std::string("<untitled>");
return {"<untitled>"};
auto pos = p.find_last_of("/\\");
if (pos == std::string::npos)
return p;
@@ -913,7 +1202,7 @@ buffer_basename(const Buffer &b)
static bool
cmd_buffer_next(CommandContext &ctx)
cmd_buffer_next(const CommandContext &ctx)
{
const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) {
@@ -930,7 +1219,7 @@ cmd_buffer_next(CommandContext &ctx)
static bool
cmd_buffer_prev(CommandContext &ctx)
cmd_buffer_prev(const CommandContext &ctx)
{
const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) {
@@ -947,7 +1236,7 @@ cmd_buffer_prev(CommandContext &ctx)
static bool
cmd_buffer_close(CommandContext &ctx)
cmd_buffer_close(const CommandContext &ctx)
{
if (ctx.editor.BufferCount() == 0)
return true;
@@ -1116,6 +1405,85 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true;
}
// Generic command prompt completion
if (kind == Editor::PromptKind::Command) {
std::string text = ctx.editor.PromptText();
// Split into command and arg prefix
auto sp = text.find(' ');
if (sp == std::string::npos) {
// complete command name from public commands
std::string prefix = text;
std::vector<std::string> names;
for (const auto &c: CommandRegistry::All()) {
if (c.isPublic) {
if (prefix.empty() || c.name.rfind(prefix, 0) == 0)
names.push_back(c.name);
}
}
if (names.empty()) {
// no change
} else if (names.size() == 1) {
ctx.editor.SetPromptText(names[0]);
} else {
// compute LCP
std::string lcp = names[0];
for (size_t i = 1; i < names.size(); ++i) {
const std::string &s = names[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 != text)
ctx.editor.SetPromptText(lcp);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
} else {
std::string cmd = text.substr(0, sp);
std::string argprefix = text.substr(sp + 1);
// Only special-case argument completion for certain commands
if (cmd == "theme") {
#ifdef KTE_BUILD_GUI
std::vector<std::string> cands;
const auto &reg = kte::ThemeRegistry();
for (const auto &t: reg) {
std::string n = t->Name();
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
cands.push_back(n);
}
if (cands.empty()) {
// no change
} else if (cands.size() == 1) {
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
} else {
std::string lcp = cands[0];
for (size_t i = 1; i < cands.size(); ++i) {
const std::string &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 != argprefix)
ctx.editor.SetPromptText(cmd + std::string(" ") + lcp);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
#else
(void) argprefix; // no completion in non-GUI build
#endif
}
// default: no special arg completion
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
}
}
}
ctx.editor.AppendPromptText(ctx.arg);
@@ -1223,6 +1591,7 @@ cmd_insert_text(CommandContext &ctx)
return true;
}
// Toggle read-only state of the current buffer
static bool
cmd_toggle_read_only(CommandContext &ctx)
@@ -1258,8 +1627,10 @@ cmd_show_help(CommandContext &ctx)
std::ostringstream out;
std::string line;
auto unquote = [](std::string s) {
if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin());
if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back();
if (!s.empty() && (s.front() == '"' || s.front() == '\''))
s.erase(s.begin());
if (!s.empty() && (s.back() == '"' || s.back() == '\''))
s.pop_back();
return s;
};
while (std::getline(iss, line)) {
@@ -1275,17 +1646,20 @@ cmd_show_help(CommandContext &ctx)
std::string title;
std::getline(ls, title);
// trim leading spaces
while (!title.empty() && (title.front() == ' ' || title.front() == '\t')) title.erase(title.begin());
while (!title.empty() && (title.front() == ' ' || title.front() == '\t'))
title.erase(title.begin());
title = unquote(title);
out << "\n\n";
for (auto &c : title) c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
for (auto &c: title)
c = static_cast<char>(std::toupper(static_cast<unsigned char>(c)));
out << title << "\n";
} else if (macro == "PP" || macro == "P" || macro == "TP") {
out << "\n";
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
std::string rest;
std::getline(ls, rest);
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t')) rest.erase(rest.begin());
while (!rest.empty() && (rest.front() == ' ' || rest.front() == '\t'))
rest.erase(rest.begin());
out << unquote(rest) << "\n";
} else if (macro == "nf" || macro == "fi") {
// ignore fill mode toggles for now
@@ -1297,11 +1671,23 @@ cmd_show_help(CommandContext &ctx)
// Regular text; apply minimal escape replacements
for (std::size_t i = 0; i < line.size(); ++i) {
if (line[i] == '\\') {
if (i + 1 < line.size() && line[i + 1] == '-') { out << '-'; ++i; continue; }
if (i + 1 < line.size() && line[i + 1] == '-') {
out << '-';
++i;
continue;
}
if (i + 3 < line.size() && line[i + 1] == '(') {
std::string esc = line.substr(i + 2, 2);
if (esc == "em") { out << ""; i += 3; continue; }
if (esc == "en") { out << "-"; i += 3; continue; }
if (esc == "em") {
out << "";
i += 3;
continue;
}
if (esc == "en") {
out << "-";
i += 3;
continue;
}
}
}
out << line[i];
@@ -1315,7 +1701,10 @@ cmd_show_help(CommandContext &ctx)
// 1) Prefer embedded/customizable help content
{
std::string embedded = HelpText::Text();
if (!embedded.empty()) { used_man = false; return embedded; }
if (!embedded.empty()) {
used_man = false;
return embedded;
}
}
// 2) Fall back to the manpage and convert roff to plain text
@@ -1325,11 +1714,14 @@ cmd_show_help(CommandContext &ctx)
"/usr/local/share/man/man1/kte.1",
"/usr/share/man/man1/kte.1"
};
for (const char *p : man_candidates) {
for (const char *p: man_candidates) {
std::ifstream in(p);
if (in.good()) {
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>());
if (!s.empty()) { used_man = true; return roff_to_text(s); }
if (!s.empty()) {
used_man = true;
return roff_to_text(s);
}
}
}
// Fallback minimal help text
@@ -1378,7 +1770,7 @@ cmd_show_help(CommandContext &ctx)
rows.clear();
std::string line;
line.reserve(128);
for (char ch : text) {
for (char ch: text) {
if (ch == '\n') {
rows.emplace_back(line);
line.clear();
@@ -1430,6 +1822,46 @@ cmd_newline(CommandContext &ctx)
Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
std::string value = ctx.editor.PromptText();
ctx.editor.AcceptPrompt();
if (kind == Editor::PromptKind::Command) {
// Parse COMMAND ARG and dispatch only public commands
// Trim leading/trailing spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim(value);
rtrim(value);
if (value.empty()) {
ctx.editor.SetStatus("Canceled");
return true;
}
// Split first token
std::string cmdname;
std::string arg;
auto sp = value.find(' ');
if (sp == std::string::npos) {
cmdname = value;
} else {
cmdname = value.substr(0, sp);
arg = value.substr(sp + 1);
}
const Command *cmd = CommandRegistry::FindByName(cmdname);
if (!cmd || !cmd->isPublic) {
ctx.editor.SetStatus(std::string("Unknown command: ") + cmdname);
return true;
}
bool ok = Execute(ctx.editor, cmdname, arg);
if (!ok) {
ctx.editor.SetStatus(std::string("Command failed: ") + cmdname);
}
return true;
}
if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
// Finish search: keep cursor where it is, clear search UI prompt
ctx.editor.SetSearchActive(false);
@@ -1786,7 +2218,7 @@ cmd_newline(CommandContext &ctx)
}
auto &rows = buf->Rows();
std::size_t changed = 0;
for (auto &line : rows) {
for (auto &line: rows) {
std::string before = static_cast<std::string>(line);
std::string after = std::regex_replace(before, rx, repl);
if (after != before) {
@@ -2571,9 +3003,7 @@ cmd_page_up(CommandContext &ctx)
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1;
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
if (content_rows == 0)
content_rows = 1;
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
// Base on current top-of-screen (row offset)
std::size_t rowoffs = buf->Rowoffs();
@@ -2597,7 +3027,6 @@ cmd_page_up(CommandContext &ctx)
y = rows.empty() ? 0 : rows.size() - 1;
buf->SetOffsets(rowoffs, 0);
buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
@@ -2613,9 +3042,7 @@ cmd_page_down(CommandContext &ctx)
ensure_at_least_one_line(*buf);
auto &rows = buf->Rows();
int repeat = ctx.count > 0 ? ctx.count : 1;
std::size_t content_rows = ctx.editor.Rows() > 0 ? ctx.editor.Rows() - 1 : 0;
if (content_rows == 0)
content_rows = 1;
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
std::size_t rowoffs = buf->Rowoffs();
// Compute maximum top offset
@@ -2636,7 +3063,74 @@ cmd_page_down(CommandContext &ctx)
std::size_t y = std::min<std::size_t>(rowoffs, rows.empty() ? 0 : rows.size() - 1);
buf->SetOffsets(rowoffs, 0);
buf->SetCursor(0, y);
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool
cmd_scroll_up(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
ensure_at_least_one_line(*buf);
const auto &rows = buf->Rows();
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
std::size_t rowoffs = buf->Rowoffs();
// Scroll up by 3 lines (or count if specified), without moving cursor
int scroll_amount = ctx.count > 0 ? ctx.count : 3;
if (rowoffs >= static_cast<std::size_t>(scroll_amount))
rowoffs -= static_cast<std::size_t>(scroll_amount);
else
rowoffs = 0;
buf->SetOffsets(rowoffs, buf->Coloffs());
// If cursor is now below the visible area, move it to the last visible line
std::size_t cury = buf->Cury();
if (cury >= rowoffs + content_rows) {
std::size_t new_y = rowoffs + content_rows - 1;
if (new_y >= rows.size() && !rows.empty())
new_y = rows.size() - 1;
buf->SetCursor(buf->Curx(), new_y);
}
return true;
}
static bool
cmd_scroll_down(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf)
return false;
ensure_at_least_one_line(*buf);
const auto &rows = buf->Rows();
std::size_t content_rows = std::max<std::size_t>(1, ctx.editor.ContentRows());
std::size_t rowoffs = buf->Rowoffs();
// Scroll down by 3 lines (or count if specified), without moving cursor
int scroll_amount = ctx.count > 0 ? ctx.count : 3;
// Compute maximum top offset
std::size_t max_top = 0;
if (!rows.empty() && rows.size() > content_rows)
max_top = rows.size() - content_rows;
rowoffs += static_cast<std::size_t>(scroll_amount);
if (rowoffs > max_top)
rowoffs = max_top;
buf->SetOffsets(rowoffs, buf->Coloffs());
// If cursor is now above the visible area, move it to the first visible line
std::size_t cury = buf->Cury();
if (cury < rowoffs) {
buf->SetCursor(buf->Curx(), rowoffs);
}
return true;
}
@@ -3249,6 +3743,8 @@ InstallDefaultCommands()
CommandRegistry::Register({CommandId::MoveEnd, "end", "Move to end of line", cmd_move_end});
CommandRegistry::Register({CommandId::PageUp, "page-up", "Page up", cmd_page_up});
CommandRegistry::Register({CommandId::PageDown, "page-down", "Page down", cmd_page_down});
CommandRegistry::Register({CommandId::ScrollUp, "scroll-up", "Scroll viewport up", cmd_scroll_up});
CommandRegistry::Register({CommandId::ScrollDown, "scroll-down", "Scroll viewport down", cmd_scroll_down});
CommandRegistry::Register({CommandId::WordPrev, "word-prev", "Move to previous word", cmd_word_prev});
CommandRegistry::Register({CommandId::WordNext, "word-next", "Move to next word", cmd_word_next});
CommandRegistry::Register({
@@ -3275,7 +3771,25 @@ InstallDefaultCommands()
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
});
// Read-only
CommandRegistry::Register({CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only});
CommandRegistry::Register({
CommandId::ToggleReadOnly, "toggle-read-only", "Toggle buffer read-only", cmd_toggle_read_only
});
// GUI Themes
CommandRegistry::Register({CommandId::ThemeNext, "theme-next", "Cycle to next GUI theme", cmd_theme_next});
CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev});
// Theme by name (public in command prompt)
CommandRegistry::Register({
CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true
});
// Background light/dark (public)
CommandRegistry::Register({
CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true
});
// Generic command prompt (C-k ;)
CommandRegistry::Register({
CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt",
cmd_command_prompt_start
});
// Buffer operations
CommandRegistry::Register({
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
@@ -3305,6 +3819,9 @@ InstallDefaultCommands()
// UI helpers
CommandRegistry::Register(
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
// Syntax highlighting (public commands)
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
}

View File

@@ -58,6 +58,8 @@ enum class CommandId {
MoveEnd,
PageUp,
PageDown,
ScrollUp, // scroll viewport up (towards beginning) without moving cursor
ScrollDown, // scroll viewport down (towards end) without moving cursor
WordPrev,
WordNext,
DeleteWordPrev, // delete previous word (ESC BACKSPACE)
@@ -69,6 +71,9 @@ enum class CommandId {
Redo,
// UI/status helpers
UArgStatus, // update status line during universal-argument collection
// Themes (GUI)
ThemeNext,
ThemePrev,
// Region formatting
IndentRegion, // indent region (C-k =)
UnindentRegion, // unindent region (C-k -)
@@ -86,6 +91,15 @@ enum class CommandId {
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
// Meta
UnknownKCommand, // arg: single character that was not recognized after C-k
// Generic command prompt
CommandPromptStart, // begin generic command prompt (C-k ;)
// Theme by name
ThemeSetByName,
// Background mode (GUI)
BackgroundSet,
// Syntax highlighting
Syntax, // ":syntax on|off|reload"
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
};
@@ -109,6 +123,8 @@ struct Command {
std::string name; // stable, unique name (e.g., "save", "save-as")
std::string help; // short help/description
CommandHandler handler;
// Public commands are exposed in the ": " prompt (C-k ;)
bool isPublic = false;
};

View File

@@ -3,6 +3,9 @@
#include <filesystem>
#include "Editor.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/CppHighlighter.h"
#include "syntax/NullHighlighter.h"
Editor::Editor() = default;
@@ -151,7 +154,32 @@ Editor::OpenFile(const std::string &path, std::string &err)
const bool rows_empty = rows.empty();
const bool single_empty_line = (!rows.empty() && rows.size() == 1 && rows[0].size() == 0);
if (unnamed && clean && (rows_empty || single_empty_line)) {
return cur.OpenFromFile(path, err);
bool ok = cur.OpenFromFile(path, err);
if (!ok)
return false;
// Setup highlighting using registry (extension + shebang)
cur.EnsureHighlighter();
std::string first = "";
const auto &rows = cur.Rows();
if (!rows.empty())
first = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
cur.SetFiletype(ft);
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
cur.SetFiletype("");
cur.SetSyntaxEnabled(true);
if (auto *eng = cur.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
return true;
}
}
@@ -159,6 +187,30 @@ Editor::OpenFile(const std::string &path, std::string &err)
if (!b.OpenFromFile(path, err)) {
return false;
}
// Initialize syntax highlighting by extension + shebang via registry (v2)
b.EnsureHighlighter();
std::string first = "";
{
const auto &rows = b.Rows();
if (!rows.empty())
first = static_cast<std::string>(rows[0]);
}
std::string ft = kte::HighlighterRegistry::DetectForPath(path, first);
if (!ft.empty()) {
b.SetFiletype(ft);
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
eng->InvalidateFrom(0);
}
} else {
b.SetFiletype("");
b.SetSyntaxEnabled(true);
if (auto *eng = b.Highlighter()) {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
eng->InvalidateFrom(0);
}
}
// Add as a new buffer and switch to it
std::size_t idx = AddBuffer(std::move(b));
SwitchTo(idx);
@@ -173,6 +225,27 @@ Editor::SwitchTo(std::size_t index)
return false;
}
curbuf_ = index;
// Robustness: ensure a valid highlighter is installed when switching buffers
Buffer &b = buffers_[curbuf_];
if (b.SyntaxEnabled()) {
b.EnsureHighlighter();
if (auto *eng = b.Highlighter()) {
if (!eng->HasHighlighter()) {
// Try to set based on existing filetype; fall back to NullHighlighter
if (!b.Filetype().empty()) {
auto hl = kte::HighlighterRegistry::CreateFor(b.Filetype());
if (hl) {
eng->SetHighlighter(std::move(hl));
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
}
eng->InvalidateFrom(0);
}
}
}
return true;
}

View File

@@ -32,6 +32,16 @@ public:
}
[[nodiscard]] std::size_t ContentRows() const
{
// Always compute from current rows_ to avoid stale values.
// Reserve 1 row for status line.
if (rows_ == 0)
return 1;
return std::max<std::size_t>(1, rows_ - 1);
}
// Mode and flags (mirroring legacy fields)
void SetMode(int m)
{
@@ -315,7 +325,8 @@ public:
GotoLine,
Chdir,
ReplaceFind, // step 1 of Search & Replace: find what
ReplaceWith // step 2 of Search & Replace: replace with
ReplaceWith, // step 2 of Search & Replace: replace with
Command // generic command prompt (": ")
};
@@ -524,10 +535,28 @@ private:
// Temporary state for Search & Replace flow
public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; }
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; }
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; }
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; }
void SetReplaceFindTmp(const std::string &s)
{
replace_find_tmp_ = s;
}
void SetReplaceWithTmp(const std::string &s)
{
replace_with_tmp_ = s;
}
[[nodiscard]] const std::string &ReplaceFindTmp() const
{
return replace_find_tmp_;
}
[[nodiscard]] const std::string &ReplaceWithTmp() const
{
return replace_with_tmp_;
}
private:
std::string replace_find_tmp_;

View File

@@ -102,6 +102,25 @@ GUIConfig::LoadFromFile(const std::string &path)
if (v > 0.0f) {
font_size = v;
}
} else if (key == "theme") {
theme = val;
} else if (key == "background" || key == "bg") {
std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (v == "light" || v == "dark")
background = v;
} else if (key == "syntax") {
std::string v = val;
std::transform(v.begin(), v.end(), v.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (v == "1" || v == "on" || v == "true" || v == "yes") {
syntax = true;
} else if (v == "0" || v == "off" || v == "false" || v == "no") {
syntax = false;
}
}
}

View File

@@ -16,6 +16,14 @@ public:
int columns = 80;
int rows = 42;
float font_size = (float) KTE_FONT_SIZE;
std::string theme = "nord";
// Background mode for themes that support light/dark variants
// Values: "dark" (default), "light"
std::string background = "dark";
// Default syntax highlighting state for GUI (kge): on/off
// Accepts: on/off/true/false/yes/no/1/0 in the ini file.
bool syntax = true; // default: enabled
// Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load();

View File

@@ -16,6 +16,8 @@
#include "Font.h" // embedded default font (DefaultFontRegular)
#include "GUIConfig.h"
#include "GUITheme.h"
#include "syntax/HighlighterRegistry.h"
#include "syntax/NullHighlighter.h"
#ifndef KTE_FONT_SIZE
@@ -32,8 +34,8 @@ GUIFrontend::Init(Editor &ed)
return false;
}
// Load GUI configuration (fullscreen, columns/rows, font size)
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load();
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
GUIConfig cfg = GUIConfig::Load();
// GL attributes for core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
@@ -47,7 +49,7 @@ GUIFrontend::Init(Editor &ed)
// Compute desired window size from config
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
if (fullscreen) {
if (cfg.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{};
@@ -61,8 +63,8 @@ GUIFrontend::Init(Editor &ed)
#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);
int w = cfg.columns * static_cast<int>(cfg.font_size);
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
// As a safety, clamp to display usable bounds if retrievable
SDL_Rect usable{};
@@ -86,7 +88,7 @@ GUIFrontend::Init(Editor &ed)
// 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) {
if (cfg.fullscreen) {
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
SDL_SetWindowPosition(window_, usable.x, usable.y);
@@ -105,8 +107,45 @@ GUIFrontend::Init(Editor &ed)
ImGuiIO &io = ImGui::GetIO();
(void) io;
ImGui::StyleColorsDark();
// Apply a Nord-inspired theme
kte::ApplyNordImGuiTheme();
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
// Apply default syntax highlighting preference from GUI config to the current buffer
if (Buffer *b = ed.CurrentBuffer()) {
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
// Ensure a highlighter is available if possible
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
// Try detect from filename and first line; fall back to cpp or existing filetype
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty())
first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(
b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false;
@@ -135,7 +174,7 @@ GUIFrontend::Init(Editor &ed)
#endif
// Initialize GUI font from embedded default (use configured size or compiled default)
LoadGuiFont_(nullptr, (float) font_size);
LoadGuiFont_(nullptr, (float) cfg.font_size);
return true;
}
@@ -164,28 +203,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
input_.ProcessSDLEvent(e);
}
// Execute pending mapped inputs (drain queue)
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
break;
if (mi.hasCommand) {
// Track kill ring before and after to sync GUI clipboard when it changes
const std::string before = ed.KillRingHead();
Execute(ed, mi.id, mi.arg, mi.count);
const std::string after = ed.KillRingHead();
if (after != before && !after.empty()) {
// Update the system clipboard to mirror the kill ring head in GUI
SDL_SetClipboardText(after.c_str());
}
}
}
if (ed.QuitRequested()) {
running = false;
}
// Start a new ImGui frame
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame(window_);
ImGui::NewFrame();
@@ -214,7 +232,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
// Visible content rows inside the scroll child
std::size_t content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
auto content_rows = static_cast<std::size_t>(std::floor(avail_h / line_h));
// Editor::Rows includes the status line; add 1 back for it.
std::size_t rows = std::max<std::size_t>(1, content_rows + 1);
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
@@ -225,6 +243,27 @@ GUIFrontend::Step(Editor &ed, bool &running)
}
}
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
break;
if (mi.hasCommand) {
// Track kill ring before and after to sync GUI clipboard when it changes
const std::string before = ed.KillRingHead();
Execute(ed, mi.id, mi.arg, mi.count);
const std::string after = ed.KillRingHead();
if (after != before && !after.empty()) {
// Update the system clipboard to mirror the kill ring head in GUI
SDL_SetClipboardText(after.c_str());
}
}
}
if (ed.QuitRequested()) {
running = false;
}
// No runtime font UI; always use embedded font.
// Draw editor UI
@@ -264,11 +303,11 @@ GUIFrontend::Shutdown()
bool
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
{
ImGuiIO &io = ImGui::GetIO();
const ImGuiIO &io = ImGui::GetIO();
io.Fonts->Clear();
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
(void *) DefaultFontBoldCompressedData,
(int) DefaultFontBoldCompressedSize,
const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
DefaultFontBoldCompressedData,
DefaultFontBoldCompressedSize,
size_px);
if (!font) {
font = io.Fonts->AddFontDefault();

View File

@@ -25,7 +25,7 @@ public:
void Shutdown() override;
private:
bool LoadGuiFont_(const char *path, float size_px);
static bool LoadGuiFont_(const char *path, float size_px);
GUIInputHandler input_{};
GUIRenderer renderer_{};

View File

@@ -3,6 +3,7 @@
#include <ncurses.h>
#include <SDL.h>
#include <imgui.h>
#include "GUIInputHandler.h"
#include "KKeymap.h"
@@ -92,10 +93,14 @@ map_key(const SDL_Keycode key,
out = {true, CommandId::Backspace, "", 0};
return true;
case SDLK_TAB:
// Do not insert text on KEYDOWN; allow SDL_TEXTINPUT to deliver '\t'
// as printable input so that all printable characters flow via TEXTINPUT.
out.hasCommand = false;
// Insert a literal tab character when not interpreting a k-prefix suffix.
// If k-prefix is active, let the k-prefix handler below consume the key
// (so Tab doesn't leave k-prefix stuck).
if (!k_prefix) {
out = {true, CommandId::InsertText, std::string("\t"), 0};
return true;
}
break; // fall through so k-prefix handler can process
case SDLK_RETURN:
case SDLK_KP_ENTER:
out = {true, CommandId::Newline, "", 0};
@@ -280,7 +285,11 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
bool produced = false;
switch (e.type) {
case SDL_MOUSEWHEEL: {
// Map vertical wheel to line-wise cursor movement (MoveUp/MoveDown)
// Map vertical wheel to viewport scrolling (ScrollUp/ScrollDown)
// Note: We don't check WantCaptureMouse here because ImGui sets it to true
// whenever the mouse is over any ImGui window (including our editor content area).
// The NoScrollWithMouse flag on the child window prevents ImGui from handling
// scroll internally, so we can safely process wheel events ourselves.
int dy = e.wheel.y;
#ifdef SDL_MOUSEWHEEL_FLIPPED
if (e.wheel.direction == SDL_MOUSEWHEEL_FLIPPED)
@@ -288,7 +297,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
#endif
if (dy != 0) {
int repeat = dy > 0 ? dy : -dy;
CommandId id = dy > 0 ? CommandId::MoveUp : CommandId::MoveDown;
CommandId id = dy > 0 ? CommandId::ScrollUp : CommandId::ScrollDown;
std::lock_guard<std::mutex> lk(mu_);
for (int i = 0; i < repeat; ++i) {
q_.push(MappedInput{true, id, std::string(), 0});
@@ -347,13 +356,19 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
uarg_text_,
mi);
// If we inserted a TAB on KEYDOWN, suppress any subsequent SDL_TEXTINPUT
// for this keystroke to avoid double insertion on platforms that emit it.
if (produced && mi.hasCommand && mi.id == CommandId::InsertText && mi.arg == "\t") {
suppress_text_input_once_ = true;
}
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
// Digits without shift, or a plain '-'
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
const bool is_minus_key = (key == SDLK_MINUS);
if (uarg_active_ && uarg_collecting_ && (is_digit_key || is_minus_key)) {
if (uarg_active_ && uarg_collecting_ &&(is_digit_key || is_minus_key)) {
suppress_text_input_once_ = true;
}
}
@@ -545,7 +560,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
if (produced && mi.hasCommand) {
// Attach universal-argument count if present, then clear the state
if (uarg_active_ && mi.id != CommandId::UArgStatus) {
if (uarg_active_ &&mi
.
id != CommandId::UArgStatus
)
{
int count = 0;
if (!uarg_had_digits_ && !uarg_negative_) {
count = (uarg_value_ > 0) ? uarg_value_ : 4;

View File

@@ -10,6 +10,8 @@
#include <regex>
#include "GUIRenderer.h"
#include "Highlight.h"
#include "GUITheme.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
@@ -44,6 +46,8 @@ GUIRenderer::Draw(Editor &ed)
ImGui::SetNextWindowSize(main_sz);
ImGuiWindowFlags flags = ImGuiWindowFlags_NoTitleBar
| ImGuiWindowFlags_NoScrollbar
| ImGuiWindowFlags_NoScrollWithMouse
| ImGuiWindowFlags_NoResize
| ImGuiWindowFlags_NoMove
| ImGuiWindowFlags_NoCollapse
@@ -63,54 +67,65 @@ GUIRenderer::Draw(Editor &ed)
ImGui::TextUnformatted("[no buffer]");
} else {
const auto &lines = buf->Rows();
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Detect click-to-move inside this scroll region
ImVec2 list_origin = ImGui::GetCursorScreenPos();
float scroll_y = ImGui::GetScrollY();
float scroll_x = ImGui::GetScrollX();
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
std::size_t cy = buf->Cury();
std::size_t cx = buf->Curx();
const float line_h = ImGui::GetTextLineHeight();
const float row_h = ImGui::GetTextLineHeightWithSpacing();
const float space_w = ImGui::CalcTextSize(" ").x;
// 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;
{
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());
// Detect programmatic change (e.g., page_down command changed rowoffs)
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
float target_y = static_cast<float>(buf_rowoffs) * row_h;
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
}
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
float target_x = static_cast<float>(buf_coloffs) * space_w;
float target_y = static_cast<float>(buf_rowoffs) * row_h;
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
}
// Reserve space for status bar at bottom
ImGui::BeginChild("scroll", ImVec2(0, -ImGui::GetFrameHeightWithSpacing()), false,
ImGuiWindowFlags_HorizontalScrollbar | ImGuiWindowFlags_NoScrollWithMouse);
// Get child window position and scroll for click handling
ImVec2 child_window_pos = ImGui::GetWindowPos();
float scroll_y = ImGui::GetScrollY();
float scroll_x = ImGui::GetScrollX();
std::size_t rowoffs = 0; // we render from the first line; scrolling is handled by ImGui
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
bool forced_scroll = false;
{
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 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)
// Check if rowoffs was programmatically changed this frame
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 user scrolled (not programmatic), update buffer offsets accordingly
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
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 (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left)));
@@ -118,11 +133,12 @@ GUIRenderer::Draw(Editor &ed)
}
// 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();
prev_scroll_y = scroll_y;
prev_scroll_x = scroll_x;
}
prev_buf_rowoffs = buf_rowoffs;
prev_buf_coloffs = buf_coloffs;
// Synchronize cursor and scrolling.
// 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.
@@ -151,28 +167,26 @@ GUIRenderer::Draw(Editor &ed)
last_row = first_row + vis_rows - 1;
}
}
// Phase 3: prefetch visible viewport highlights and warm around in background
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(std::max(0L, first_row));
int rc = static_cast<int>(std::max(1L, vis_rows));
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
}
// Handle mouse click before rendering to avoid dependent on drawn items
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
ImVec2 mp = ImGui::GetIO().MousePos;
// 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;
// Compute content-relative position accounting for scroll
// mp.y - child_window_pos.y gives us pixels from top of child window
// Adding scroll_y gives us pixels from top of content (buffer row 0)
float content_y = (mp.y - child_window_pos.y) + scroll_y;
long by_l = static_cast<long>(content_y / row_h);
if (by_l < 0)
by_l = 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);
// Convert to buffer row
std::size_t by = static_cast<std::size_t>(by_l);
if (by >= lines.size()) {
if (!lines.empty())
by = lines.size() - 1;
@@ -180,58 +194,43 @@ GUIRenderer::Draw(Editor &ed)
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;
// Compute content-relative X position accounting for scroll
// mp.x - child_window_pos.x gives us pixels from left edge of child window
// Adding scroll_x gives us pixels from left edge of content (column 0)
float content_x = (mp.x - child_window_pos.x) + scroll_x;
if (content_x < 0.0f)
content_x = 0.0f;
// Empty buffer guard: if there are no lines yet, just move to 0:0
if (lines.empty()) {
Execute(ed, CommandId::MoveCursorTo, std::string("0:0"));
} else {
// Convert pixel X to a render-column target including horizontal col offset
// Use our own tab expansion of width 8 to match command layer logic.
// Convert pixel X to source column accounting for tabs
std::string line_clicked = static_cast<std::string>(lines[by]);
const std::size_t tabw = 8;
// We iterate source columns computing absolute rendered column (rx_abs) from 0,
// then translate to viewport-space by subtracting Coloffs.
std::size_t coloffs = buf->Coloffs();
std::size_t rx_abs = 0; // absolute rendered column
std::size_t i = 0; // source column iterator
// Fast-forward i until rx_abs >= coloffs to align with leftmost visible column
if (!line_clicked.empty() && coloffs > 0) {
while (i < line_clicked.size() && rx_abs < coloffs) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
} else {
rx_abs += 1;
}
++i;
}
}
// Now search for closest source column to clicked px within/after viewport
std::size_t best_col = i; // default to first visible column
// Iterate through source columns, computing rendered position, to find closest match
std::size_t rx = 0; // rendered column position
std::size_t best_col = 0;
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) {
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
// Check current position
float rx_px = static_cast<float>(rx) * space_w;
float dist = std::fabs(content_x - rx_px);
if (dist < best_dist) {
best_dist = dist;
best_col = i;
}
if (i == line_clicked.size())
break;
// advance to next source column
// Advance to next position if not at end
if (i < line_clicked.size()) {
if (line_clicked[i] == '\t') {
rx_abs += (tabw - (rx_abs % tabw));
rx += (tabw - (rx % tabw));
} else {
rx_abs += 1;
rx += 1;
}
}
++i;
}
// Dispatch absolute buffer coordinates (row:col)
@@ -254,10 +253,12 @@ GUIRenderer::Draw(Editor &ed)
std::size_t rx_abs_draw = 0; // rendered column for drawing
// Compute search highlight ranges for this line in source indices
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> hl_src_ranges;
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
if (search_mode) {
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
if (ed.PromptActive() && (
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
@@ -297,28 +298,31 @@ GUIRenderer::Draw(Editor &ed)
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 0;
for (const auto &rg : hl_src_ranges) {
for (const auto &rg: hl_src_ranges) {
std::size_t sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset
if (rx_end <= coloffs_now) continue; // fully left of view
if (rx_end <= coloffs_now)
continue; // fully left of view
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w, line_pos.y + line_h);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
// Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end;
ImU32 col = is_current ? IM_COL32(255, 220, 120, 140) : IM_COL32(200, 200, 0, 90);
ImU32 col = is_current
? IM_COL32(255, 220, 120, 140)
: IM_COL32(200, 200, 0, 90);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
}
// Emit entire line (ImGui child scrolling will handle clipping)
// Emit entire line to an expanded buffer (tabs -> spaces)
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 {
@@ -327,7 +331,44 @@ GUIRenderer::Draw(Editor &ed)
}
}
// Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
*buf, static_cast<int>(i), buf->Version());
// Helper to convert a src column to expanded rx position
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
std::size_t rx = 0;
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
}
return rx;
};
for (const auto &sp: lh.spans) {
std::size_t rx_s = src_to_rx_full(
static_cast<std::size_t>(std::max(0, sp.col_start)));
std::size_t rx_e = src_to_rx_full(
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
if (rx_e <= coloffs_now)
continue;
std::size_t vx0 = (rx_s > coloffs_now) ? (rx_s - coloffs_now) : 0;
std::size_t vx1 = (rx_e > coloffs_now) ? (rx_e - coloffs_now) : 0;
if (vx0 >= expanded.size())
continue;
vx1 = std::min<std::size_t>(vx1, expanded.size());
if (vx1 <= vx0)
continue;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y);
ImGui::GetWindowDrawList()->AddText(
p, col, expanded.c_str() + vx0, expanded.c_str() + vx1);
}
// We drew text via draw list (no layout advance). Manually advance the cursor to the next line.
// Use row_h (with spacing) to match click calculation and ensure consistent line positions.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} else {
// No syntax: draw as one run
ImGui::TextUnformatted(expanded.c_str());
}
// Draw a visible cursor indicator on the current line
if (i == cy) {
@@ -390,13 +431,18 @@ GUIRenderer::Draw(Editor &ed)
float max_px = std::max(0.0f, right_x - left_x);
std::string prefix;
if (!label.empty()) prefix = label + ": ";
if (kind == Editor::PromptKind::Command) {
prefix = ": ";
} else if (!label.empty()) {
prefix = label + ": ";
}
// Compose showing right-end of filename portion when too long for space
std::string final_msg;
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
float avail_px = std::max(0.0f, max_px - prefix_sz.x);
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && avail_px > 0.0f) {
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
Editor::PromptKind::Chdir) && avail_px > 0.0f) {
// Trim from left until it fits by pixel width
std::string tail = ptext;
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
@@ -408,7 +454,11 @@ GUIRenderer::Draw(Editor &ed)
while (start < tail.size()) {
// Estimate how many chars to skip based on ratio
float ratio = tail_sz.x / avail_px;
size_t skip = ratio > 1.5f ? std::min(tail.size() - start, (size_t)std::max<size_t>(1, (size_t)(tail.size() / 4))) : 1;
size_t skip = ratio > 1.5f
? std::min(tail.size() - start,
(size_t) std::max<size_t>(
1, (size_t) (tail.size() / 4)))
: 1;
start += skip;
std::string candidate = tail.substr(start);
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
@@ -425,7 +475,10 @@ GUIRenderer::Draw(Editor &ed)
while (lo < hi) {
size_t mid = (lo + hi) / 2;
std::string cand = tail.substr(mid);
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px) hi = mid; else lo = mid + 1;
if (ImGui::CalcTextSize(cand.c_str()).x <= avail_px)
hi = mid;
else
lo = mid + 1;
}
tail = tail.substr(lo);
}
@@ -529,7 +582,8 @@ GUIRenderer::Draw(Editor &ed)
}
// Draw right
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x), p0.y + (bar_h - right_sz.y) * 0.5f));
ImGui::SetCursorScreenPos(ImVec2(std::max(right_x, left_x),
p0.y + (bar_h - right_sz.y) * 0.5f));
ImGui::TextUnformatted(right.c_str());
// Draw middle message centered in remaining space

View File

@@ -1,127 +1,415 @@
// GUITheme.h - ImGui theme configuration for kte GUI
// Provides a Nord-inspired color palette and style settings.
// GUITheme.h ImGui theming helpers and background mode
#pragma once
#include <imgui.h>
#include <vector>
#include <memory>
#include <string>
#include <cstddef>
#include <algorithm>
#include <cctype>
#include "themes/ThemeHelpers.h"
namespace kte {
// Convert RGB hex (0xRRGGBB) to ImVec4 with optional alpha
static inline ImVec4
RGBA(unsigned int rgb, float a = 1.0f)
// Background mode selection for light/dark palettes
enum class BackgroundMode { Light, Dark };
// Global background mode; default to Dark to match prior defaults
static inline auto gBackgroundMode = BackgroundMode::Dark;
// Basic theme identifier (kept minimal; some ids are aliases)
enum class ThemeId {
EInk = 0,
GruvboxDarkMedium = 1,
GruvboxLightMedium = 1, // alias to unified gruvbox index
Nord = 2,
Plan9 = 3,
Solarized = 4,
};
// Current theme tracking
static inline auto gCurrentTheme = ThemeId::Nord;
static inline std::size_t gCurrentThemeIndex = 0;
// Forward declarations for helpers used below
static size_t ThemeIndexFromId(ThemeId id);
static ThemeId ThemeIdFromIndex(size_t idx);
// Helpers to set/query background mode
static void
SetBackgroundMode(const BackgroundMode m)
{
float r = ((rgb >> 16) & 0xFF) / 255.0f;
float g = ((rgb >> 8) & 0xFF) / 255.0f;
float b = ((rgb) & 0xFF) / 255.0f;
return ImVec4(r, g, b, a);
gBackgroundMode = m;
}
// Apply a Nord-inspired theme to the current ImGui style.
// Safe to call after ImGui::CreateContext().
static inline void
ApplyNordImGuiTheme()
static BackgroundMode
GetBackgroundMode()
{
// Nord palette
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
const ImVec4 nord1 = RGBA(0x3B4252);
const ImVec4 nord2 = RGBA(0x434C5E);
const ImVec4 nord3 = RGBA(0x4C566A);
const ImVec4 nord4 = RGBA(0xD8DEE9);
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
const ImVec4 nord12 = RGBA(0xD08770); // orange
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
return gBackgroundMode;
}
ImGuiStyle &style = ImGui::GetStyle();
// Base style tweaks to suit Nord aesthetics
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
static inline const char *
BackgroundModeName()
{
return gBackgroundMode == BackgroundMode::Light ? "light" : "dark";
}
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = nord4; // primary text
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
colors[ImGuiCol_WindowBg] = nord10;
colors[ImGuiCol_ChildBg] = nord0;
colors[ImGuiCol_PopupBg] = RGBA(0x2E3440, 0.98f);
colors[ImGuiCol_Border] = nord1;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
// Include individual theme implementations split under ./themes
#include "themes/Nord.h"
#include "themes/Plan9.h"
#include "themes/Solarized.h"
#include "themes/Gruvbox.h"
#include "themes/EInk.h"
colors[ImGuiCol_FrameBg] = nord2;
colors[ImGuiCol_FrameBgHovered] = nord3;
colors[ImGuiCol_FrameBgActive] = nord10;
colors[ImGuiCol_TitleBg] = nord1;
colors[ImGuiCol_TitleBgActive] = nord3;
colors[ImGuiCol_TitleBgCollapsed] = nord1;
// Theme abstraction and registry (generalized theme system)
class Theme {
public:
virtual ~Theme() = default;
colors[ImGuiCol_MenuBarBg] = nord1;
colors[ImGuiCol_ScrollbarBg] = nord0;
colors[ImGuiCol_ScrollbarGrab] = nord3;
colors[ImGuiCol_ScrollbarGrabHovered] = nord10;
colors[ImGuiCol_ScrollbarGrabActive] = nord9;
[[nodiscard]] virtual const char *Name() const = 0; // canonical name (e.g., "nord", "gruvbox-dark")
virtual void Apply() const = 0; // apply to current ImGui style
virtual ThemeId Id() = 0; // theme identifier
};
colors[ImGuiCol_CheckMark] = nord8;
colors[ImGuiCol_SliderGrab] = nord8;
colors[ImGuiCol_SliderGrabActive] = nord9;
namespace detail {
struct NordTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "nord";
}
colors[ImGuiCol_Button] = nord3;
colors[ImGuiCol_ButtonHovered] = nord10;
colors[ImGuiCol_ButtonActive] = nord9;
colors[ImGuiCol_Header] = nord3;
colors[ImGuiCol_HeaderHovered] = nord10;
colors[ImGuiCol_HeaderActive] = nord10;
void Apply() const override
{
ApplyNordImGuiTheme();
}
colors[ImGuiCol_Separator] = nord2;
colors[ImGuiCol_SeparatorHovered] = nord10;
colors[ImGuiCol_SeparatorActive] = nord9;
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = nord9;
ThemeId Id() override
{
return ThemeId::Nord;
}
};
colors[ImGuiCol_Tab] = nord2;
colors[ImGuiCol_TabHovered] = nord10;
colors[ImGuiCol_TabActive] = nord3;
colors[ImGuiCol_TabUnfocused] = nord2;
colors[ImGuiCol_TabUnfocusedActive] = nord3;
struct GruvboxTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "gruvbox";
}
// Docking colors are available only when docking branch is enabled; omit for compatibility
colors[ImGuiCol_TableHeaderBg] = nord2;
colors[ImGuiCol_TableBorderStrong] = nord1;
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
void Apply() const override
{
if (gBackgroundMode == BackgroundMode::Light)
ApplyGruvboxLightMediumTheme();
else
ApplyGruvboxDarkMediumTheme();
}
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
colors[ImGuiCol_DragDropTarget] = nord13;
colors[ImGuiCol_NavHighlight] = nord9;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
// Plots
colors[ImGuiCol_PlotLines] = nord8;
colors[ImGuiCol_PlotLinesHovered] = nord9;
colors[ImGuiCol_PlotHistogram] = nord13;
colors[ImGuiCol_PlotHistogramHovered] = nord12;
ThemeId Id() override
{
// Legacy maps to dark; unified under base id GruvboxDarkMedium
return ThemeId::GruvboxDarkMedium;
}
};
struct EInkTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "eink";
}
void Apply() const override
{
if (gBackgroundMode == BackgroundMode::Dark)
ApplyEInkDarkImGuiTheme();
else
ApplyEInkImGuiTheme();
}
ThemeId Id() override
{
return ThemeId::EInk;
}
};
struct SolarizedTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "solarized";
}
void Apply() const override
{
if (gBackgroundMode == BackgroundMode::Light)
ApplySolarizedLightTheme();
else
ApplySolarizedDarkTheme();
}
ThemeId Id() override
{
return ThemeId::Solarized;
}
};
struct Plan9Theme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "plan9";
}
void Apply() const override
{
ApplyPlan9Theme();
}
ThemeId Id() override
{
return ThemeId::Plan9;
}
};
} // namespace detail
static const std::vector<std::unique_ptr<Theme> > &
ThemeRegistry()
{
static std::vector<std::unique_ptr<Theme> > reg;
if (reg.empty()) {
// Alphabetical by canonical name: eink, gruvbox, nord, plan9, solarized
reg.emplace_back(std::make_unique<detail::EInkTheme>());
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
reg.emplace_back(std::make_unique<detail::NordTheme>());
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
}
return reg;
}
// Canonical theme name for a given ThemeId (via registry order)
[[maybe_unused]] static const char *
ThemeName(const ThemeId id)
{
const auto &reg = ThemeRegistry();
const size_t idx = ThemeIndexFromId(id);
if (idx < reg.size())
return reg[idx]->Name();
return "unknown";
}
// Helper to apply a theme by id and update current theme
static void
ApplyTheme(const ThemeId id)
{
const auto &reg = ThemeRegistry();
const size_t idx = ThemeIndexFromId(id);
if (idx < reg.size()) {
reg[idx]->Apply();
gCurrentTheme = id;
gCurrentThemeIndex = idx;
}
}
[[maybe_unused]] static ThemeId
CurrentTheme()
{
return gCurrentTheme;
}
// Cycle helpers
[[maybe_unused]] static ThemeId
NextTheme()
{
const auto &reg = ThemeRegistry();
if (reg.empty()) {
return gCurrentTheme;
}
const size_t nxt = (gCurrentThemeIndex + 1) % reg.size();
ApplyTheme(ThemeIdFromIndex(nxt));
return gCurrentTheme;
}
[[maybe_unused]] static ThemeId
PrevTheme()
{
const auto &reg = ThemeRegistry();
if (reg.empty()) {
return gCurrentTheme;
}
const size_t prv = (gCurrentThemeIndex + reg.size() - 1) % reg.size();
ApplyTheme(ThemeIdFromIndex(prv));
return gCurrentTheme;
}
// Name-based API
[[maybe_unused]] static const Theme *
GetThemeByName(const std::string &name)
{
const auto &reg = ThemeRegistry();
for (const auto &t: reg) {
if (name == t->Name())
return t.get();
}
return nullptr;
}
[[maybe_unused]] static bool
ApplyThemeByName(const std::string &name)
{
// Handle aliases and background-specific names
std::string n = name;
// lowercase copy
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
if (n == "gruvbox-dark") {
SetBackgroundMode(BackgroundMode::Dark);
n = "gruvbox";
} else if (n == "gruvbox-light") {
SetBackgroundMode(BackgroundMode::Light);
n = "gruvbox";
} else if (n == "solarized-dark") {
SetBackgroundMode(BackgroundMode::Dark);
n = "solarized";
} else if (n == "solarized-light") {
SetBackgroundMode(BackgroundMode::Light);
n = "solarized";
} else if (n == "eink-dark") {
SetBackgroundMode(BackgroundMode::Dark);
n = "eink";
} else if (n == "eink-light") {
SetBackgroundMode(BackgroundMode::Light);
n = "eink";
}
const auto &reg = ThemeRegistry();
for (size_t i = 0; i < reg.size(); ++i) {
if (n == reg[i]->Name()) {
reg[i]->Apply();
gCurrentThemeIndex = i;
gCurrentTheme = ThemeIdFromIndex(i);
return true;
}
}
return false;
}
[[maybe_unused]] static const char *
CurrentThemeName()
{
const auto &reg = ThemeRegistry();
if (gCurrentThemeIndex < reg.size()) {
return reg[gCurrentThemeIndex]->Name();
}
return "unknown";
}
// Helpers to map between legacy ThemeId and registry index
static size_t
ThemeIndexFromId(const ThemeId id)
{
switch (id) {
case ThemeId::EInk:
return 0;
case ThemeId::GruvboxDarkMedium:
return 1;
case ThemeId::Nord:
return 2;
case ThemeId::Plan9:
return 3;
case ThemeId::Solarized:
return 4;
}
return 0;
}
static ThemeId
ThemeIdFromIndex(const size_t idx)
{
switch (idx) {
default:
case 0:
return ThemeId::EInk;
case 1:
return ThemeId::GruvboxDarkMedium; // unified gruvbox
case 2:
return ThemeId::Nord;
case 3:
return ThemeId::Plan9;
case 4:
return ThemeId::Solarized;
}
}
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
[[maybe_unused]] static ImVec4
SyntaxInk(const TokenKind k)
{
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
// Base text
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
switch (k) {
case TokenKind::Keyword:
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
case TokenKind::Type:
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
case TokenKind::String:
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Char:
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
case TokenKind::Comment:
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
case TokenKind::Number:
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
case TokenKind::Preproc:
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
case TokenKind::Constant:
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
case TokenKind::Function:
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
case TokenKind::Operator:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Punctuation:
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
case TokenKind::Identifier:
return def;
case TokenKind::Whitespace:
return def;
case TokenKind::Error:
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
case TokenKind::Default: default:
return def;
}
}
} // namespace kte

View File

@@ -15,24 +15,26 @@ HelpText::Text()
return std::string(
"KTE - Kyle's Text Editor\n\n"
"About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
" kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
" WordStar/VDE-style command model with some emacs influences.\n"
"\n"
"Core keybindings:\n"
"K-commands (prefix C-k):\n"
" C-k ' Toggle read-only\n"
" C-k - Unindent region\n"
" C-k = Indent region\n"
" C-k - Unindent region (mark required)\n"
" C-k = Indent region (mark required)\n"
" C-k ; Command prompt (:\\ )\n"
" C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\n"
" C-k a Mark all and jump to end\n"
" C-k C-x Save and quit\n"
" C-k a Mark start of file, jump to end\n"
" C-k b Switch buffer\n"
" C-k c Close current buffer\n"
" C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n"
" C-k f Flush kill ring\n"
" C-k g Jump to line\n"
" C-k h Show this help\n"
" C-k j Jump to mark\n"
" C-k l Reload buffer from disk\n"
" C-k n Previous buffer\n"
" C-k o Change working directory (prompt)\n"
@@ -44,12 +46,36 @@ HelpText::Text()
" C-k v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n"
" C-k x Save and quit\n"
" C-k y Yank\n"
"\n"
"ESC/Alt commands:\n"
" ESC < Go to beginning of file\n"
" ESC > Go to end of file\n"
" ESC m Toggle mark\n"
" ESC w Copy region to kill ring (Alt-w)\n"
" ESC b Previous word\n"
" ESC f Next word\n"
" ESC d Delete next word (Alt-d)\n"
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
" ESC q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n"
" ESC d Delete next word\n"
" Alt-w Copy region to kill ring\n\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\n"
"\n"
"Control keys:\n"
" C-a C-e Line start / end\n"
" C-b C-f Move left / right\n"
" C-n C-p Move down / up\n"
" C-d Delete char\n"
" C-w / C-y Kill region / Yank\n"
" C-s Incremental find\n"
" C-r Regex search\n"
" C-t Regex search & replace\n"
" C-h Search & replace\n"
" C-l / C-g Refresh / Cancel\n"
" C-u [digits] Universal argument (repeat count)\n"
"\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
"\n"
"GUI appearance (command prompt):\n"
" : theme NAME Set GUI theme (eink, gruvbox, nord, plan9, solarized)\n"
" : background MODE Set background: light | dark (affects eink, gruvbox, solarized)\n"
);
}

37
Highlight.h Normal file
View File

@@ -0,0 +1,37 @@
// Highlight.h - core syntax highlighting types for kte
#pragma once
#include <cstdint>
#include <vector>
namespace kte {
// Token kinds shared between renderers and highlighters
enum class TokenKind {
Default,
Keyword,
Type,
String,
Char,
Comment,
Number,
Preproc,
Constant,
Function,
Operator,
Punctuation,
Identifier,
Whitespace,
Error
};
struct HighlightSpan {
int col_start{0}; // inclusive, 0-based columns in buffer indices
int col_end{0}; // exclusive
TokenKind kind{TokenKind::Default};
};
struct LineHighlight {
std::vector<HighlightSpan> spans;
std::uint64_t version{0}; // buffer version used for this line
};
} // namespace kte

View File

@@ -108,6 +108,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case '=':
out = CommandId::IndentRegion;
return true;
case ';':
out = CommandId::CommandPromptStart; // C-k ; : generic command prompt
return true;
default:
break;
}

View File

@@ -2,9 +2,11 @@ ROADMAP / TODO:
- [x] Search + Replace
- [x] Regex search + replace
- [ ] The undo system should actually work
- [x] Able to mark buffers as read-only
- [x] Built-in help text
- [x] Shorten paths in the homedir with ~
- [x] When the filename is longer than the message window, scoot left to
keep it in view
- [x] Syntax highlighting
- [ ] The undo system should actually work
- [ ] LSP integration

View File

@@ -35,16 +35,16 @@ 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
// Mouse wheel → scroll viewport without moving cursor
#ifdef BUTTON4_PRESSED
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
out = {true, CommandId::MoveUp, "", 0};
out = {true, CommandId::ScrollUp, "", 0};
return true;
}
#endif
#ifdef BUTTON5_PRESSED
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
out = {true, CommandId::MoveDown, "", 0};
out = {true, CommandId::ScrollDown, "", 0};
return true;
}
#endif

View File

@@ -9,6 +9,7 @@
#include "TerminalRenderer.h"
#include "Buffer.h"
#include "Editor.h"
#include "Highlight.h"
// Version string expected to be provided by build system as KTE_VERSION_STR
#ifndef KTE_VERSION_STR
@@ -33,6 +34,8 @@ TerminalRenderer::Draw(Editor &ed)
const Buffer *buf = ed.CurrentBuffer();
int content_rows = rows - 1; // last line is status
if (content_rows < 1)
content_rows = 1;
int saved_cur_y = -1, saved_cur_x = -1; // logical cursor position within content area
if (buf) {
@@ -41,6 +44,13 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t coloffs = buf->Coloffs();
const int tabw = 8;
// Phase 3: prefetch visible viewport highlights (current terminal area)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
int fr = static_cast<int>(rowoffs);
int rc = std::max(0, content_rows);
buf->Highlighter()->PrefetchViewport(*buf, fr, rc, buf->Version());
}
for (int r = 0; r < content_rows; ++r) {
move(r, 0);
std::size_t li = rowoffs + static_cast<std::size_t>(r);
@@ -48,11 +58,13 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty();
std::vector<std::pair<std::size_t, std::size_t>> ranges; // [start, end)
std::vector<std::pair<std::size_t, std::size_t> > ranges; // [start, end)
if (search_mode && li < lines.size()) {
std::string sline = static_cast<std::string>(lines[li]);
// If regex search prompt is active (RegexSearch or RegexReplaceFind), use regex to compute highlight ranges
if (ed.PromptActive() && (ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
if (ed.PromptActive() && (
ed.CurrentPromptKind() == Editor::PromptKind::RegexSearch || ed.
CurrentPromptKind() == Editor::PromptKind::RegexReplaceFind)) {
try {
std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
@@ -75,12 +87,15 @@ TerminalRenderer::Draw(Editor &ed)
}
}
auto is_src_in_hl = [&](std::size_t si) -> bool {
if (ranges.empty()) return false;
if (ranges.empty())
return false;
// ranges are non-overlapping and ordered by construction
// linear scan is fine for now
for (const auto &rg : ranges) {
if (si < rg.first) break;
if (si >= rg.first && si < rg.second) return true;
for (const auto &rg: ranges) {
if (si < rg.first)
break;
if (si >= rg.first && si < rg.second)
return true;
}
return false;
};
@@ -96,6 +111,46 @@ TerminalRenderer::Draw(Editor &ed)
std::string line = static_cast<std::string>(lines[li]);
src_i = 0;
render_col = 0;
// Syntax highlighting: fetch per-line spans
const kte::LineHighlight *lh_ptr = nullptr;
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
HasHighlighter()) {
lh_ptr = &buf->Highlighter()->GetLine(
*buf, static_cast<int>(li), buf->Version());
}
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
if (!lh_ptr)
return kte::TokenKind::Default;
for (const auto &sp: lh_ptr->spans) {
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
src_index) < sp.col_end)
return sp.kind;
}
return kte::TokenKind::Default;
};
auto apply_token_attr = [&](kte::TokenKind k) {
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
attrset(A_NORMAL);
switch (k) {
case kte::TokenKind::Keyword:
case kte::TokenKind::Type:
case kte::TokenKind::Constant:
case kte::TokenKind::Function:
attron(A_BOLD);
break;
case kte::TokenKind::Comment:
attron(A_DIM);
break;
case kte::TokenKind::String:
case kte::TokenKind::Char:
case kte::TokenKind::Number:
// standout a bit using A_UNDERLINE if available
attron(A_UNDERLINE);
break;
default:
break;
}
};
while (written < cols) {
char ch = ' ';
bool from_src = false;
@@ -119,15 +174,35 @@ TerminalRenderer::Draw(Editor &ed)
// Now render visible spaces
while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && src_i >= cur_mx && src_i < cur_mend;
bool in_cur =
has_current && li == cur_my && src_i >= cur_mx
&& src_i < cur_mend;
// Toggle highlight attributes
int attr = 0;
if (in_hl) attr |= A_STANDOUT;
if (in_cur) attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; }
if (in_hl)
attr |= A_STANDOUT;
if (in_cur)
attr |= A_BOLD;
if ((attr & A_STANDOUT) && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
// Apply syntax attribute only if not in search highlight
if (!in_hl) {
apply_token_attr(token_at(src_i));
}
addch(' ');
++written;
++render_col;
@@ -151,11 +226,28 @@ TerminalRenderer::Draw(Editor &ed)
from_src = false;
}
bool in_hl = search_mode && from_src && is_src_in_hl(src_i);
bool in_cur = has_current && li == cur_my && from_src && src_i >= cur_mx && src_i < cur_mend;
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; }
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; }
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; }
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; }
bool in_cur =
has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
cur_mend;
if (in_hl && !hl_on) {
attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
if (!in_hl && from_src) {
apply_token_attr(token_at(src_i));
}
addch(static_cast<unsigned char>(ch));
++written;
++render_col;
@@ -173,6 +265,7 @@ TerminalRenderer::Draw(Editor &ed)
attroff(A_BOLD);
cur_on = false;
}
attrset(A_NORMAL);
clrtoeol();
}
@@ -222,20 +315,23 @@ TerminalRenderer::Draw(Editor &ed)
}
// Prefer keeping the tail of the filename visible when it exceeds the window
std::string msg;
if (!label.empty()) {
if (kind == Editor::PromptKind::Command) {
msg = ": ";
} else if (!label.empty()) {
msg = label + ": ";
}
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind == Editor::PromptKind::Chdir) && cols > 0) {
if ((kind == Editor::PromptKind::OpenFile || kind == Editor::PromptKind::SaveAs || kind ==
Editor::PromptKind::Chdir) && cols > 0) {
int avail = cols - static_cast<int>(msg.size());
if (avail <= 0) {
// No room for label; fall back to showing the rightmost portion of the whole string
std::string whole = msg + ptext;
if ((int)whole.size() > cols)
if ((int) whole.size() > cols)
whole = whole.substr(whole.size() - cols);
msg = whole;
} else {
if ((int)ptext.size() > avail) {
if ((int) ptext.size() > avail) {
ptext = ptext.substr(ptext.size() - avail);
}
msg += ptext;

View File

@@ -338,31 +338,42 @@ UndoSystem::UpdateBufferReference(Buffer &new_buf)
buf_ = &new_buf;
}
// ---- Debug helpers ----
const char *
UndoSystem::type_str(UndoType t)
{
switch (t) {
case UndoType::Insert: return "Insert";
case UndoType::Delete: return "Delete";
case UndoType::Paste: return "Paste";
case UndoType::Newline: return "Newline";
case UndoType::DeleteRow: return "DeleteRow";
case UndoType::Insert:
return "Insert";
case UndoType::Delete:
return "Delete";
case UndoType::Paste:
return "Paste";
case UndoType::Newline:
return "Newline";
case UndoType::DeleteRow:
return "DeleteRow";
}
return "?";
}
bool
UndoSystem::is_descendant(UndoNode *root, const UndoNode *target)
{
if (!root || !target) return false;
if (root == target) return true;
if (!root || !target)
return false;
if (root == target)
return true;
for (UndoNode *child = root->child; child != nullptr; child = child->next) {
if (is_descendant(child, target)) return true;
if (is_descendant(child, target))
return true;
}
return false;
}
void
UndoSystem::debug_log(const char *op) const
{
@@ -374,14 +385,14 @@ UndoSystem::debug_log(const char *op) const
"[UNDO] %s cur=(%d,%d) pending=%p t=%s r=%d c=%d nlen=%zu current=%p saved=%p\n",
op,
row, col,
(const void*)p,
(const void *) p,
p ? type_str(p->type) : "-",
p ? p->row : -1,
p ? p->col : -1,
p ? p->text.size() : 0,
(void*)tree_.current,
(void*)tree_.saved);
(void *) tree_.current,
(void *) tree_.saved);
#else
(void)op;
(void) op;
#endif
}

View File

@@ -43,7 +43,9 @@ private:
// Debug helpers (compiled only when KTE_UNDO_DEBUG is defined)
void debug_log(const char *op) const;
static const char *type_str(UndoType t);
static bool is_descendant(UndoNode *root, const UndoNode *target);
void update_dirty_flag();

View File

@@ -1,7 +1,7 @@
.\" 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"
.TH KGE 1 "2025-12-01" "kte 0.1.0" "User Commands"
.SH NAME
kge \- Kyle's Graphical Editor (GUI-first)
.SH SYNOPSIS
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
.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.
.B C-k '
Toggle read-only for the current buffer.
.TP
.B C-k -
If the mark is set, unindent the region.
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
.B C-k =
If the mark is set, indent the region.
.TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a
Set the mark at the beginning of the file, then jump to the end of the file.
.TP
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
Delete the entire line.
.TP
.B C-k e
Edit a new file.
Edit (open) a new file.
.TP
.B C-k f
Flush the kill ring.
@@ -88,14 +88,20 @@ Flush the kill ring.
.B C-k g
Go to a specific line.
.TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.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.
.B C-k n
Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP
.B C-k p
Switch to the next buffer.
@@ -106,14 +112,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
.B C-k C-q
Immediately exit the editor.
.TP
.B C-k r
Redo changes.
.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.
.B C-k v
Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP
.B C-k x
Save the file and exit. Also C-k C-x.
@@ -121,23 +133,50 @@ Save the file and exit. Also C-k C-x.
.B C-k y
Yank the kill ring.
.TP
.B C-k \e
Dump core.
.B C-k C-x
Save the file and exit.
.SS Other keybindings
.TP
.B C-g
Cancel the current operation.
.TP
.B C-a
Move to the beginning of the line.
.TP
.B C-e
Move to the end of the line.
.TP
.B C-b
Move left.
.TP
.B C-f
Move right.
.TP
.B C-n
Move down.
.TP
.B C-p
Move up.
.TP
.B C-l
Refresh the display.
.TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r
Regex search.
.TP
.B C-s
Incremental find.
.TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u
Universal argument. C-u followed by numbers will repeat an operation n times.
.TP
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
.B C-y
Yank the kill ring.
.TP
.B ESC <
Move to the beginning of the file.
.TP
.B ESC >
Move to the end of the file.
.TP
.B ESC m
Toggle the mark.
.TP
.B ESC BACKSPACE
Delete the previous word.
.TP

View File

@@ -1,7 +1,7 @@
.\" 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"
.TH KTE 1 "2025-12-01" "kte 0.1.0" "User Commands"
.SH NAME
kte \- Kyle's Text Editor (terminal-first)
.SH SYNOPSIS
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
.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.
.B C-k '
Toggle read-only for the current buffer.
.TP
.B C-k -
If the mark is set, unindent the region.
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
.B C-k =
If the mark is set, indent the region.
.TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a
Set the mark at the beginning of the file, then jump to the end of the file.
.TP
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
Delete the entire line.
.TP
.B C-k e
Edit a new file.
Edit (open) a new file.
.TP
.B C-k f
Flush the kill ring.
@@ -93,14 +93,20 @@ Flush the kill ring.
.B C-k g
Go to a specific line.
.TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.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.
.B C-k n
Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP
.B C-k p
Switch to the next buffer.
@@ -111,14 +117,20 @@ Exit the editor. If the file has unsaved changes, a warning will be printed; a s
.B C-k C-q
Immediately exit the editor.
.TP
.B C-k r
Redo changes.
.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.
.B C-k v
Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP
.B C-k x
Save the file and exit. Also C-k C-x.
@@ -126,23 +138,76 @@ Save the file and exit. Also C-k C-x.
.B C-k y
Yank the kill ring.
.TP
.B C-k \e
Dump core.
.B C-k C-x
Save the file and exit.
.SH GUI APPEARANCE
When running the GUI frontend, you can control appearance via the generic
command prompt (type "C-k ;" then enter commands):
.TP
.B : theme NAME
Set the GUI theme. Available names: "nord", "gruvbox", "plan9", "solarized", "eink".
Compatibility aliases are also accepted: "gruvbox-dark", "gruvbox-light",
"solarized-dark", "solarized-light", "eink-dark", "eink-light".
.TP
.B : background MODE
Set background mode for supported themes. MODE is either "light" or "dark".
Themes that respond to background: eink, gruvbox, solarized. The
"nord" and "plan9" themes do not vary with background.
.SH CONFIGURATION
The GUI reads a simple configuration file at
~/.config/kte/kge.ini. Recognized keys include:
.IP "fullscreen=on|off"
.IP "columns=NUM"
.IP "rows=NUM"
.IP "font_size=NUM"
.IP "theme=NAME"
.IP "background=light|dark"
The theme name accepts the values listed above. The background key controls
light/dark variants when the selected theme supports it.
.SS Other keybindings
.TP
.B C-g
Cancel the current operation.
.TP
.B C-a
Move to the beginning of the line.
.TP
.B C-e
Move to the end of the line.
.TP
.B C-b
Move left.
.TP
.B C-f
Move right.
.TP
.B C-n
Move down.
.TP
.B C-p
Move up.
.TP
.B C-l
Refresh the display.
.TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r
Regex search.
.TP
.B C-s
Incremental find.
.TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u
Universal argument. C-u followed by numbers will repeat an operation n times.
.TP
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
.B C-y
Yank the kill ring.
.TP
.B ESC <
Move to the beginning of the file.
.TP
.B ESC >
Move to the end of the file.
.TP
.B ESC m
Toggle the mark.
.TP
.B ESC BACKSPACE
Delete the previous word.
.TP

525
docs/lsp plan.md Normal file
View File

@@ -0,0 +1,525 @@
# LSP Support Implementation Plan for kte
## Executive Summary
This plan outlines a comprehensive approach to integrating Language Server Protocol (LSP) support into kte while
respecting its core architectural principles: **frontend/backend separation**, **testability**, and **dual terminal/GUI
support**.
---
## 1. Core Architecture
### 1.1 LSP Client Module Structure
```c++
// LspClient.h - Core LSP client abstraction
class LspClient {
public:
virtual ~LspClient() = default;
// Lifecycle
virtual bool initialize(const std::string& rootPath) = 0;
virtual void shutdown() = 0;
// Document Synchronization
virtual void didOpen(const std::string& uri, const std::string& languageId,
int version, const std::string& text) = 0;
virtual void didChange(const std::string& uri, int version,
const std::vector<TextDocumentContentChangeEvent>& changes) = 0;
virtual void didClose(const std::string& uri) = 0;
virtual void didSave(const std::string& uri) = 0;
// Language Features
virtual void completion(const std::string& uri, Position pos,
CompletionCallback callback) = 0;
virtual void hover(const std::string& uri, Position pos,
HoverCallback callback) = 0;
virtual void definition(const std::string& uri, Position pos,
LocationCallback callback) = 0;
virtual void references(const std::string& uri, Position pos,
LocationsCallback callback) = 0;
virtual void diagnostics(DiagnosticsCallback callback) = 0;
// Process Management
virtual bool isRunning() const = 0;
virtual std::string getServerName() const = 0;
};
```
### 1.2 Process-based LSP Implementation
```c++
// LspProcessClient.h - Manages LSP server subprocess
class LspProcessClient : public LspClient {
private:
std::string serverCommand_;
std::vector<std::string> serverArgs_;
std::unique_ptr<Process> process_;
std::unique_ptr<JsonRpcTransport> transport_;
std::unordered_map<int, PendingRequest> pendingRequests_;
int nextRequestId_ = 1;
// Async I/O handling
std::thread readerThread_;
std::mutex mutex_;
std::condition_variable cv_;
public:
LspProcessClient(const std::string& command,
const std::vector<std::string>& args);
// ... implementation of LspClient interface
};
```
### 1.3 JSON-RPC Transport Layer
```c++
// JsonRpcTransport.h
class JsonRpcTransport {
public:
// Send a request and get the request ID
int sendRequest(const std::string& method, const nlohmann::json& params);
// Send a notification (no response expected)
void sendNotification(const std::string& method, const nlohmann::json& params);
// Read next message (blocking)
std::optional<JsonRpcMessage> readMessage();
private:
void writeMessage(const nlohmann::json& message);
std::string readContentLength();
int fdIn_; // stdin to server
int fdOut_; // stdout from server
};
```
---
## 2. Incremental Document Updates
### 2.1 Change Tracking in Buffer
The key to efficient LSP integration is tracking changes incrementally. This integrates with the existing `Buffer`
class:
```c++
// TextDocumentContentChangeEvent.h
struct TextDocumentContentChangeEvent {
std::optional<Range> range; // If nullopt, entire document changed
std::optional<int> rangeLength; // Deprecated but some servers use it
std::string text;
};
// BufferChangeTracker.h - Integrates with Buffer to track changes
class BufferChangeTracker {
public:
explicit BufferChangeTracker(Buffer* buffer);
// Called by Buffer on each edit operation
void recordInsertion(Position pos, const std::string& text);
void recordDeletion(Range range, const std::string& deletedText);
// Get accumulated changes since last sync
std::vector<TextDocumentContentChangeEvent> getChanges();
// Clear changes after sending to LSP
void clearChanges();
// Get current document version
int getVersion() const { return version_; }
private:
Buffer* buffer_;
int version_ = 0;
std::vector<TextDocumentContentChangeEvent> pendingChanges_;
// Optional: Coalesce adjacent changes
void coalesceChanges();
};
```
### 2.2 Integration with Buffer Operations
```c++
// Buffer.h additions
class Buffer {
// ... existing code ...
// LSP integration
void setChangeTracker(std::unique_ptr<BufferChangeTracker> tracker);
BufferChangeTracker* getChangeTracker() { return changeTracker_.get(); }
// These methods should call tracker when present
void insertText(Position pos, const std::string& text);
void deleteRange(Range range);
private:
std::unique_ptr<BufferChangeTracker> changeTracker_;
};
```
### 2.3 Sync Strategy Selection
```c++
// LspSyncMode.h
enum class LspSyncMode {
None, // No sync
Full, // Send full document on each change
Incremental // Send only changes (preferred)
};
// Determined during server capability negotiation
LspSyncMode negotiateSyncMode(const ServerCapabilities& caps);
```
---
## 3. Diagnostics Display System
### 3.1 Diagnostic Data Model
```c++
// Diagnostic.h
enum class DiagnosticSeverity {
Error = 1,
Warning = 2,
Information = 3,
Hint = 4
};
struct Diagnostic {
Range range;
DiagnosticSeverity severity;
std::optional<std::string> code;
std::optional<std::string> source;
std::string message;
std::vector<DiagnosticRelatedInformation> relatedInfo;
};
// DiagnosticStore.h - Central storage for diagnostics
class DiagnosticStore {
public:
void setDiagnostics(const std::string& uri,
std::vector<Diagnostic> diagnostics);
const std::vector<Diagnostic>& getDiagnostics(const std::string& uri) const;
std::vector<Diagnostic> getDiagnosticsAtLine(const std::string& uri,
int line) const;
std::optional<Diagnostic> getDiagnosticAtPosition(const std::string& uri,
Position pos) const;
void clear(const std::string& uri);
void clearAll();
// Statistics
int getErrorCount(const std::string& uri) const;
int getWarningCount(const std::string& uri) const;
private:
std::unordered_map<std::string, std::vector<Diagnostic>> diagnostics_;
};
```
### 3.2 Frontend-Agnostic Diagnostic Display Interface
Following kte's existing abstraction pattern with `Frontend`, `Renderer`, and `InputHandler`:
```c++
// DiagnosticDisplay.h - Abstract interface for showing diagnostics
class DiagnosticDisplay {
public:
virtual ~DiagnosticDisplay() = default;
// Update the diagnostic indicators for a buffer
virtual void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) = 0;
// Show inline diagnostic at cursor position
virtual void showInlineDiagnostic(const Diagnostic& diagnostic) = 0;
// Show diagnostic list/panel
virtual void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) = 0;
virtual void hideDiagnosticList() = 0;
// Status bar summary
virtual void updateStatusBar(int errorCount, int warningCount) = 0;
};
```
### 3.3 Terminal Diagnostic Display
```c++
// TerminalDiagnosticDisplay.h
class TerminalDiagnosticDisplay : public DiagnosticDisplay {
public:
explicit TerminalDiagnosticDisplay(TerminalRenderer* renderer);
void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) override;
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
void hideDiagnosticList() override;
void updateStatusBar(int errorCount, int warningCount) override;
private:
TerminalRenderer* renderer_;
// Terminal-specific display strategies
void renderGutterMarkers(const std::vector<Diagnostic>& diagnostics);
void renderUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderVirtualText(const Diagnostic& diagnostic);
};
```
**Terminal Display Strategies:**
1. **Gutter markers**: Show `E` (error), `W` (warning), `I` (info), `H` (hint) in left gutter
2. **Underlines**: Use terminal underline/curly underline capabilities (where supported)
3. **Virtual text**: Display diagnostic message at end of line (configurable)
4. **Status line**: `[E:3 W:5]` summary
5. **Message line**: Full diagnostic on cursor line shown in bottom bar
```
1 │ fn main() {
E 2 │ let x: i32 = "hello";
3 │ }
──────────────────────────────────────
error[E0308]: mismatched types
expected `i32`, found `&str`
[E:1 W:0] main.rs
```
### 3.4 GUI Diagnostic Display
```c++
// GUIDiagnosticDisplay.h
class GUIDiagnosticDisplay : public DiagnosticDisplay {
public:
explicit GUIDiagnosticDisplay(GUIRenderer* renderer, GUITheme* theme);
void updateDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics) override;
void showInlineDiagnostic(const Diagnostic& diagnostic) override;
void showDiagnosticList(const std::vector<Diagnostic>& diagnostics) override;
void hideDiagnosticList() override;
void updateStatusBar(int errorCount, int warningCount) override;
private:
GUIRenderer* renderer_;
GUITheme* theme_;
// GUI-specific display
void renderWavyUnderlines(const std::vector<Diagnostic>& diagnostics);
void renderTooltip(Position pos, const Diagnostic& diagnostic);
void renderDiagnosticPanel();
};
```
**GUI Display Features:**
1. **Wavy underlines**: Classic IDE-style (red for errors, yellow for warnings, etc.)
2. **Gutter icons**: Colored icons/dots in the gutter
3. **Hover tooltips**: Rich tooltips on hover showing full diagnostic
4. **Diagnostic panel**: Bottom panel with clickable diagnostic list
5. **Minimap markers**: Colored marks on the minimap (if present)
---
## 4. LspManager - Central Coordination
```c++
// LspManager.h
class LspManager {
public:
explicit LspManager(Editor* editor, DiagnosticDisplay* display);
// Server management
void registerServer(const std::string& languageId,
const LspServerConfig& config);
bool startServerForBuffer(Buffer* buffer);
void stopServer(const std::string& languageId);
void stopAllServers();
// Document sync
void onBufferOpened(Buffer* buffer);
void onBufferChanged(Buffer* buffer);
void onBufferClosed(Buffer* buffer);
void onBufferSaved(Buffer* buffer);
// Feature requests
void requestCompletion(Buffer* buffer, Position pos,
CompletionCallback callback);
void requestHover(Buffer* buffer, Position pos,
HoverCallback callback);
void requestDefinition(Buffer* buffer, Position pos,
LocationCallback callback);
// Configuration
void setDebugLogging(bool enabled);
private:
Editor* editor_;
DiagnosticDisplay* display_;
DiagnosticStore diagnosticStore_;
std::unordered_map<std::string, std::unique_ptr<LspClient>> servers_;
std::unordered_map<std::string, LspServerConfig> serverConfigs_;
void handleDiagnostics(const std::string& uri,
const std::vector<Diagnostic>& diagnostics);
std::string getLanguageId(Buffer* buffer);
std::string getUri(Buffer* buffer);
};
```
---
## 5. Configuration
```c++
// LspServerConfig.h
struct LspServerConfig {
std::string command;
std::vector<std::string> args;
std::vector<std::string> filePatterns; // e.g., {"*.rs", "*.toml"}
std::string rootPatterns; // e.g., "Cargo.toml"
LspSyncMode preferredSyncMode = LspSyncMode::Incremental;
bool autostart = true;
std::unordered_map<std::string, nlohmann::json> initializationOptions;
std::unordered_map<std::string, nlohmann::json> settings;
};
// Default configurations
std::vector<LspServerConfig> getDefaultServerConfigs() {
return {
{
.command = "rust-analyzer",
.filePatterns = {"*.rs"},
.rootPatterns = "Cargo.toml"
},
{
.command = "clangd",
.args = {"--background-index"},
.filePatterns = {"*.c", "*.cc", "*.cpp", "*.h", "*.hpp"},
.rootPatterns = "compile_commands.json"
},
{
.command = "gopls",
.filePatterns = {"*.go"},
.rootPatterns = "go.mod"
},
// ... more servers
};
}
```
---
## 6. Implementation Phases
### Phase 1: Foundation (2-3 weeks)
- [ ] JSON-RPC transport layer
- [ ] Process management for LSP servers
- [ ] Basic `LspClient` with initialize/shutdown
- [ ] `textDocument/didOpen`, `textDocument/didClose` (full sync)
### Phase 2: Incremental Sync (1-2 weeks)
- [ ] `BufferChangeTracker` integration with `Buffer`
- [ ] `textDocument/didChange` with incremental updates
- [ ] Change coalescing for rapid edits
- [ ] Version tracking
### Phase 3: Diagnostics (2-3 weeks)
- [ ] `DiagnosticStore` implementation
- [ ] `TerminalDiagnosticDisplay` with gutter markers & status line
- [ ] `GUIDiagnosticDisplay` with wavy underlines & tooltips
- [ ] `textDocument/publishDiagnostics` handling
### Phase 4: Language Features (3-4 weeks)
- [ ] Completion (`textDocument/completion`)
- [ ] Hover (`textDocument/hover`)
- [ ] Go to definition (`textDocument/definition`)
- [ ] Find references (`textDocument/references`)
- [ ] Code actions (`textDocument/codeAction`)
### Phase 5: Polish & Advanced Features (2-3 weeks)
- [ ] Multiple server support
- [ ] Server auto-detection
- [ ] Configuration file support
- [ ] Workspace symbol search
- [ ] Rename refactoring
---
## 7. Alignment with kte Core Principles
### 7.1 Frontend/Backend Separation
- LSP logic is completely separate from display
- `DiagnosticDisplay` interface allows identical behavior across Terminal/GUI
- Follows existing pattern: `Renderer`, `InputHandler`, `Frontend`
### 7.2 Testability
- `LspClient` is abstract, enabling `MockLspClient` for testing
- `DiagnosticDisplay` can be mocked for testing diagnostic flow
- Change tracking can be unit tested in isolation
### 7.3 Performance
- Incremental sync minimizes data sent to LSP servers
- Async message handling doesn't block UI
- Diagnostic rendering is batched
### 7.4 Simplicity
- Minimal dependencies (nlohmann/json for JSON handling)
- Self-contained process management
- Clear separation of concerns
---
## 8. File Organization
```
kte/
├── lsp/
│ ├── LspClient.h
│ ├── LspProcessClient.h
│ ├── LspProcessClient.cc
│ ├── LspManager.h
│ ├── LspManager.cc
│ ├── LspServerConfig.h
│ ├── JsonRpcTransport.h
│ ├── JsonRpcTransport.cc
│ ├── LspTypes.h # Position, Range, Location, etc.
│ ├── Diagnostic.h
│ ├── DiagnosticStore.h
│ ├── DiagnosticStore.cc
│ └── BufferChangeTracker.h
├── diagnostic/
│ ├── DiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.h
│ ├── TerminalDiagnosticDisplay.cc
│ ├── GUIDiagnosticDisplay.h
│ └── GUIDiagnosticDisplay.cc
```
---
## 9. Dependencies
- **nlohmann/json**: JSON parsing/serialization (header-only)
- **POSIX/Windows process APIs**: For spawning LSP servers
- Existing kte infrastructure: `Buffer`, `Renderer`, `Frontend`, etc.
---
This plan provides a solid foundation for LSP support while maintaining kte's clean architecture. The key insight is
that LSP is fundamentally a backend feature that should be displayed through the existing frontend abstraction layer,
ensuring consistent behavior across terminal and GUI modes.

102
docs/syntax on.md Normal file
View File

@@ -0,0 +1,102 @@
### Objective
Introduce fast, minimaldependency syntax highlighting to kte, consistent with current architecture (Editor/Buffer + GUI/Terminal renderers), preserving ke UX and performance.
### Guiding principles
- Keep core small and fast; no heavy deps (C++17 only).
- Start simple (stateless line regex), evolve incrementally (stateful, caching).
- Work in both Terminal (ncurses) and GUI (ImGui) with consistent token classes and theme mapping.
- Integrate without disrupting existing search highlight, selection, or cursor rendering.
### Scope of v1
- Languages: plain text (off), C/C++ minimal set (keywords, types, strings, chars, comments, numbers, preprocessor).
- Stateless perline highlighting; handle singleline comments and strings; defer multiline state to v2.
- Toggle: `:syntax on|off` and perbuffer filetype selection.
### Architecture
1. Core types (new):
- `enum class TokenKind { Default, Keyword, Type, String, Char, Comment, Number, Preproc, Constant, Function, Operator, Punctuation, Identifier, Whitespace, Error };`
- `struct HighlightSpan { int col_start; int col_end; TokenKind kind; };` // 0based columns in buffer indices per rendered line
- `struct LineHighlight { std::vector<HighlightSpan> spans; uint64_t version; };`
2. Interfaces (new):
- `class LanguageHighlighter { public: virtual ~LanguageHighlighter() = default; virtual void HighlightLine(const Buffer& buf, int row, std::vector<HighlightSpan>& out) const = 0; virtual bool Stateful() const { return false; } };`
- `class HighlighterEngine { public: void SetHighlighter(std::unique_ptr<LanguageHighlighter>); const LineHighlight& GetLine(const Buffer&, int row, uint64_t buf_version); void InvalidateFrom(int row); };`
- `class HighlighterRegistry { public: static const LanguageHighlighter& ForFiletype(std::string_view ft); static std::string DetectForPath(std::string_view path, std::string_view first_line); };`
3. Editor/Buffer integration:
- PerBuffer settings: `bool syntax_enabled; std::string filetype; std::unique_ptr<HighlighterEngine> highlighter;`
- Buffer emits a monotonically increasing `version` on edit; renderers request line highlights by `(row, version)`.
- Invalidate cache minimally on edits (v1: current line only; v2: from current line down when stateful constructs present).
### Rendering integration
- TerminalRenderer/GUIRenderer changes:
- During line rendering, query `Editor.CurrentBuffer()->highlighter->GetLine(buf, row, buf_version)` to obtain spans.
- Apply token styles while drawing glyph runs.
- Zorder and blending:
1) Backgrounds (e.g., selection, search highlight rectangles)
2) Text with syntax colors
3) Cursor/IME decorations
- Search highlights must remain visible over syntax colors:
- Terminal: combine color/attr with reverse/bold for search; if color conflicts, prefer search.
- GUI: draw semitransparent rects behind text (already present); keep syntax color for text.
### Theme and color mapping
- Extend `GUITheme.h` with a `SyntaxPalette` mapping `TokenKind -> ImVec4 ink` (and optional background tint for comments/strings disabled by default). Provide default Light/Dark palettes.
- Terminal: map `TokenKind` to ncurses color pairs where available; degrade gracefully on 8/16color terminals (e.g., comments=dim, keywords=bold, strings=yellow/green if available).
### Language detection
- v1: by file extension; allow manual `:set filetype=<lang>`.
- v2: add shebang detection for scripts, simple modelines (optional).
### Commands/UX
- `:syntax on|off` — global default; buffer inherits on open.
- `:set filetype=<lang>` — perbuffer override.
- `:syntax reload` — rebuild patterns/themes.
- Status line shows filetype and syntax state when changed.
### Implementation plan (phased)
1. Phase 1 — Minimal regex highlighter for C/C++
- Implement `CppRegexHighlighter : LanguageHighlighter` with precompiled `std::regex` (or handrolled simple scanners to avoid regex backtracking). Classes: line comment `//…`, block comment start `/*` (no state), string `"…"`, char `'…'` (no multiline), numbers, keywords/types, preprocessor `^\s*#\w+`.
- Add `HighlighterEngine` with a simple perrow cache keyed by `(row, buf_version)`; no background worker.
- Integrate into both renderers; add palette to `GUITheme.h`; add terminal color selection.
- Add commands.
2. Phase 2 — Stateful constructs and more languages
- Add state machine for multiline comments `/*…*/` and multiline strings (C++11 raw strings), with invalidation from edit line downward until state stabilizes.
- Add simple highlighters: JSON (strings, numbers, booleans, null, punctuation), Markdown (headers/emphasis/code fences), Shell (comments, strings, keywords), Go (types, constants, keywords), Python (strings, comments, keywords), Rust (strings, comments, keywords), Lisp (comments, strings, keywords),.
- Filetype detection by extension + shebang.
3. Phase 3 — Performance and caching
- Viewportfirst highlighting: compute only visible rows each frame; background task warms cache around viewport.
- Reuse span buffers, avoid allocations; smallvector optimization if needed.
- Bench with large files; ensure O(n_visible) cost per frame.
4. Phase 4 — Extensibility
- Public registration API for external highlighters.
- Optional Treesitter adapter behind a compile flag (off by default) to keep dependencies minimal.
### Data flow (per frame)
- Renderer asks Editor for Buffer and viewport rows.
- For each row: `engine.GetLine(buf, row, buf.version)` → spans.
- Renderer emits runs with style from `SyntaxPalette[kind]`.
- Search highlights are applied as separate background rectangles (GUI) or attribute toggles (Terminal), not overriding text color.
### Testing
- Unit tests for tokenization per language: golden inputs → spans.
- Fuzz/edge cases: escaped quotes, numeric literals, preprocessor lines.
- Renderer tests with `TestRenderer` asserting the sequence of style changes for a line.
- Performance tests: highlight 1k visible lines repeatedly; assert time under threshold.
### Risks and mitigations
- Regex backtracking/perf: prefer linear scans; precompute keyword tables; avoid nested regex.
- Terminal color limitations: featuredetect colors; provide bold/dim fallbacks.
- Stateful correctness: invalidate conservatively (from edit line downward) and cap work per frame.
### Deliverables
- New files: `Highlight.h/.cc`, `HighlighterEngine.h/.cc`, `LanguageHighlighter.h`, `CppHighlighter.h/.cc`, optional `HighlighterRegistry.h/.cc`.
- Renderer updates: `GUIRenderer.cc`, `TerminalRenderer.cc` to consume spans.
- Theming: `GUITheme.h` additions for syntax colors.
- Editor/Buffer: perbuffer syntax settings and highlighter handle.
- Commands in `Command.cc` and help text updates.
- Docs: README/ROADMAP update and a brief `docs/syntax.md`.
- Tests: unit and renderer golden tests.

70
docs/syntax.md Normal file
View File

@@ -0,0 +1,70 @@
Syntax highlighting in kte
==========================
Overview
--------
kte provides lightweight syntax highlighting with a pluggable highlighter interface. The initial implementation targets C/C++ and focuses on speed and responsiveness.
Core types
----------
- `TokenKind` — token categories (keywords, types, strings, comments, numbers, preprocessor, operators, punctuation, identifiers, whitespace, etc.).
- `HighlightSpan` — a half-open column range `[col_start, col_end)` with a `TokenKind`.
- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version` used to compute it.
Engine and caching
------------------
- `HighlighterEngine` maintains a per-line cache of `LineHighlight` keyed by row and buffer version.
- Cache invalidation occurs when the buffer version changes or when the buffer calls `InvalidateFrom(row)`, which clears cached lines and line states from `row` downward.
- The engine supports both stateless and stateful highlighters. For stateful highlighters, it memoizes a simple per-line state and computes lines sequentially when necessary.
Stateful highlighters
---------------------
- `LanguageHighlighter` is the base interface for stateless per-line tokenization.
- `StatefulHighlighter` extends it with a `LineState` and the method `HighlightLineStateful(buf, row, prev_state, out)`.
- The engine detects `StatefulHighlighter` via dynamic_cast and feeds each line the previous lines state, caching the resulting state per line.
C/C++ highlighter
-----------------
- `CppHighlighter` implements `StatefulHighlighter`.
- Stateless constructs: line comments `//`, strings `"..."`, chars `'...'`, numbers, identifiers (keywords/types), preprocessor at beginning of line after leading whitespace, operators/punctuation, and whitespace.
- Stateful constructs (v2):
- Multi-line block comments `/* ... */` — the state records whether the next line continues a comment.
- Raw strings `R"delim(... )delim"` — the state tracks whether we are inside a raw string and its delimiter `delim` until the closing sequence appears.
Limitations and TODOs
---------------------
- Raw string detection is intentionally simple and does not handle all corner cases of the C++ standard.
- Preprocessor handling is line-based; continuation lines with `\\` are not yet tracked.
- No semantic analysis; identifiers are classified via small keyword/type sets.
- Additional languages (JSON, Markdown, Shell, Python, Go, Rust, Lisp, …) are planned.
- Terminal color mapping is conservative to support 8/16-color terminals. Rich color-pair themes can be added later.
Renderer integration
--------------------
- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
- Search highlight and cursor overlays take precedence over syntax colors.
Extensibility (Phase 4)
-----------------------
- Public registration API: external code can register custom highlighters by filetype.
- Use `HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
- Registered factories are preferred over built-ins for the same filetype key.
- Filetype keys are normalized via `HighlighterRegistry::Normalize()`.
- Optional Tree-sitter adapter: disabled by default to keep dependencies minimal.
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if needed.
- Register a Tree-sitter-backed highlighter for a language (example assumes you link a grammar):
```c++
extern "C" const TSLanguage* tree_sitter_c();
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
```
- Current adapter is a stub scaffold; it compiles and integrates cleanly when enabled, but
intentionally emits no spans until Tree-sitter node-to-token mapping is implemented.

View File

@@ -1,3 +1,9 @@
#!/usr/bin/env bash
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc -fmt 3
fmt_arg=""
if [ "${V}" = "1" ]
then
fmt_args="-fmt 3"
fi
ls -1 *.cc *.h | grep -v '^Font.h$' | xargs cloc ${fmt_args}

279
syntax/CppHighlighter.cc Normal file
View File

@@ -0,0 +1,279 @@
#include "CppHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static bool
is_digit(char c)
{
return c >= '0' && c <= '9';
}
CppHighlighter::CppHighlighter()
{
const char *kw[] = {
"if", "else", "for", "while", "do", "switch", "case", "default", "break", "continue",
"return", "goto", "struct", "class", "namespace", "using", "template", "typename",
"public", "private", "protected", "virtual", "override", "const", "constexpr", "auto",
"static", "inline", "operator", "new", "delete", "try", "catch", "throw", "friend",
"enum", "union", "extern", "volatile", "mutable", "noexcept", "sizeof", "this"
};
for (auto s: kw)
keywords_.insert(s);
const char *types[] = {
"int", "long", "short", "char", "signed", "unsigned", "float", "double", "void",
"bool", "wchar_t", "size_t", "ptrdiff_t", "uint8_t", "uint16_t", "uint32_t", "uint64_t",
"int8_t", "int16_t", "int32_t", "int64_t"
};
for (auto s: types)
types_.insert(s);
}
bool
CppHighlighter::is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
bool
CppHighlighter::is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
void
CppHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
// Stateless entry simply delegates to stateful with a clean previous state
StatefulHighlighter::LineState prev;
(void) HighlightLineStateful(buf, row, prev, out);
}
StatefulHighlighter::LineState
CppHighlighter::HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
StatefulHighlighter::LineState state = prev;
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
if (s.empty())
return state;
auto push = [&](int a, int b, TokenKind k) {
if (b > a)
out.push_back({a, b, k});
};
int n = static_cast<int>(s.size());
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
int i = 0;
// Continue multi-line raw string from previous line
if (state.in_raw_string) {
std::string needle = ")" + state.raw_delim + "\"";
auto pos = s.find(needle);
if (pos == std::string::npos) {
push(0, n, TokenKind::String);
state.in_raw_string = true;
return state;
} else {
int end = static_cast<int>(pos + needle.size());
push(0, end, TokenKind::String);
i = end;
state.in_raw_string = false;
state.raw_delim.clear();
}
}
// Continue multi-line block comment from previous line
if (state.in_block_comment) {
int j = i;
while (i + 1 < n) {
if (s[i] == '*' && s[i + 1] == '/') {
i += 2;
push(j, i, TokenKind::Comment);
state.in_block_comment = false;
break;
}
++i;
}
if (state.in_block_comment) {
push(j, n, TokenKind::Comment);
return state;
}
}
while (i < n) {
char c = s[i];
// Preprocessor at beginning of line (after leading whitespace)
if (i == bol && c == '#') {
push(0, n, TokenKind::Preproc);
break;
}
// Whitespace
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(i, j, TokenKind::Whitespace);
i = j;
continue;
}
// Line comment
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(i, n, TokenKind::Comment);
break;
}
// Block comment
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (closed) {
push(i, j, TokenKind::Comment);
i = j;
continue;
}
// Spill to next lines
push(i, n, TokenKind::Comment);
state.in_block_comment = true;
return state;
}
// Raw string start: very simple detection: R"delim(
if (c == 'R' && i + 1 < n && s[i + 1] == '"') {
int k = i + 2;
std::string delim;
while (k < n && s[k] != '(') {
delim.push_back(s[k]);
++k;
}
if (k < n && s[k] == '(') {
int body_start = k + 1;
std::string needle = ")" + delim + "\"";
auto pos = s.find(needle, static_cast<std::size_t>(body_start));
if (pos == std::string::npos) {
push(i, n, TokenKind::String);
state.in_raw_string = true;
state.raw_delim = delim;
return state;
} else {
int end = static_cast<int>(pos + needle.size());
push(i, end, TokenKind::String);
i = end;
continue;
}
}
// If malformed, just treat 'R' as identifier fallback
}
// Regular string literal
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(i, j, TokenKind::String);
i = j;
continue;
}
// Char literal
if (c == '\'') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '\'')
break;
}
push(i, j, TokenKind::Char);
i = j;
continue;
}
// Number literal (simple)
if (is_digit(c) || (c == '.' && i + 1 < n && is_digit(s[i + 1]))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
s[j] == 'X' || s[j] == 'b' || s[j] == 'B' || s[j] == '_'))
++j;
push(i, j, TokenKind::Number);
i = j;
continue;
}
// Identifier / keyword / type
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (keywords_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(i, j, k);
i = j;
continue;
}
// Operators and punctuation (single char for now)
TokenKind kind = TokenKind::Operator;
if (std::ispunct(static_cast<unsigned char>(c)) && c != '_' && c != '#') {
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
kind = TokenKind::Punctuation;
push(i, i + 1, kind);
++i;
continue;
}
// Fallback
push(i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

35
syntax/CppHighlighter.h Normal file
View File

@@ -0,0 +1,35 @@
// CppHighlighter.h - minimal stateless C/C++ line highlighter
#pragma once
#include <regex>
#include <string>
#include <unordered_set>
#include <vector>
#include "LanguageHighlighter.h"
class Buffer;
namespace kte {
class CppHighlighter final : public StatefulHighlighter {
public:
CppHighlighter();
~CppHighlighter() override = default;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> keywords_;
std::unordered_set<std::string> types_;
static bool is_ident_start(char c);
static bool is_ident_char(char c);
};
} // namespace kte

159
syntax/ErlangHighlighter.cc Normal file
View File

@@ -0,0 +1,159 @@
#include "ErlangHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_' || c == '\'';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '@' || c == ':' || c == '?';
}
ErlangHighlighter::ErlangHighlighter()
{
const char *kw[] = {
"after", "begin", "case", "catch", "cond", "div", "end", "fun", "if", "let", "of",
"receive", "when", "try", "rem", "and", "andalso", "orelse", "not", "band", "bor", "bxor",
"bnot", "xor", "module", "export", "import", "record", "define", "undef", "include", "include_lib"
};
for (auto s: kw)
kws_.insert(s);
}
void
ErlangHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// comment
if (c == '%') {
push(out, i, n, TokenKind::Comment);
break;
}
// strings
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
// char literal $X
if (c == '$') {
int j = i + 1;
if (j < n && s[j] == '\\' && j + 1 < n)
j += 2;
else if (j < n)
++j;
push(out, i, j, TokenKind::Char);
i = j;
continue;
}
// numbers
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '#' || s[j] == '.' ||
s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
// atoms/variables/identifiers (including quoted atoms)
if (is_ident_start(c)) {
// quoted atom: '...'
if (c == '\'') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (d == '\'') {
if (j < n && s[j] == '\'') {
++j;
continue;
}
break;
}
if (d == '\\')
esc = !esc;
}
push(out, i, j, TokenKind::Identifier);
i = j;
continue;
}
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
// lowercase leading -> atom/function/module; uppercase or '_' -> variable
TokenKind k = TokenKind::Identifier;
// keyword check (lowercase)
std::string lower;
lower.reserve(id.size());
for (char ch: id)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
if (kws_.count(lower))
k = TokenKind::Keyword;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ',' || c == ';' || c == '(' || c == ')' || c == '[' || c == ']' || c == '{' || c ==
'}')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

View File

@@ -0,0 +1,17 @@
// ErlangHighlighter.h - simple Erlang highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class ErlangHighlighter final : public LanguageHighlighter {
public:
ErlangHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

121
syntax/ForthHighlighter.cc Normal file
View File

@@ -0,0 +1,121 @@
#include "ForthHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_word_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '>' || c == '<' || c == '?';
}
ForthHighlighter::ForthHighlighter()
{
const char *kw[] = {
":", ";", "if", "else", "then", "begin", "until", "while", "repeat",
"do", "loop", "+loop", "leave", "again", "case", "of", "endof", "endcase",
".", ".r", ".s", ".\"", ",", "cr", "emit", "type", "key",
"+", "-", "*", "/", "mod", "/mod", "+-", "abs", "min", "max",
"dup", "drop", "swap", "over", "rot", "-rot", "nip", "tuck", "pick", "roll",
"and", "or", "xor", "invert", "lshift", "rshift",
"variable", "constant", "value", "to", "create", "does>", "allot", ",",
"cells", "cell+", "chars", "char+",
"[", "]", "immediate",
"s\"", ".\""
};
for (auto s: kw)
kws_.insert(s);
}
void
ForthHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// backslash comment to end of line
if (c == '\\') {
push(out, i, n, TokenKind::Comment);
break;
}
// parenthesis comment ( ... ) if at word boundary
if (c == '(') {
int j = i + 1;
while (j < n && s[j] != ')')
++j;
if (j < n)
++j;
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
// strings: ." ... " and S" ... " and raw "..."
if (c == '"') {
int j = i + 1;
while (j < n && s[j] != '"')
++j;
if (j < n)
++j;
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '#'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
// word/identifier
if (std::isalpha(static_cast<unsigned char>(c)) || std::ispunct(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && is_word_char(s[j]))
++j;
std::string w = s.substr(i, j - i);
// normalize to lowercase for keyword compare (Forth is case-insensitive typically)
std::string lower;
lower.reserve(w.size());
for (char ch: w)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
TokenKind k = kws_.count(lower) ? TokenKind::Keyword : TokenKind::Identifier;
// Single-char punctuation fallback
if (w.size() == 1 && std::ispunct(static_cast<unsigned char>(w[0])) && !kws_.count(lower)) {
k = (w[0] == '(' || w[0] == ')' || w[0] == ',')
? TokenKind::Punctuation
: TokenKind::Operator;
}
push(out, i, j, k);
i = j;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

17
syntax/ForthHighlighter.h Normal file
View File

@@ -0,0 +1,17 @@
// ForthHighlighter.h - simple Forth highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class ForthHighlighter final : public LanguageHighlighter {
public:
ForthHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

157
syntax/GoHighlighter.cc Normal file
View File

@@ -0,0 +1,157 @@
#include "GoHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
GoHighlighter::GoHighlighter()
{
const char *kw[] = {
"break", "case", "chan", "const", "continue", "default", "defer", "else", "fallthrough", "for", "func",
"go", "goto", "if", "import", "interface", "map", "package", "range", "return", "select", "struct",
"switch", "type", "var"
};
for (auto s: kw)
kws_.insert(s);
const char *tp[] = {
"bool", "byte", "complex64", "complex128", "error", "float32", "float64", "int", "int8", "int16",
"int32", "int64", "rune", "string", "uint", "uint8", "uint16", "uint32", "uint64", "uintptr"
};
for (auto s: tp)
types_.insert(s);
}
void
GoHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
// line comment
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
if (c == '"' || c == '`') {
char q = c;
int j = i + 1;
bool esc = false;
if (q == '`') {
while (j < n && s[j] != '`')
++j;
if (j < n)
++j;
} else {
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'x' ||
s[j] == 'X' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/GoHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// GoHighlighter.h - simple Go highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class GoHighlighter final : public LanguageHighlighter {
public:
GoHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

209
syntax/HighlighterEngine.cc Normal file
View File

@@ -0,0 +1,209 @@
#include "HighlighterEngine.h"
#include "../Buffer.h"
#include "LanguageHighlighter.h"
#include <thread>
namespace kte {
HighlighterEngine::HighlighterEngine() = default;
HighlighterEngine::~HighlighterEngine()
{
// stop background worker
if (worker_running_.load()) {
{
std::lock_guard<std::mutex> lock(mtx_);
worker_running_.store(false);
has_request_ = true; // wake it up to exit
}
cv_.notify_one();
if (worker_.joinable())
worker_.join();
}
}
void
HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
{
std::lock_guard<std::mutex> lock(mtx_);
hl_ = std::move(hl);
cache_.clear();
state_cache_.clear();
state_last_contig_.clear();
}
const LineHighlight &
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
{
std::unique_lock<std::mutex> lock(mtx_);
auto it = cache_.find(row);
if (it != cache_.end() && it->second.version == buf_version) {
return it->second;
}
// Prepare destination slot to reuse its capacity and avoid allocations
LineHighlight &slot = cache_[row];
slot.version = buf_version;
slot.spans.clear();
if (!hl_) {
return slot;
}
// Copy shared_ptr-like raw pointer for use outside critical sections
LanguageHighlighter *hl_ptr = hl_.get();
bool is_stateful = dynamic_cast<StatefulHighlighter *>(hl_ptr) != nullptr;
if (!is_stateful) {
// Stateless fast path: we can release the lock while computing to reduce contention
auto &out = slot.spans;
lock.unlock();
hl_ptr->HighlightLine(buf, row, out);
return cache_.at(row);
}
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
// but release during heavy computation.
auto *stateful = static_cast<StatefulHighlighter *>(hl_ptr);
StatefulHighlighter::LineState prev_state;
int start_row = -1;
if (!state_cache_.empty()) {
// linear search over map (unordered), track best candidate
int best = -1;
for (const auto &kv: state_cache_) {
int r = kv.first;
if (r <= row - 1 && kv.second.version == buf_version) {
if (r > best)
best = r;
}
}
if (best >= 0) {
start_row = best;
prev_state = state_cache_.at(best).state;
}
}
// We'll compute states and the target line's spans without holding the lock for most of the work.
// Create a local copy of prev_state and iterate rows; we will update caches under lock.
lock.unlock();
StatefulHighlighter::LineState cur_state = prev_state;
for (int r = start_row + 1; r <= row; ++r) {
std::vector<HighlightSpan> tmp;
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
// Update state cache for r
std::lock_guard<std::mutex> gl(mtx_);
StateEntry se;
se.version = buf_version;
se.state = next_state;
state_cache_[r] = se;
cur_state = next_state;
}
// Return reference under lock to ensure slot's address stability in map
lock.lock();
return cache_.at(row);
}
void
HighlighterEngine::InvalidateFrom(int row)
{
std::lock_guard<std::mutex> lock(mtx_);
if (cache_.empty())
return;
// Simple implementation: erase all rows >= row
for (auto it = cache_.begin(); it != cache_.end();) {
if (it->first >= row)
it = cache_.erase(it);
else
++it;
}
if (!state_cache_.empty()) {
for (auto it = state_cache_.begin(); it != state_cache_.end();) {
if (it->first >= row)
it = state_cache_.erase(it);
else
++it;
}
}
}
void
HighlighterEngine::ensure_worker_started() const
{
if (worker_running_.load())
return;
worker_running_.store(true);
worker_ = std::thread([this]() {
this->worker_loop();
});
}
void
HighlighterEngine::worker_loop() const
{
std::unique_lock<std::mutex> lock(mtx_);
while (worker_running_.load()) {
cv_.wait(lock, [this]() {
return has_request_ || !worker_running_.load();
});
if (!worker_running_.load())
break;
WarmRequest req = pending_;
has_request_ = false;
// Copy locals then release lock while computing
lock.unlock();
if (req.buf) {
int start = std::max(0, req.start_row);
int end = std::max(start, req.end_row);
for (int r = start; r <= end; ++r) {
// Re-check version staleness quickly by peeking cache version; not strictly necessary
// Compute line; GetLine is thread-safe
(void) this->GetLine(*req.buf, r, req.version);
}
}
lock.lock();
}
}
void
HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
int warm_margin) const
{
if (row_count <= 0)
return;
// Synchronously compute visible rows to ensure cache hits during draw
int start = std::max(0, first_row);
int end = start + row_count - 1;
int max_rows = static_cast<int>(buf.Nrows());
if (start >= max_rows)
return;
if (end >= max_rows)
end = max_rows - 1;
for (int r = start; r <= end; ++r) {
(void) GetLine(buf, r, buf_version);
}
// Enqueue background warm-around
int warm_start = std::max(0, start - warm_margin);
int warm_end = std::min(max_rows - 1, end + warm_margin);
{
std::lock_guard<std::mutex> lock(mtx_);
pending_.buf = &buf;
pending_.version = buf_version;
pending_.start_row = warm_start;
pending_.end_row = warm_end;
has_request_ = true;
}
ensure_worker_started();
cv_.notify_one();
}
} // namespace kte

View File

@@ -0,0 +1,85 @@
// HighlighterEngine.h - caching layer for per-line highlights
#pragma once
#include <cstdint>
#include <memory>
#include <unordered_map>
#include <vector>
#include <mutex>
#include <condition_variable>
#include <atomic>
#include <thread>
#include "../Highlight.h"
#include "LanguageHighlighter.h"
class Buffer;
namespace kte {
class HighlighterEngine {
public:
HighlighterEngine();
~HighlighterEngine();
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
// Retrieve highlights for a given line and buffer version.
// If cache is stale, recompute using the current highlighter.
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
// Invalidate cached lines from row (inclusive)
void InvalidateFrom(int row);
bool HasHighlighter() const
{
return static_cast<bool>(hl_);
}
// Phase 3: viewport-first prefetch and background warming
// Compute only the visible range now, and enqueue a background warm-around task.
// warm_margin: how many extra lines above/below to warm in the background.
void PrefetchViewport(const Buffer &buf, int first_row, int row_count, std::uint64_t buf_version,
int warm_margin = 200) const;
private:
std::unique_ptr<LanguageHighlighter> hl_;
// Simple cache by row index (mutable to allow caching in const GetLine)
mutable std::unordered_map<int, LineHighlight> cache_;
// For stateful highlighters, remember per-line state (state after finishing that row)
struct StateEntry {
std::uint64_t version{0};
// Using the interface type; forward-declare via header
StatefulHighlighter::LineState state;
};
mutable std::unordered_map<int, StateEntry> state_cache_;
// Track best known contiguous state row for a given version to avoid O(n) scans
mutable std::unordered_map<std::uint64_t, int> state_last_contig_;
// Thread-safety for caches and background worker state
mutable std::mutex mtx_;
// Background warmer
struct WarmRequest {
const Buffer *buf{nullptr};
std::uint64_t version{0};
int start_row{0};
int end_row{0}; // inclusive
};
mutable std::condition_variable cv_;
mutable std::thread worker_;
mutable std::atomic<bool> worker_running_{false};
mutable bool has_request_{false};
mutable WarmRequest pending_{};
void ensure_worker_started() const;
void worker_loop() const;
};
} // namespace kte

View File

@@ -0,0 +1,247 @@
#include "HighlighterRegistry.h"
#include "CppHighlighter.h"
#include <algorithm>
#include <filesystem>
#include <vector>
#include <cctype>
// Forward declare simple highlighters implemented in this project
namespace kte {
// Registration storage
struct RegEntry {
std::string ft; // normalized
HighlighterRegistry::Factory factory;
};
static std::vector<RegEntry> &
registry()
{
static std::vector<RegEntry> reg;
return reg;
}
class JSONHighlighter;
class MarkdownHighlighter;
class ShellHighlighter;
class GoHighlighter;
class PythonHighlighter;
class RustHighlighter;
class LispHighlighter;
class SqlHighlighter;
class ErlangHighlighter;
class ForthHighlighter;
}
// Headers for the above
#include "JsonHighlighter.h"
#include "MarkdownHighlighter.h"
#include "ShellHighlighter.h"
#include "GoHighlighter.h"
#include "PythonHighlighter.h"
#include "RustHighlighter.h"
#include "LispHighlighter.h"
#include "SqlHighlighter.h"
#include "ErlangHighlighter.h"
#include "ForthHighlighter.h"
namespace kte {
static std::string
to_lower(std::string_view s)
{
std::string r(s);
std::transform(r.begin(), r.end(), r.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
return r;
}
std::string
HighlighterRegistry::Normalize(std::string_view ft)
{
std::string f = to_lower(ft);
if (f == "c" || f == "c++" || f == "cc" || f == "hpp" || f == "hh" || f == "h" || f == "cxx")
return "cpp";
if (f == "cpp")
return "cpp";
if (f == "json")
return "json";
if (f == "markdown" || f == "md" || f == "mkd" || f == "mdown")
return "markdown";
if (f == "shell" || f == "sh" || f == "bash" || f == "zsh" || f == "ksh" || f == "fish")
return "shell";
if (f == "go" || f == "golang")
return "go";
if (f == "py" || f == "python")
return "python";
if (f == "rs" || f == "rust")
return "rust";
if (f == "lisp" || f == "scheme" || f == "scm" || f == "rkt" || f == "el" || f == "clj" || f == "cljc" || f ==
"cl")
return "lisp";
if (f == "sql" || f == "sqlite" || f == "sqlite3")
return "sql";
if (f == "erlang" || f == "erl" || f == "hrl")
return "erlang";
if (f == "forth" || f == "fth" || f == "4th" || f == "fs")
return "forth";
return f;
}
std::unique_ptr<LanguageHighlighter>
HighlighterRegistry::CreateFor(std::string_view filetype)
{
std::string ft = Normalize(filetype);
// Prefer externally registered factories
for (const auto &e: registry()) {
if (e.ft == ft && e.factory)
return e.factory();
}
if (ft == "cpp")
return std::make_unique<CppHighlighter>();
if (ft == "json")
return std::make_unique<JSONHighlighter>();
if (ft == "markdown")
return std::make_unique<MarkdownHighlighter>();
if (ft == "shell")
return std::make_unique<ShellHighlighter>();
if (ft == "go")
return std::make_unique<GoHighlighter>();
if (ft == "python")
return std::make_unique<PythonHighlighter>();
if (ft == "rust")
return std::make_unique<RustHighlighter>();
if (ft == "lisp")
return std::make_unique<LispHighlighter>();
if (ft == "sql")
return std::make_unique<SqlHighlighter>();
if (ft == "erlang")
return std::make_unique<ErlangHighlighter>();
if (ft == "forth")
return std::make_unique<ForthHighlighter>();
return nullptr;
}
static std::string
shebang_to_ft(std::string_view first_line)
{
if (first_line.size() < 2 || first_line.substr(0, 2) != "#!")
return "";
std::string low = to_lower(first_line);
if (low.find("python") != std::string::npos)
return "python";
if (low.find("bash") != std::string::npos)
return "shell";
if (low.find("sh") != std::string::npos)
return "shell";
if (low.find("zsh") != std::string::npos)
return "shell";
if (low.find("fish") != std::string::npos)
return "shell";
if (low.find("scheme") != std::string::npos || low.find("racket") != std::string::npos || low.find("guile") !=
std::string::npos)
return "lisp";
return "";
}
std::string
HighlighterRegistry::DetectForPath(std::string_view path, std::string_view first_line)
{
// Extension
std::string p(path);
std::error_code ec;
std::string ext = std::filesystem::path(p).extension().string();
for (auto &ch: ext)
ch = static_cast<char>(std::tolower(static_cast<unsigned char>(ch)));
if (!ext.empty()) {
if (ext == ".c" || ext == ".cc" || ext == ".cpp" || ext == ".cxx" || ext == ".h" || ext == ".hpp" || ext
== ".hh")
return "cpp";
if (ext == ".json")
return "json";
if (ext == ".md" || ext == ".markdown" || ext == ".mkd")
return "markdown";
if (ext == ".sh" || ext == ".bash" || ext == ".zsh" || ext == ".ksh" || ext == ".fish")
return "shell";
if (ext == ".go")
return "go";
if (ext == ".py")
return "python";
if (ext == ".rs")
return "rust";
if (ext == ".lisp" || ext == ".scm" || ext == ".rkt" || ext == ".el" || ext == ".clj" || ext == ".cljc"
|| ext == ".cl")
return "lisp";
if (ext == ".sql" || ext == ".sqlite")
return "sql";
if (ext == ".erl" || ext == ".hrl")
return "erlang";
if (ext == ".forth" || ext == ".fth" || ext == ".4th" || ext == ".fs")
return "forth";
}
// Shebang
std::string ft = shebang_to_ft(first_line);
return ft;
}
} // namespace kte
// Extensibility API implementations
namespace kte {
void
HighlighterRegistry::Register(std::string_view filetype, Factory factory, bool override_existing)
{
std::string ft = Normalize(filetype);
for (auto &e: registry()) {
if (e.ft == ft) {
if (override_existing)
e.factory = std::move(factory);
return;
}
}
registry().push_back(RegEntry{ft, std::move(factory)});
}
bool
HighlighterRegistry::IsRegistered(std::string_view filetype)
{
std::string ft = Normalize(filetype);
for (const auto &e: registry())
if (e.ft == ft)
return true;
return false;
}
std::vector<std::string>
HighlighterRegistry::RegisteredFiletypes()
{
std::vector<std::string> out;
out.reserve(registry().size());
for (const auto &e: registry())
out.push_back(e.ft);
return out;
}
#ifdef KTE_ENABLE_TREESITTER
// Forward declare adapter factory
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)());
void
HighlighterRegistry::RegisterTreeSitter(std::string_view filetype,
const TSLanguage * (*get_language)())
{
std::string ft = Normalize(filetype);
Register(ft, [ft, get_language]() {
return CreateTreeSitterHighlighter(ft.c_str(), reinterpret_cast<const void* (*)()>(get_language));
}, /*override_existing=*/true);
}
#endif
} // namespace kte

View File

@@ -0,0 +1,47 @@
// HighlighterRegistry.h - create/detect language highlighters and allow external registration
#pragma once
#include <functional>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
#include "LanguageHighlighter.h"
namespace kte {
class HighlighterRegistry {
public:
using Factory = std::function<std::unique_ptr<LanguageHighlighter>()>;
// Create a highlighter for normalized filetype id (e.g., "cpp", "json", "markdown", "shell", "go", "python", "rust", "lisp").
static std::unique_ptr<LanguageHighlighter> CreateFor(std::string_view filetype);
// Detect filetype by path extension and shebang (first line).
// Returns normalized id or empty string if unknown.
static std::string DetectForPath(std::string_view path, std::string_view first_line);
// Normalize various aliases/extensions to canonical ids.
static std::string Normalize(std::string_view ft);
// Extensibility: allow external code to register highlighters at runtime.
// The filetype key is normalized via Normalize(). If a factory is already registered for the
// normalized key and override=false, the existing factory is kept.
static void Register(std::string_view filetype, Factory factory, bool override_existing = true);
// Returns true if a factory is registered for the (normalized) filetype.
static bool IsRegistered(std::string_view filetype);
// Return a list of currently registered (normalized) filetypes. Primarily for diagnostics/tests.
static std::vector<std::string> RegisteredFiletypes();
#ifdef KTE_ENABLE_TREESITTER
// Forward declaration to avoid hard dependency when disabled.
struct TSLanguage;
// Convenience: register a Tree-sitter-backed highlighter for a filetype.
// The getter should return a non-null language pointer for the grammar.
static void RegisterTreeSitter(std::string_view filetype,
const TSLanguage * (*get_language)());
#endif
};
} // namespace kte

90
syntax/JsonHighlighter.cc Normal file
View File

@@ -0,0 +1,90 @@
#include "JsonHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static bool
is_digit(char c)
{
return c >= '0' && c <= '9';
}
void
JSONHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
auto push = [&](int a, int b, TokenKind k) {
if (b > a)
out.push_back({a, b, k});
};
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(i, j, TokenKind::String);
i = j;
continue;
}
if (is_digit(c) || (c == '-' && i + 1 < n && is_digit(s[i + 1]))) {
int j = i + 1;
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == 'e' ||
s[j] == 'E' || s[j] == '+' || s[j] == '-' || s[j] == '_'))
++j;
push(i, j, TokenKind::Number);
i = j;
continue;
}
// booleans/null
if (std::isalpha(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && std::isalpha(static_cast<unsigned char>(s[j])))
++j;
std::string id = s.substr(i, j - i);
if (id == "true" || id == "false" || id == "null")
push(i, j, TokenKind::Constant);
else
push(i, j, TokenKind::Identifier);
i = j;
continue;
}
// punctuation
if (c == '{' || c == '}' || c == '[' || c == ']' || c == ',' || c == ':') {
push(i, i + 1, TokenKind::Punctuation);
++i;
continue;
}
// fallback
push(i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

12
syntax/JsonHighlighter.h Normal file
View File

@@ -0,0 +1,12 @@
// JsonHighlighter.h - simple JSON line highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <vector>
namespace kte {
class JSONHighlighter final : public LanguageHighlighter {
public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

View File

@@ -0,0 +1,51 @@
// LanguageHighlighter.h - interface for line-based highlighters
#pragma once
#include <memory>
#include <vector>
#include <string>
#include "../Highlight.h"
class Buffer;
namespace kte {
class LanguageHighlighter {
public:
virtual ~LanguageHighlighter() = default;
// Produce highlight spans for a given buffer row. Implementations should append to out.
virtual void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const = 0;
virtual bool Stateful() const
{
return false;
}
};
// Optional extension for stateful highlighters (e.g., multi-line comments/strings).
// Engines may detect and use this via dynamic_cast without breaking stateless impls.
class StatefulHighlighter : public LanguageHighlighter {
public:
struct LineState {
bool in_block_comment{false};
bool in_raw_string{false};
// For raw strings, remember the delimiter between the opening R"delim( and closing )delim"
std::string raw_delim;
};
// Highlight one line given the previous line state; return the resulting state after this line.
// Implementations should append spans for this line to out and compute the next state.
virtual LineState HighlightLineStateful(const Buffer &buf,
int row,
const LineState &prev,
std::vector<HighlightSpan> &out) const = 0;
bool Stateful() const override
{
return true;
}
};
} // namespace kte

107
syntax/LispHighlighter.cc Normal file
View File

@@ -0,0 +1,107 @@
#include "LispHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
LispHighlighter::LispHighlighter()
{
const char *kw[] = {
"defun", "lambda", "let", "let*", "define", "set!", "if", "cond", "begin", "quote", "quasiquote",
"unquote", "unquote-splicing", "loop", "do", "and", "or", "not"
};
for (auto s: kw)
kws_.insert(s);
}
void
LispHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol < n && s[bol] == ';') {
push(out, bol, n, TokenKind::Comment);
if (bol > 0)
push(out, 0, bol, TokenKind::Whitespace);
return;
}
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == ';') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isalpha(static_cast<unsigned char>(c)) || c == '*' || c == '-' || c == '+' || c == '/' || c ==
'_') {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '*' || s[j] == '-' ||
s[j] == '+' || s[j] == '/' || s[j] == '_' || s[j] == '!'))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = kws_.count(id) ? TokenKind::Keyword : TokenKind::Identifier;
push(out, i, j, k);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isdigit(static_cast<unsigned char>(s[j])) || s[j] == '.'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

17
syntax/LispHighlighter.h Normal file
View File

@@ -0,0 +1,17 @@
// LispHighlighter.h - simple Lisp/Scheme family highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class LispHighlighter final : public LanguageHighlighter {
public:
LispHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

View File

@@ -0,0 +1,132 @@
#include "MarkdownHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push_span(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
void
MarkdownHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
LineState st; // not used in stateless entry
(void) HighlightLineStateful(buf, row, st, out);
}
StatefulHighlighter::LineState
MarkdownHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const
{
StatefulHighlighter::LineState state = prev;
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
// Reuse in_block_comment flag as "in fenced code" state.
if (state.in_block_comment) {
// If line contains closing fence ``` then close after it
auto pos = s.find("```");
if (pos == std::string::npos) {
push_span(out, 0, n, TokenKind::String);
state.in_block_comment = true;
return state;
} else {
int end = static_cast<int>(pos + 3);
push_span(out, 0, end, TokenKind::String);
// rest of line processed normally after fence
int i = end;
// whitespace
if (i < n)
push_span(out, i, n, TokenKind::Default);
state.in_block_comment = false;
return state;
}
}
// Detect fenced code block start at beginning (allow leading spaces)
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol + 3 <= n && s.compare(bol, 3, "```") == 0) {
push_span(out, bol, n, TokenKind::String);
state.in_block_comment = true; // enter fenced mode
return state;
}
// Headings: lines starting with 1-6 '#'
if (bol < n && s[bol] == '#') {
int j = bol;
while (j < n && s[j] == '#')
++j; // hashes
// include following space and text as Keyword to stand out
push_span(out, bol, n, TokenKind::Keyword);
return state;
}
// Process inline: emphasis and code spans
int i = 0;
while (i < n) {
char c = s[i];
if (c == '`') {
int j = i + 1;
while (j < n && s[j] != '`')
++j;
if (j < n)
++j;
push_span(out, i, j, TokenKind::String);
i = j;
continue;
}
if (c == '*' || c == '_') {
// bold/italic markers: treat the marker and until next same marker as Type to highlight
char m = c;
int j = i + 1;
while (j < n && s[j] != m)
++j;
if (j < n)
++j;
push_span(out, i, j, TokenKind::Type);
i = j;
continue;
}
// links []() minimal: treat [text](url) as Function
if (c == '[') {
int j = i + 1;
while (j < n && s[j] != ']')
++j;
if (j < n)
++j; // include ]
if (j < n && s[j] == '(') {
while (j < n && s[j] != ')')
++j;
if (j < n)
++j;
}
push_span(out, i, j, TokenKind::Function);
i = j;
continue;
}
// whitespace
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push_span(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// fallback: default single char
push_span(out, i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

View File

@@ -0,0 +1,14 @@
// MarkdownHighlighter.h - simple Markdown highlighter
#pragma once
#include "LanguageHighlighter.h"
namespace kte {
class MarkdownHighlighter final : public StatefulHighlighter {
public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

17
syntax/NullHighlighter.cc Normal file
View File

@@ -0,0 +1,17 @@
#include "NullHighlighter.h"
#include "../Buffer.h"
namespace kte {
void
NullHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
if (n <= 0)
return;
out.push_back({0, n, TokenKind::Default});
}
} // namespace kte

11
syntax/NullHighlighter.h Normal file
View File

@@ -0,0 +1,11 @@
// NullHighlighter.h - default highlighter that emits a single Default span per line
#pragma once
#include "LanguageHighlighter.h"
namespace kte {
class NullHighlighter final : public LanguageHighlighter {
public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

172
syntax/PythonHighlighter.cc Normal file
View File

@@ -0,0 +1,172 @@
#include "PythonHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
PythonHighlighter::PythonHighlighter()
{
const char *kw[] = {
"and", "as", "assert", "break", "class", "continue", "def", "del", "elif", "else", "except", "False",
"finally", "for", "from", "global", "if", "import", "in", "is", "lambda", "None", "nonlocal", "not",
"or", "pass", "raise", "return", "True", "try", "while", "with", "yield"
};
for (auto s: kw)
kws_.insert(s);
}
void
PythonHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
LineState st;
(void) HighlightLineStateful(buf, row, st, out);
}
StatefulHighlighter::LineState
PythonHighlighter::HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const
{
StatefulHighlighter::LineState state = prev;
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return state;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
// Triple-quoted string continuation uses in_raw_string with raw_delim either "'''" or "\"\"\""
if (state.in_raw_string && (state.raw_delim == "'''" || state.raw_delim == "\"\"\"")) {
auto pos = s.find(state.raw_delim);
if (pos == std::string::npos) {
push(out, 0, n, TokenKind::String);
return state; // still inside
} else {
int end = static_cast<int>(pos + static_cast<int>(state.raw_delim.size()));
push(out, 0, end, TokenKind::String);
// remainder processed normally
s = s.substr(end);
n = static_cast<int>(s.size());
state.in_raw_string = false;
state.raw_delim.clear();
// Continue parsing remainder as a separate small loop
int base = end;
// original offset, but we already emitted to 'out' with base=0; following spans should be from 'end'
// For simplicity, mark rest as Default
if (n > 0)
push(out, base, base + n, TokenKind::Default);
return state;
}
}
int i = 0;
// Detect comment start '#', ignoring inside strings
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '#') {
push(out, i, n, TokenKind::Comment);
break;
}
// Strings: triple quotes and single-line
if (c == '"' || c == '\'') {
char q = c;
// triple?
if (i + 2 < n && s[i + 1] == q && s[i + 2] == q) {
std::string delim(3, q);
int j = i + 3; // search for closing triple
auto pos = s.find(delim, static_cast<std::size_t>(j));
if (pos == std::string::npos) {
push(out, i, n, TokenKind::String);
state.in_raw_string = true;
state.raw_delim = delim;
return state;
} else {
int end = static_cast<int>(pos + 3);
push(out, i, end, TokenKind::String);
i = end;
continue;
}
} else {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == q)
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ':' || c == ',' || c == '(' || c == ')' || c == '[' || c == ']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
return state;
}
} // namespace kte

View File

@@ -0,0 +1,20 @@
// PythonHighlighter.h - simple Python highlighter with triple-quote state
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class PythonHighlighter final : public StatefulHighlighter {
public:
PythonHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
LineState HighlightLineStateful(const Buffer &buf, int row, const LineState &prev,
std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
};
} // namespace kte

145
syntax/RustHighlighter.cc Normal file
View File

@@ -0,0 +1,145 @@
#include "RustHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_';
}
RustHighlighter::RustHighlighter()
{
const char *kw[] = {
"as", "break", "const", "continue", "crate", "else", "enum", "extern", "false", "fn", "for", "if",
"impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub", "ref", "return", "self", "Self",
"static", "struct", "super", "trait", "true", "type", "unsafe", "use", "where", "while", "dyn", "async",
"await", "try"
};
for (auto s: kw)
kws_.insert(s);
const char *tp[] = {
"u8", "u16", "u32", "u64", "u128", "usize", "i8", "i16", "i32", "i64", "i128", "isize", "f32", "f64",
"bool", "char", "str"
};
for (auto s: tp)
types_.insert(s);
}
void
RustHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '/' && i + 1 < n && s[i + 1] == '/') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
if (c == '"') {
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
TokenKind k = TokenKind::Identifier;
if (kws_.count(id))
k = TokenKind::Keyword;
else if (types_.count(id))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ';' || c == ',' || c == '(' || c == ')' || c == '{' || c == '}' || c == '[' || c ==
']')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/RustHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// RustHighlighter.h - simple Rust highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class RustHighlighter final : public LanguageHighlighter {
public:
RustHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

105
syntax/ShellHighlighter.cc Normal file
View File

@@ -0,0 +1,105 @@
#include "ShellHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
void
ShellHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
// if first non-space is '#', whole line is comment
int bol = 0;
while (bol < n && (s[bol] == ' ' || s[bol] == '\t'))
++bol;
if (bol < n && s[bol] == '#') {
push(out, bol, n, TokenKind::Comment);
if (bol > 0)
push(out, 0, bol, TokenKind::Whitespace);
return;
}
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
if (c == '#') {
push(out, i, n, TokenKind::Comment);
break;
}
if (c == '\'' || c == '"') {
char q = c;
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (q == '"') {
if (esc) {
esc = false;
continue;
}
if (d == '\\') {
esc = true;
continue;
}
if (d == '"')
break;
} else {
if (d == '\'')
break;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
// simple keywords
if (std::isalpha(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '_'))
++j;
std::string id = s.substr(i, j - i);
static const char *kws[] = {
"if", "then", "fi", "for", "in", "do", "done", "case", "esac", "while", "function",
"elif", "else"
};
bool kw = false;
for (auto k: kws)
if (id == k) {
kw = true;
break;
}
push(out, i, j, kw ? TokenKind::Keyword : TokenKind::Identifier);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == '(' || c == ')' || c == '{' || c == '}' || c == ',' || c == ';')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

11
syntax/ShellHighlighter.h Normal file
View File

@@ -0,0 +1,11 @@
// ShellHighlighter.h - simple POSIX shell highlighter
#pragma once
#include "LanguageHighlighter.h"
namespace kte {
class ShellHighlighter final : public LanguageHighlighter {
public:
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
};
} // namespace kte

156
syntax/SqlHighlighter.cc Normal file
View File

@@ -0,0 +1,156 @@
#include "SqlHighlighter.h"
#include "../Buffer.h"
#include <cctype>
namespace kte {
static void
push(std::vector<HighlightSpan> &out, int a, int b, TokenKind k)
{
if (b > a)
out.push_back({a, b, k});
}
static bool
is_ident_start(char c)
{
return std::isalpha(static_cast<unsigned char>(c)) || c == '_';
}
static bool
is_ident_char(char c)
{
return std::isalnum(static_cast<unsigned char>(c)) || c == '_' || c == '$';
}
SqlHighlighter::SqlHighlighter()
{
const char *kw[] = {
"select", "insert", "update", "delete", "from", "where", "group", "by", "order", "limit",
"offset", "values", "into", "create", "table", "index", "unique", "on", "as", "and", "or",
"not", "null", "is", "primary", "key", "constraint", "foreign", "references", "drop", "alter",
"add", "column", "rename", "to", "if", "exists", "join", "left", "right", "inner", "outer",
"cross", "using", "set", "distinct", "having", "union", "all", "case", "when", "then", "else",
"end", "pragma", "transaction", "begin", "commit", "rollback", "replace"
};
for (auto s: kw)
kws_.insert(s);
const char *types[] = {"integer", "real", "text", "blob", "numeric", "boolean", "date", "datetime"};
for (auto s: types)
types_.insert(s);
}
void
SqlHighlighter::HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const
{
const auto &rows = buf.Rows();
if (row < 0 || static_cast<std::size_t>(row) >= rows.size())
return;
std::string s = static_cast<std::string>(rows[static_cast<std::size_t>(row)]);
int n = static_cast<int>(s.size());
int i = 0;
while (i < n) {
char c = s[i];
if (c == ' ' || c == '\t') {
int j = i + 1;
while (j < n && (s[j] == ' ' || s[j] == '\t'))
++j;
push(out, i, j, TokenKind::Whitespace);
i = j;
continue;
}
// line comments: -- ...
if (c == '-' && i + 1 < n && s[i + 1] == '-') {
push(out, i, n, TokenKind::Comment);
break;
}
// simple block comment on same line: /* ... */
if (c == '/' && i + 1 < n && s[i + 1] == '*') {
int j = i + 2;
bool closed = false;
while (j + 1 <= n) {
if (j + 1 < n && s[j] == '*' && s[j + 1] == '/') {
j += 2;
closed = true;
break;
}
++j;
}
if (!closed) {
push(out, i, n, TokenKind::Comment);
break;
} else {
push(out, i, j, TokenKind::Comment);
i = j;
continue;
}
}
// strings: '...' or "..."
if (c == '\'' || c == '"') {
char q = c;
int j = i + 1;
bool esc = false;
while (j < n) {
char d = s[j++];
if (d == q) {
// Handle doubled quote escaping for SQL single quotes
if (q == '\'' && j < n && s[j] == '\'') {
++j;
continue;
}
break;
}
if (d == '\\') {
esc = !esc;
} else {
esc = false;
}
}
push(out, i, j, TokenKind::String);
i = j;
continue;
}
if (std::isdigit(static_cast<unsigned char>(c))) {
int j = i + 1;
while (j < n && (std::isalnum(static_cast<unsigned char>(s[j])) || s[j] == '.' || s[j] == '_'))
++j;
push(out, i, j, TokenKind::Number);
i = j;
continue;
}
if (is_ident_start(c)) {
int j = i + 1;
while (j < n && is_ident_char(s[j]))
++j;
std::string id = s.substr(i, j - i);
std::string lower;
lower.reserve(id.size());
for (char ch: id)
lower.push_back(static_cast<char>(std::tolower(static_cast<unsigned char>(ch))));
TokenKind k = TokenKind::Identifier;
if (kws_.count(lower))
k = TokenKind::Keyword;
else if (types_.count(lower))
k = TokenKind::Type;
push(out, i, j, k);
i = j;
continue;
}
if (std::ispunct(static_cast<unsigned char>(c))) {
TokenKind k = TokenKind::Operator;
if (c == ',' || c == ';' || c == '(' || c == ')')
k = TokenKind::Punctuation;
push(out, i, i + 1, k);
++i;
continue;
}
push(out, i, i + 1, TokenKind::Default);
++i;
}
}
} // namespace kte

18
syntax/SqlHighlighter.h Normal file
View File

@@ -0,0 +1,18 @@
// SqlHighlighter.h - simple SQL/SQLite highlighter
#pragma once
#include "LanguageHighlighter.h"
#include <unordered_set>
namespace kte {
class SqlHighlighter final : public LanguageHighlighter {
public:
SqlHighlighter();
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
std::unordered_set<std::string> kws_;
std::unordered_set<std::string> types_;
};
} // namespace kte

View File

@@ -0,0 +1,51 @@
#include "TreeSitterHighlighter.h"
#ifdef KTE_ENABLE_TREESITTER
#include "Buffer.h"
#include <utility>
namespace kte {
TreeSitterHighlighter::TreeSitterHighlighter(const TSLanguage *lang, std::string filetype)
: language_(lang), filetype_(std::move(filetype)) {}
TreeSitterHighlighter::~TreeSitterHighlighter()
{
disposeParser();
}
void
TreeSitterHighlighter::ensureParsed(const Buffer & /*buf*/) const
{
// Intentionally a stub to avoid pulling the Tree-sitter API and library by default.
// In future, when linking against tree-sitter, initialize parser_, set language_,
// and build tree_ from the buffer contents.
}
void
TreeSitterHighlighter::disposeParser() const
{
// Stub; nothing to dispose when not actually creating parser/tree
}
void
TreeSitterHighlighter::HighlightLine(const Buffer &/*buf*/, int /*row*/, std::vector<HighlightSpan> &/*out*/) const
{
// For now, no-op. When tree-sitter is wired, map nodes to TokenKind spans per line.
}
std::unique_ptr<LanguageHighlighter>
CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)())
{
const auto *lang = reinterpret_cast<const TSLanguage *>(get_lang ? get_lang() : nullptr);
return std::make_unique < TreeSitterHighlighter > (lang, filetype ? std::string(filetype) : std::string());
}
} // namespace kte
#endif // KTE_ENABLE_TREESITTER

View File

@@ -0,0 +1,48 @@
// TreeSitterHighlighter.h - optional adapter for Tree-sitter (behind KTE_ENABLE_TREESITTER)
#pragma once
#ifdef KTE_ENABLE_TREESITTER
#include <memory>
#include <string>
#include <vector>
#include "LanguageHighlighter.h"
// Forward-declare Tree-sitter C API to avoid hard coupling in headers if includes are not present
extern "C" {
struct TSLanguage;
struct TSParser;
struct TSTree;
}
namespace kte {
// A minimal adapter that uses Tree-sitter to parse the whole buffer and then, for now,
// does very limited token classification. This acts as a scaffold for future richer
// queries. If no queries are provided, it currently produces no spans (safe fallback).
class TreeSitterHighlighter : public LanguageHighlighter {
public:
explicit TreeSitterHighlighter(const TSLanguage *lang, std::string filetype);
~TreeSitterHighlighter() override;
void HighlightLine(const Buffer &buf, int row, std::vector<HighlightSpan> &out) const override;
private:
const TSLanguage *language_{nullptr};
std::string filetype_;
// Lazy parser to avoid startup cost; mutable to allow creation in const method
mutable TSParser *parser_{nullptr};
mutable TSTree *tree_{nullptr};
void ensureParsed(const Buffer &buf) const;
void disposeParser() const;
};
// Factory used by HighlighterRegistry when registering via RegisterTreeSitter.
std::unique_ptr<LanguageHighlighter> CreateTreeSitterHighlighter(const char *filetype,
const void * (*get_lang)());
} // namespace kte
#endif // KTE_ENABLE_TREESITTER

177
themes/EInk.h Normal file
View File

@@ -0,0 +1,177 @@
// themes/EInk.h — Monochrome e-ink inspired ImGui themes (header-only)
#pragma once
#include "imgui.h"
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyEInkImGuiTheme()
{
// E-Ink grayscale palette (light background)
const ImVec4 paper = RGBA(0xF2F2EE); // light paper
const ImVec4 bg1 = RGBA(0xE6E6E2);
const ImVec4 bg2 = RGBA(0xDADAD5);
const ImVec4 bg3 = RGBA(0xCFCFCA);
const ImVec4 ink = RGBA(0x111111); // primary text (near black)
const ImVec4 dim = RGBA(0x666666); // disabled text
const ImVec4 border = RGBA(0xB8B8B3);
const ImVec4 accent = RGBA(0x222222); // controls/active
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
colors[ImGuiCol_WindowBg] = paper;
colors[ImGuiCol_ChildBg] = paper;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = paper;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = accent;
colors[ImGuiCol_SliderGrab] = accent;
colors[ImGuiCol_SliderGrabActive] = ink;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg2;
colors[ImGuiCol_SeparatorActive] = accent;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = ink;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = accent;
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = accent;
colors[ImGuiCol_PlotLinesHovered] = ink;
colors[ImGuiCol_PlotHistogram] = accent;
colors[ImGuiCol_PlotHistogramHovered] = ink;
}
static inline void
ApplyEInkDarkImGuiTheme()
{
// E-Ink dark variant (dark background, light ink)
const ImVec4 paper = RGBA(0x1A1A1A);
const ImVec4 bg1 = RGBA(0x222222);
const ImVec4 bg2 = RGBA(0x2B2B2B);
const ImVec4 bg3 = RGBA(0x343434);
const ImVec4 ink = RGBA(0xEDEDEA);
const ImVec4 dim = RGBA(0xB5B5B3);
const ImVec4 border = RGBA(0x444444);
const ImVec4 accent = RGBA(0xDDDDDD);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = ImVec4(dim.x, dim.y, dim.z, 1.0f);
colors[ImGuiCol_WindowBg] = paper;
colors[ImGuiCol_ChildBg] = paper;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = paper;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = accent;
colors[ImGuiCol_SliderGrab] = accent;
colors[ImGuiCol_SliderGrabActive] = ink;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg2;
colors[ImGuiCol_SeparatorActive] = accent;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(accent.x, accent.y, accent.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = ink;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(accent.x, accent.y, accent.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = accent;
colors[ImGuiCol_NavHighlight] = accent;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = accent;
colors[ImGuiCol_PlotLinesHovered] = ink;
colors[ImGuiCol_PlotHistogram] = accent;
colors[ImGuiCol_PlotHistogramHovered] = ink;
}

204
themes/Gruvbox.h Normal file
View File

@@ -0,0 +1,204 @@
// themes/Gruvbox.h — Gruvbox Dark/Light (medium) ImGui themes (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyGruvboxDarkMediumTheme()
{
// Gruvbox (dark, medium) palette
const ImVec4 bg0 = RGBA(0x282828); // dark0
const ImVec4 bg1 = RGBA(0x3C3836); // dark1
const ImVec4 bg2 = RGBA(0x504945); // dark2
const ImVec4 bg3 = RGBA(0x665C54); // dark3
const ImVec4 fg1 = RGBA(0xEBDBB2); // light1
const ImVec4 fg0 = RGBA(0xFBF1C7); // light0
// accent colors
const ImVec4 yellow = RGBA(0xFABD2F);
const ImVec4 blue = RGBA(0x83A598);
const ImVec4 aqua = RGBA(0x8EC07C);
const ImVec4 orange = RGBA(0xFE8019);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = fg1;
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
colors[ImGuiCol_WindowBg] = bg0;
colors[ImGuiCol_ChildBg] = bg0;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = bg2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = bg0;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = aqua;
colors[ImGuiCol_SliderGrab] = aqua;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = bg2;
colors[ImGuiCol_SeparatorHovered] = bg1;
colors[ImGuiCol_SeparatorActive] = blue;
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = blue;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = orange;
colors[ImGuiCol_NavHighlight] = orange;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = aqua;
colors[ImGuiCol_PlotLinesHovered] = blue;
colors[ImGuiCol_PlotHistogram] = yellow;
colors[ImGuiCol_PlotHistogramHovered] = orange;
}
static inline void
ApplyGruvboxLightMediumTheme()
{
// Gruvbox (light, medium) palette
const ImVec4 bg0 = RGBA(0xFBF1C7); // light0
const ImVec4 bg1 = RGBA(0xEBDBB2); // light1
const ImVec4 bg2 = RGBA(0xD5C4A1); // light2
const ImVec4 bg3 = RGBA(0xBDAE93); // light3
const ImVec4 fg1 = RGBA(0x3C3836); // dark1
const ImVec4 fg0 = RGBA(0x282828); // dark0
// accents
const ImVec4 yellow = RGBA(0xB57614);
const ImVec4 blue = RGBA(0x076678);
const ImVec4 aqua = RGBA(0x427B58);
const ImVec4 orange = RGBA(0xAF3A03);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = fg1;
colors[ImGuiCol_TextDisabled] = ImVec4(fg1.x, fg1.y, fg1.z, 0.55f);
colors[ImGuiCol_WindowBg] = bg0;
colors[ImGuiCol_ChildBg] = bg0;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = bg2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = bg0;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = bg1;
colors[ImGuiCol_CheckMark] = aqua;
colors[ImGuiCol_SliderGrab] = aqua;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg3;
colors[ImGuiCol_ButtonHovered] = bg2;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg3;
colors[ImGuiCol_HeaderHovered] = bg2;
colors[ImGuiCol_HeaderActive] = bg2;
colors[ImGuiCol_Separator] = bg2;
colors[ImGuiCol_SeparatorHovered] = bg1;
colors[ImGuiCol_SeparatorActive] = blue;
colors[ImGuiCol_ResizeGrip] = ImVec4(fg0.x, fg0.y, fg0.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(aqua.x, aqua.y, aqua.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = blue;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = bg1;
colors[ImGuiCol_TableBorderLight] = ImVec4(bg1.x, bg1.y, bg1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(orange.x, orange.y, orange.z, 0.30f);
colors[ImGuiCol_DragDropTarget] = orange;
colors[ImGuiCol_NavHighlight] = orange;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(fg0.x, fg0.y, fg0.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.45f);
colors[ImGuiCol_PlotLines] = aqua;
colors[ImGuiCol_PlotLinesHovered] = blue;
colors[ImGuiCol_PlotHistogram] = yellow;
colors[ImGuiCol_PlotHistogramHovered] = orange;
}

111
themes/Nord.h Normal file
View File

@@ -0,0 +1,111 @@
// themes/Nord.h — Nord-inspired ImGui theme (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyNordImGuiTheme()
{
// Nord palette
const ImVec4 nord0 = RGBA(0x2E3440); // darkest bg
const ImVec4 nord1 = RGBA(0x3B4252);
const ImVec4 nord2 = RGBA(0x434C5E);
const ImVec4 nord3 = RGBA(0x4C566A);
const ImVec4 nord4 = RGBA(0xD8DEE9);
const ImVec4 nord6 = RGBA(0xECEFF4); // lightest
const ImVec4 nord8 = RGBA(0x88C0D0); // cyan
const ImVec4 nord9 = RGBA(0x81A1C1); // blue
const ImVec4 nord10 = RGBA(0x5E81AC); // blue dark
const ImVec4 nord12 = RGBA(0xD08770); // orange
const ImVec4 nord13 = RGBA(0xEBCB8B); // yellow
ImGuiStyle &style = ImGui::GetStyle();
// Base style tweaks to suit Nord aesthetics
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 4.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 4.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 4.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = nord4; // primary text
colors[ImGuiCol_TextDisabled] = ImVec4(nord4.x, nord4.y, nord4.z, 0.55f);
colors[ImGuiCol_WindowBg] = nord0;
colors[ImGuiCol_ChildBg] = nord0;
colors[ImGuiCol_PopupBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.98f);
colors[ImGuiCol_Border] = nord2;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = nord2;
colors[ImGuiCol_FrameBgHovered] = nord3;
colors[ImGuiCol_FrameBgActive] = nord1;
colors[ImGuiCol_TitleBg] = nord1;
colors[ImGuiCol_TitleBgActive] = nord2;
colors[ImGuiCol_TitleBgCollapsed] = nord1;
colors[ImGuiCol_MenuBarBg] = nord1;
colors[ImGuiCol_ScrollbarBg] = nord10;
colors[ImGuiCol_ScrollbarGrab] = nord3;
colors[ImGuiCol_ScrollbarGrabHovered] = nord2;
colors[ImGuiCol_ScrollbarGrabActive] = nord1;
colors[ImGuiCol_CheckMark] = nord8;
colors[ImGuiCol_SliderGrab] = nord8;
colors[ImGuiCol_SliderGrabActive] = nord9;
colors[ImGuiCol_Button] = nord3;
colors[ImGuiCol_ButtonHovered] = nord2;
colors[ImGuiCol_ButtonActive] = nord1;
colors[ImGuiCol_Header] = nord3;
colors[ImGuiCol_HeaderHovered] = nord10;
colors[ImGuiCol_HeaderActive] = nord10;
colors[ImGuiCol_Separator] = nord2;
colors[ImGuiCol_SeparatorHovered] = nord10;
colors[ImGuiCol_SeparatorActive] = nord9;
colors[ImGuiCol_ResizeGrip] = ImVec4(nord6.x, nord6.y, nord6.z, 0.12f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(nord8.x, nord8.y, nord8.z, 0.67f);
colors[ImGuiCol_ResizeGripActive] = nord9;
colors[ImGuiCol_Tab] = nord2;
colors[ImGuiCol_TabHovered] = nord10;
colors[ImGuiCol_TabActive] = nord3;
colors[ImGuiCol_TabUnfocused] = nord2;
colors[ImGuiCol_TabUnfocusedActive] = nord3;
// Docking colors omitted for compatibility
colors[ImGuiCol_TableHeaderBg] = nord2;
colors[ImGuiCol_TableBorderStrong] = nord1;
colors[ImGuiCol_TableBorderLight] = ImVec4(nord1.x, nord1.y, nord1.z, 0.6f);
colors[ImGuiCol_TableRowBg] = ImVec4(nord1.x, nord1.y, nord1.z, 0.2f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(nord1.x, nord1.y, nord1.z, 0.35f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(nord8.x, nord8.y, nord8.z, 0.35f);
colors[ImGuiCol_DragDropTarget] = nord13;
colors[ImGuiCol_NavHighlight] = nord9;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(nord6.x, nord6.y, nord6.z, 0.7f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(nord0.x, nord0.y, nord0.z, 0.6f);
// Plots
colors[ImGuiCol_PlotLines] = nord8;
colors[ImGuiCol_PlotLinesHovered] = nord9;
colors[ImGuiCol_PlotHistogram] = nord13;
colors[ImGuiCol_PlotHistogramHovered] = nord12;
}

89
themes/Plan9.h Normal file
View File

@@ -0,0 +1,89 @@
// themes/Plan9.h — Plan 9 acme-inspired ImGui theme (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplyPlan9Theme()
{
// Acme-like colors
const ImVec4 paper = RGBA(0xFFFFE8); // pale yellow paper
const ImVec4 pane = RGBA(0xFFF4C1); // slightly deeper for frames
const ImVec4 ink = RGBA(0x000000); // black text
constexpr auto dim = ImVec4(0, 0, 0, 0.60f);
const ImVec4 border = RGBA(0x000000); // 1px black
const ImVec4 blue = RGBA(0x0064FF); // acme-ish blue accents
const ImVec4 blueH = RGBA(0x4C8DFF); // hover/active
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(6.0f, 6.0f);
style.FramePadding = ImVec2(5.0f, 3.0f);
style.CellPadding = ImVec2(5.0f, 3.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = ink;
c[ImGuiCol_TextDisabled] = dim;
c[ImGuiCol_WindowBg] = paper;
c[ImGuiCol_ChildBg] = paper;
c[ImGuiCol_PopupBg] = ImVec4(pane.x, pane.y, pane.z, 0.98f);
c[ImGuiCol_Border] = border;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = pane;
c[ImGuiCol_FrameBgHovered] = RGBA(0xFFEBA0);
c[ImGuiCol_FrameBgActive] = RGBA(0xFFE387);
c[ImGuiCol_TitleBg] = pane;
c[ImGuiCol_TitleBgActive] = RGBA(0xFFE8A6);
c[ImGuiCol_TitleBgCollapsed] = pane;
c[ImGuiCol_MenuBarBg] = pane;
c[ImGuiCol_ScrollbarBg] = paper;
c[ImGuiCol_ScrollbarGrab] = RGBA(0xEADFA5);
c[ImGuiCol_ScrollbarGrabHovered] = RGBA(0xE2D37F);
c[ImGuiCol_ScrollbarGrabActive] = RGBA(0xD8C757);
c[ImGuiCol_CheckMark] = blue;
c[ImGuiCol_SliderGrab] = blue;
c[ImGuiCol_SliderGrabActive] = blueH;
c[ImGuiCol_Button] = RGBA(0xFFF1B0);
c[ImGuiCol_ButtonHovered] = RGBA(0xFFE892);
c[ImGuiCol_ButtonActive] = RGBA(0xFFE072);
c[ImGuiCol_Header] = RGBA(0xFFF1B0);
c[ImGuiCol_HeaderHovered] = RGBA(0xFFE892);
c[ImGuiCol_HeaderActive] = RGBA(0xFFE072);
c[ImGuiCol_Separator] = border;
c[ImGuiCol_SeparatorHovered] = blue;
c[ImGuiCol_SeparatorActive] = blueH;
c[ImGuiCol_ResizeGrip] = ImVec4(0, 0, 0, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(blue.x, blue.y, blue.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blueH;
c[ImGuiCol_Tab] = RGBA(0xFFE8A6);
c[ImGuiCol_TabHovered] = RGBA(0xFFE072);
c[ImGuiCol_TabActive] = RGBA(0xFFD859);
c[ImGuiCol_TabUnfocused] = RGBA(0xFFE8A6);
c[ImGuiCol_TabUnfocusedActive] = RGBA(0xFFD859);
c[ImGuiCol_TableHeaderBg] = RGBA(0xFFE8A6);
c[ImGuiCol_TableBorderStrong] = border;
c[ImGuiCol_TableBorderLight] = ImVec4(0, 0, 0, 0.35f);
c[ImGuiCol_TableRowBg] = ImVec4(0, 0, 0, 0.04f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(0, 0, 0, 0.08f);
c[ImGuiCol_TextSelectedBg] = ImVec4(blueH.x, blueH.y, blueH.z, 0.35f);
c[ImGuiCol_DragDropTarget] = blue;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(0, 0, 0, 0.20f);
c[ImGuiCol_PlotLines] = blue;
c[ImGuiCol_PlotLinesHovered] = blueH;
c[ImGuiCol_PlotHistogram] = blue;
c[ImGuiCol_PlotHistogramHovered] = blueH;
}

184
themes/Solarized.h Normal file
View File

@@ -0,0 +1,184 @@
// themes/Solarized.h — Solarized Dark/Light ImGui themes (header-only)
#pragma once
#include "ThemeHelpers.h"
// Expects to be included from GUITheme.h after <imgui.h> and RGBA() helper
static void
ApplySolarizedDarkTheme()
{
// Base colors from Ethan Schoonover Solarized
const ImVec4 base03 = RGBA(0x002b36);
const ImVec4 base02 = RGBA(0x073642);
const ImVec4 base01 = RGBA(0x586e75);
const ImVec4 base00 = RGBA(0x657b83);
const ImVec4 base0 = RGBA(0x839496);
const ImVec4 base1 = RGBA(0x93a1a1);
const ImVec4 base2 = RGBA(0xeee8d5);
const ImVec4 yellow = RGBA(0xb58900);
const ImVec4 orange = RGBA(0xcb4b16);
const ImVec4 blue = RGBA(0x268bd2);
const ImVec4 cyan = RGBA(0x2aa198);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 3.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 3.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = base0;
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
c[ImGuiCol_WindowBg] = base03;
c[ImGuiCol_ChildBg] = base03;
c[ImGuiCol_PopupBg] = ImVec4(base02.x, base02.y, base02.z, 0.98f);
c[ImGuiCol_Border] = base02;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = base02;
c[ImGuiCol_FrameBgHovered] = base01;
c[ImGuiCol_FrameBgActive] = base00;
c[ImGuiCol_TitleBg] = base02;
c[ImGuiCol_TitleBgActive] = base01;
c[ImGuiCol_TitleBgCollapsed] = base02;
c[ImGuiCol_MenuBarBg] = base02;
c[ImGuiCol_ScrollbarBg] = base02;
c[ImGuiCol_ScrollbarGrab] = base01;
c[ImGuiCol_ScrollbarGrabHovered] = base00;
c[ImGuiCol_ScrollbarGrabActive] = blue;
c[ImGuiCol_CheckMark] = cyan;
c[ImGuiCol_SliderGrab] = cyan;
c[ImGuiCol_SliderGrabActive] = blue;
c[ImGuiCol_Button] = base01;
c[ImGuiCol_ButtonHovered] = base00;
c[ImGuiCol_ButtonActive] = blue;
c[ImGuiCol_Header] = base01;
c[ImGuiCol_HeaderHovered] = base00;
c[ImGuiCol_HeaderActive] = base00;
c[ImGuiCol_Separator] = base01;
c[ImGuiCol_SeparatorHovered] = base00;
c[ImGuiCol_SeparatorActive] = blue;
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blue;
c[ImGuiCol_Tab] = base01;
c[ImGuiCol_TabHovered] = base00;
c[ImGuiCol_TabActive] = base02;
c[ImGuiCol_TabUnfocused] = base01;
c[ImGuiCol_TabUnfocusedActive] = base02;
c[ImGuiCol_TableHeaderBg] = base01;
c[ImGuiCol_TableBorderStrong] = base00;
c[ImGuiCol_TableBorderLight] = ImVec4(base00.x, base00.y, base00.z, 0.6f);
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
c[ImGuiCol_DragDropTarget] = yellow;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_PlotLines] = cyan;
c[ImGuiCol_PlotLinesHovered] = blue;
c[ImGuiCol_PlotHistogram] = yellow;
c[ImGuiCol_PlotHistogramHovered] = orange;
}
static inline void
ApplySolarizedLightTheme()
{
// Base colors from Ethan Schoonover Solarized (light variant)
const ImVec4 base3 = RGBA(0xfdf6e3);
const ImVec4 base2 = RGBA(0xeee8d5);
const ImVec4 base1 = RGBA(0x93a1a1);
const ImVec4 base0 = RGBA(0x839496);
const ImVec4 base00 = RGBA(0x657b83);
const ImVec4 base01 = RGBA(0x586e75);
const ImVec4 base02 = RGBA(0x073642);
const ImVec4 base03 = RGBA(0x002b36);
const ImVec4 yellow = RGBA(0xb58900);
const ImVec4 orange = RGBA(0xcb4b16);
const ImVec4 blue = RGBA(0x268bd2);
const ImVec4 cyan = RGBA(0x2aa198);
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 14.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 3.0f;
style.FrameRounding = 3.0f;
style.PopupRounding = 3.0f;
style.GrabRounding = 3.0f;
style.TabRounding = 3.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 1.0f;
ImVec4 *c = style.Colors;
c[ImGuiCol_Text] = base00;
c[ImGuiCol_TextDisabled] = ImVec4(base01.x, base01.y, base01.z, 1.0f);
c[ImGuiCol_WindowBg] = base3;
c[ImGuiCol_ChildBg] = base3;
c[ImGuiCol_PopupBg] = ImVec4(base2.x, base2.y, base2.z, 0.98f);
c[ImGuiCol_Border] = base1;
c[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
c[ImGuiCol_FrameBg] = base2;
c[ImGuiCol_FrameBgHovered] = base1;
c[ImGuiCol_FrameBgActive] = base0;
c[ImGuiCol_TitleBg] = base2;
c[ImGuiCol_TitleBgActive] = base1;
c[ImGuiCol_TitleBgCollapsed] = base2;
c[ImGuiCol_MenuBarBg] = base2;
c[ImGuiCol_ScrollbarBg] = base2;
c[ImGuiCol_ScrollbarGrab] = base1;
c[ImGuiCol_ScrollbarGrabHovered] = base0;
c[ImGuiCol_ScrollbarGrabActive] = blue;
c[ImGuiCol_CheckMark] = cyan;
c[ImGuiCol_SliderGrab] = cyan;
c[ImGuiCol_SliderGrabActive] = blue;
c[ImGuiCol_Button] = base1;
c[ImGuiCol_ButtonHovered] = base0;
c[ImGuiCol_ButtonActive] = blue;
c[ImGuiCol_Header] = base1;
c[ImGuiCol_HeaderHovered] = base0;
c[ImGuiCol_HeaderActive] = base0;
c[ImGuiCol_Separator] = base1;
c[ImGuiCol_SeparatorHovered] = base0;
c[ImGuiCol_SeparatorActive] = blue;
c[ImGuiCol_ResizeGrip] = ImVec4(base1.x, base1.y, base1.z, 0.12f);
c[ImGuiCol_ResizeGripHovered] = ImVec4(cyan.x, cyan.y, cyan.z, 0.67f);
c[ImGuiCol_ResizeGripActive] = blue;
c[ImGuiCol_Tab] = base1;
c[ImGuiCol_TabHovered] = base0;
c[ImGuiCol_TabActive] = base2;
c[ImGuiCol_TabUnfocused] = base1;
c[ImGuiCol_TabUnfocusedActive] = base2;
c[ImGuiCol_TableHeaderBg] = base1;
c[ImGuiCol_TableBorderStrong] = base0;
c[ImGuiCol_TableBorderLight] = ImVec4(base0.x, base0.y, base0.z, 0.6f);
c[ImGuiCol_TableRowBg] = ImVec4(base02.x, base02.y, base02.z, 0.2f);
c[ImGuiCol_TableRowBgAlt] = ImVec4(base02.x, base02.y, base02.z, 0.35f);
c[ImGuiCol_TextSelectedBg] = ImVec4(cyan.x, cyan.y, cyan.z, 0.30f);
c[ImGuiCol_DragDropTarget] = yellow;
c[ImGuiCol_NavHighlight] = blue;
c[ImGuiCol_NavWindowingHighlight] = ImVec4(base2.x, base2.y, base2.z, 0.70f);
c[ImGuiCol_NavWindowingDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_ModalWindowDimBg] = ImVec4(base03.x, base03.y, base03.z, 0.60f);
c[ImGuiCol_PlotLines] = cyan;
c[ImGuiCol_PlotLinesHovered] = blue;
c[ImGuiCol_PlotHistogram] = yellow;
c[ImGuiCol_PlotHistogramHovered] = orange;
}

17
themes/ThemeHelpers.h Normal file
View File

@@ -0,0 +1,17 @@
#ifndef KTE_THEMEHELPERS_H
#define KTE_THEMEHELPERS_H
#include "imgui.h"
// Small helper to convert packed RGB (0xRRGGBB) + optional alpha to ImVec4
static ImVec4
RGBA(const unsigned int rgb, float a = 1.0f)
{
const float r = static_cast<float>(rgb >> 16 & 0xFF) / 255.0f;
const float g = static_cast<float>(rgb >> 8 & 0xFF) / 255.0f;
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
return {r, g, b, a};
}
#endif //KTE_THEMEHELPERS_H