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.
This commit is contained in:
2025-12-01 15:21:52 -08:00
parent d98785e825
commit 655cc40162
24 changed files with 3234 additions and 1497 deletions

28
.idea/workspace.xml generated
View File

@@ -7,6 +7,7 @@
<option name="/Default/Environment/Hierarchy/GeneratedFilesCacheKey/Timestamp/@EntryValue" value="3" type="long" /> <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/FeatureSuggestion/FeatureSuggestionManager/DisabledSuggesters/=SwitchToGoToActionSuggester/@EntryIndexedValue" value="true" type="bool" />
<option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" /> <option name="/Default/Housekeeping/GlobalSettingsUpgraded/IsUpgraded/@EntryValue" value="true" type="bool" />
<option name="/Default/Housekeeping/LiveTemplatesHousekeeping/HotspotSessionHintIsShown/@EntryValue" value="true" type="bool" />
<option name="/Default/Housekeeping/OptionsDialog/SelectedPageId/@EntryValue" value="CppFormatterOtherPage" type="string" /> <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/Housekeeping/RefactoringsMru/RenameRefactoring/DoSearchForTextInStrings/@EntryValue" value="true" type="bool" />
<option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" /> <option name="/Default/RiderDebugger/RiderRestoreDecompile/RestoreDecompileSetting/@EntryValue" value="false" type="bool" />
@@ -34,7 +35,29 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real"> <list default="true" id="e1fe3ab0-3650-4fca-8664-a247d5dfa457" name="Changes" comment="Add Nord theme for real">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" /> <change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIConfig.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIConfig.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIConfig.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIConfig.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIFrontend.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIFrontend.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUIFrontend.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIInputHandler.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIInputHandler.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUIRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/GUIRenderer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/GUITheme.h" beforeDir="false" afterPath="$PROJECT_DIR$/GUITheme.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/HelpText.cc" beforeDir="false" afterPath="$PROJECT_DIR$/HelpText.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/HelpText.h" beforeDir="false" afterPath="$PROJECT_DIR$/HelpText.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/KKeymap.cc" beforeDir="false" afterPath="$PROJECT_DIR$/KKeymap.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/TerminalRenderer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/TerminalRenderer.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/UndoSystem.cc" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.cc" afterDir="false" />
<change beforePath="$PROJECT_DIR$/UndoSystem.h" beforeDir="false" afterPath="$PROJECT_DIR$/UndoSystem.h" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/kge.1" beforeDir="false" afterPath="$PROJECT_DIR$/docs/kge.1" afterDir="false" />
<change beforePath="$PROJECT_DIR$/docs/kte.1" beforeDir="false" afterPath="$PROJECT_DIR$/docs/kte.1" afterDir="false" />
<change beforePath="$PROJECT_DIR$/test_undo.cc" beforeDir="false" afterPath="$PROJECT_DIR$/test_undo.cc" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -63,12 +86,13 @@
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" /> <setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" />
<setting file="mock:///AIAssistantSnippet.." root0="SKIP_HIGHLIGHTING" /> <setting file="mock:///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>
<component name="OptimizeOnSaveOptions"> <component name="OptimizeOnSaveOptions">
<option name="myRunOnSave" value="true" /> <option name="myRunOnSave" value="true" />
</component> </component>
<component name="ProblemsViewState"> <component name="ProblemsViewState">
<option name="selectedTabId" value="AISelfReview" /> <option name="selectedTabId" value="CurrentFile" />
</component> </component>
<component name="ProjectApplicationVersion"> <component name="ProjectApplicationVersion">
<option name="ide" value="CLion" /> <option name="ide" value="CLion" />
@@ -173,7 +197,7 @@
<workItem from="1764539556448" duration="156000" /> <workItem from="1764539556448" duration="156000" />
<workItem from="1764539725338" duration="1075000" /> <workItem from="1764539725338" duration="1075000" />
<workItem from="1764542392763" duration="3512000" /> <workItem from="1764542392763" duration="3512000" />
<workItem from="1764548345516" duration="39312000" /> <workItem from="1764548345516" duration="50201000" />
</task> </task>
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions."> <task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
<option name="closed" value="true" /> <option name="closed" value="true" />

View File

@@ -262,6 +262,7 @@ public:
return filename_; return filename_;
} }
// Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+" // Set a virtual (non file-backed) display name for this buffer, e.g. "+HELP+"
// This does not mark the buffer as file-backed. // This does not mark the buffer as file-backed.
void SetVirtualName(const std::string &name) void SetVirtualName(const std::string &name)
@@ -282,17 +283,20 @@ public:
return dirty_; return dirty_;
} }
// Read-only flag // Read-only flag
[[nodiscard]] bool IsReadOnly() const [[nodiscard]] bool IsReadOnly() const
{ {
return read_only_; return read_only_;
} }
void SetReadOnly(bool ro) void SetReadOnly(bool ro)
{ {
read_only_ = ro; read_only_ = ro;
} }
void ToggleReadOnly() void ToggleReadOnly()
{ {
read_only_ = !read_only_; read_only_ = !read_only_;

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD 17)
set(KTE_VERSION "1.1.0") set(KTE_VERSION "1.1.1")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.

View File

@@ -4,12 +4,16 @@
#include <regex> #include <regex>
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <cctype>
#include "Command.h" #include "Command.h"
#include "Editor.h" #include "Editor.h"
#include "Buffer.h" #include "Buffer.h"
#include "UndoSystem.h" #include "UndoSystem.h"
#include "HelpText.h" #include "HelpText.h"
#ifdef KTE_BUILD_GUI
#include "GUITheme.h"
#endif
// Keep buffer viewport offsets so that the cursor stays within the visible // Keep buffer viewport offsets so that the cursor stays within the visible
@@ -87,8 +91,10 @@ ensure_at_least_one_line(Buffer &buf)
} }
} }
// Determine if a command mutates the buffer contents (text edits) // 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) { switch (id) {
case CommandId::InsertText: case CommandId::InsertText:
@@ -723,6 +729,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 static bool
cmd_unknown_kcommand(CommandContext &ctx) cmd_unknown_kcommand(CommandContext &ctx)
{ {
@@ -737,8 +758,135 @@ cmd_unknown_kcommand(CommandContext &ctx)
} }
// GUI theme cycling commands (available in GUI build; show message otherwise)
#ifdef KTE_BUILD_GUI
static bool static bool
cmd_find_start(CommandContext &ctx) 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(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -761,7 +909,7 @@ cmd_find_start(CommandContext &ctx)
static bool static bool
cmd_regex_find_start(CommandContext &ctx) cmd_regex_find_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -784,7 +932,7 @@ cmd_regex_find_start(CommandContext &ctx)
static bool static bool
cmd_search_replace_start(CommandContext &ctx) cmd_search_replace_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -808,7 +956,7 @@ cmd_search_replace_start(CommandContext &ctx)
static bool static bool
cmd_regex_replace_start(CommandContext &ctx) cmd_regex_replace_start(const CommandContext &ctx)
{ {
Buffer *buf = ctx.editor.CurrentBuffer(); Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) { if (!buf) {
@@ -832,7 +980,7 @@ cmd_regex_replace_start(CommandContext &ctx)
static bool static bool
cmd_open_file_start(CommandContext &ctx) cmd_open_file_start(const CommandContext &ctx)
{ {
// Start a generic prompt to read a path // Start a generic prompt to read a path
ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", ""); ctx.editor.StartPrompt(Editor::PromptKind::OpenFile, "Open", "");
@@ -843,7 +991,7 @@ cmd_open_file_start(CommandContext &ctx)
// GUI: toggle visual file picker (no-op in terminal; renderer will consume flag) // GUI: toggle visual file picker (no-op in terminal; renderer will consume flag)
static bool static bool
cmd_visual_file_picker_toggle(CommandContext &ctx) cmd_visual_file_picker_toggle(const CommandContext &ctx)
{ {
// Toggle visibility // Toggle visibility
bool show = !ctx.editor.FilePickerVisible(); bool show = !ctx.editor.FilePickerVisible();
@@ -866,7 +1014,7 @@ cmd_visual_file_picker_toggle(CommandContext &ctx)
static bool 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) // Start a prompt to read a 1-based line number and jump there (clamped)
ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", ""); ctx.editor.StartPrompt(Editor::PromptKind::GotoLine, "Goto", "");
@@ -877,7 +1025,7 @@ cmd_jump_to_line_start(CommandContext &ctx)
// --- Buffers: switch/next/prev/close --- // --- Buffers: switch/next/prev/close ---
static bool 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 only one (or zero) buffer is open, do nothing per spec
if (ctx.editor.BufferCount() <= 1) { if (ctx.editor.BufferCount() <= 1) {
@@ -895,7 +1043,7 @@ buffer_display_name(const Buffer &b)
{ {
if (!b.Filename().empty()) if (!b.Filename().empty())
return b.Filename(); return b.Filename();
return std::string("<untitled>"); return {"<untitled>"};
} }
@@ -904,7 +1052,7 @@ buffer_basename(const Buffer &b)
{ {
const std::string &p = b.Filename(); const std::string &p = b.Filename();
if (p.empty()) if (p.empty())
return std::string("<untitled>"); return {"<untitled>"};
auto pos = p.find_last_of("/\\"); auto pos = p.find_last_of("/\\");
if (pos == std::string::npos) if (pos == std::string::npos)
return p; return p;
@@ -913,7 +1061,7 @@ buffer_basename(const Buffer &b)
static bool static bool
cmd_buffer_next(CommandContext &ctx) cmd_buffer_next(const CommandContext &ctx)
{ {
const auto cnt = ctx.editor.BufferCount(); const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) { if (cnt <= 1) {
@@ -930,7 +1078,7 @@ cmd_buffer_next(CommandContext &ctx)
static bool static bool
cmd_buffer_prev(CommandContext &ctx) cmd_buffer_prev(const CommandContext &ctx)
{ {
const auto cnt = ctx.editor.BufferCount(); const auto cnt = ctx.editor.BufferCount();
if (cnt <= 1) { if (cnt <= 1) {
@@ -947,7 +1095,7 @@ cmd_buffer_prev(CommandContext &ctx)
static bool static bool
cmd_buffer_close(CommandContext &ctx) cmd_buffer_close(const CommandContext &ctx)
{ {
if (ctx.editor.BufferCount() == 0) if (ctx.editor.BufferCount() == 0)
return true; return true;
@@ -1116,6 +1264,85 @@ cmd_insert_text(CommandContext &ctx)
ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText()); ctx.editor.SetStatus(ctx.editor.PromptLabel() + ": " + ctx.editor.PromptText());
return true; 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); ctx.editor.AppendPromptText(ctx.arg);
@@ -1223,6 +1450,7 @@ cmd_insert_text(CommandContext &ctx)
return true; return true;
} }
// Toggle read-only state of the current buffer // Toggle read-only state of the current buffer
static bool static bool
cmd_toggle_read_only(CommandContext &ctx) cmd_toggle_read_only(CommandContext &ctx)
@@ -1258,8 +1486,10 @@ cmd_show_help(CommandContext &ctx)
std::ostringstream out; std::ostringstream out;
std::string line; std::string line;
auto unquote = [](std::string s) { auto unquote = [](std::string s) {
if (!s.empty() && (s.front() == '"' || s.front() == '\'')) s.erase(s.begin()); if (!s.empty() && (s.front() == '"' || s.front() == '\''))
if (!s.empty() && (s.back() == '"' || s.back() == '\'')) s.pop_back(); s.erase(s.begin());
if (!s.empty() && (s.back() == '"' || s.back() == '\''))
s.pop_back();
return s; return s;
}; };
while (std::getline(iss, line)) { while (std::getline(iss, line)) {
@@ -1275,17 +1505,20 @@ cmd_show_help(CommandContext &ctx)
std::string title; std::string title;
std::getline(ls, title); std::getline(ls, title);
// trim leading spaces // 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); title = unquote(title);
out << "\n\n"; 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"; out << title << "\n";
} else if (macro == "PP" || macro == "P" || macro == "TP") { } else if (macro == "PP" || macro == "P" || macro == "TP") {
out << "\n"; out << "\n";
} else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") { } else if (macro == "B" || macro == "I" || macro == "BR" || macro == "IR") {
std::string rest; std::string rest;
std::getline(ls, 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"; out << unquote(rest) << "\n";
} else if (macro == "nf" || macro == "fi") { } else if (macro == "nf" || macro == "fi") {
// ignore fill mode toggles for now // ignore fill mode toggles for now
@@ -1297,11 +1530,23 @@ cmd_show_help(CommandContext &ctx)
// Regular text; apply minimal escape replacements // Regular text; apply minimal escape replacements
for (std::size_t i = 0; i < line.size(); ++i) { for (std::size_t i = 0; i < line.size(); ++i) {
if (line[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] == '(') { if (i + 3 < line.size() && line[i + 1] == '(') {
std::string esc = line.substr(i + 2, 2); std::string esc = line.substr(i + 2, 2);
if (esc == "em") { out << ""; i += 3; continue; } if (esc == "em") {
if (esc == "en") { out << "-"; i += 3; continue; } out << "";
i += 3;
continue;
}
if (esc == "en") {
out << "-";
i += 3;
continue;
}
} }
} }
out << line[i]; out << line[i];
@@ -1315,7 +1560,10 @@ cmd_show_help(CommandContext &ctx)
// 1) Prefer embedded/customizable help content // 1) Prefer embedded/customizable help content
{ {
std::string embedded = HelpText::Text(); 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 // 2) Fall back to the manpage and convert roff to plain text
@@ -1325,11 +1573,14 @@ cmd_show_help(CommandContext &ctx)
"/usr/local/share/man/man1/kte.1", "/usr/local/share/man/man1/kte.1",
"/usr/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); std::ifstream in(p);
if (in.good()) { if (in.good()) {
std::string s((std::istreambuf_iterator<char>(in)), std::istreambuf_iterator<char>()); 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 // Fallback minimal help text
@@ -1378,7 +1629,7 @@ cmd_show_help(CommandContext &ctx)
rows.clear(); rows.clear();
std::string line; std::string line;
line.reserve(128); line.reserve(128);
for (char ch : text) { for (char ch: text) {
if (ch == '\n') { if (ch == '\n') {
rows.emplace_back(line); rows.emplace_back(line);
line.clear(); line.clear();
@@ -1430,6 +1681,46 @@ cmd_newline(CommandContext &ctx)
Editor::PromptKind kind = ctx.editor.CurrentPromptKind(); Editor::PromptKind kind = ctx.editor.CurrentPromptKind();
std::string value = ctx.editor.PromptText(); std::string value = ctx.editor.PromptText();
ctx.editor.AcceptPrompt(); 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) { if (kind == Editor::PromptKind::Search || kind == Editor::PromptKind::RegexSearch) {
// Finish search: keep cursor where it is, clear search UI prompt // Finish search: keep cursor where it is, clear search UI prompt
ctx.editor.SetSearchActive(false); ctx.editor.SetSearchActive(false);
@@ -1786,7 +2077,7 @@ cmd_newline(CommandContext &ctx)
} }
auto &rows = buf->Rows(); auto &rows = buf->Rows();
std::size_t changed = 0; std::size_t changed = 0;
for (auto &line : rows) { for (auto &line: rows) {
std::string before = static_cast<std::string>(line); std::string before = static_cast<std::string>(line);
std::string after = std::regex_replace(before, rx, repl); std::string after = std::regex_replace(before, rx, repl);
if (after != before) { if (after != before) {
@@ -3275,7 +3566,25 @@ InstallDefaultCommands()
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
}); });
// Read-only // 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 // Buffer operations
CommandRegistry::Register({ CommandRegistry::Register({
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer

View File

@@ -69,6 +69,9 @@ enum class CommandId {
Redo, Redo,
// UI/status helpers // UI/status helpers
UArgStatus, // update status line during universal-argument collection UArgStatus, // update status line during universal-argument collection
// Themes (GUI)
ThemeNext,
ThemePrev,
// Region formatting // Region formatting
IndentRegion, // indent region (C-k =) IndentRegion, // indent region (C-k =)
UnindentRegion, // unindent region (C-k -) UnindentRegion, // unindent region (C-k -)
@@ -86,6 +89,12 @@ enum class CommandId {
ShowHelp, // open +HELP+ buffer with manual text (C-k h) ShowHelp, // open +HELP+ buffer with manual text (C-k h)
// Meta // Meta
UnknownKCommand, // arg: single character that was not recognized after C-k 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,
}; };
@@ -109,6 +118,8 @@ struct Command {
std::string name; // stable, unique name (e.g., "save", "save-as") std::string name; // stable, unique name (e.g., "save", "save-as")
std::string help; // short help/description std::string help; // short help/description
CommandHandler handler; CommandHandler handler;
// Public commands are exposed in the ": " prompt (C-k ;)
bool isPublic = false;
}; };

View File

@@ -315,7 +315,8 @@ public:
GotoLine, GotoLine,
Chdir, Chdir,
ReplaceFind, // step 1 of Search & Replace: find what 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 +525,28 @@ private:
// Temporary state for Search & Replace flow // Temporary state for Search & Replace flow
public: public:
void SetReplaceFindTmp(const std::string &s) { replace_find_tmp_ = s; } void SetReplaceFindTmp(const std::string &s)
void SetReplaceWithTmp(const std::string &s) { replace_with_tmp_ = s; } {
[[nodiscard]] const std::string &ReplaceFindTmp() const { return replace_find_tmp_; } replace_find_tmp_ = s;
[[nodiscard]] const std::string &ReplaceWithTmp() const { return replace_with_tmp_; } }
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: private:
std::string replace_find_tmp_; std::string replace_find_tmp_;

View File

@@ -102,6 +102,15 @@ GUIConfig::LoadFromFile(const std::string &path)
if (v > 0.0f) { if (v > 0.0f) {
font_size = v; 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;
} }
} }

View File

@@ -16,6 +16,10 @@ public:
int columns = 80; int columns = 80;
int rows = 42; int rows = 42;
float font_size = (float) KTE_FONT_SIZE; 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";
// Load from default path: $HOME/.config/kte/kge.ini // Load from default path: $HOME/.config/kte/kge.ini
static GUIConfig Load(); static GUIConfig Load();

View File

@@ -32,8 +32,8 @@ GUIFrontend::Init(Editor &ed)
return false; return false;
} }
// Load GUI configuration (fullscreen, columns/rows, font size) // Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
const auto [fullscreen, columns, rows, font_size] = GUIConfig::Load(); GUIConfig cfg = GUIConfig::Load();
// GL attributes for core profile // GL attributes for core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0); SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
@@ -47,7 +47,7 @@ GUIFrontend::Init(Editor &ed)
// Compute desired window size from config // Compute desired window size from config
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI; 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. // "Fullscreen": fill the usable bounds of the primary display.
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible. // On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
SDL_Rect usable{}; SDL_Rect usable{};
@@ -61,8 +61,8 @@ GUIFrontend::Init(Editor &ed)
#endif #endif
} else { } else {
// Windowed: width = columns * font_size, height = (rows * 2) * font_size // Windowed: width = columns * font_size, height = (rows * 2) * font_size
int w = static_cast<int>(columns * font_size); int w = cfg.columns * static_cast<int>(cfg.font_size);
int h = static_cast<int>((rows * 2) * font_size); int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
// As a safety, clamp to display usable bounds if retrievable // As a safety, clamp to display usable bounds if retrievable
SDL_Rect usable{}; SDL_Rect usable{};
@@ -86,7 +86,7 @@ GUIFrontend::Init(Editor &ed)
// macOS: when "fullscreen" is requested, position the window at the // macOS: when "fullscreen" is requested, position the window at the
// top-left of the usable display area to mimic fullscreen while keeping // top-left of the usable display area to mimic fullscreen while keeping
// the system menu bar visible. // the system menu bar visible.
if (fullscreen) { if (cfg.fullscreen) {
SDL_Rect usable{}; SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) { if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
SDL_SetWindowPosition(window_, usable.x, usable.y); SDL_SetWindowPosition(window_, usable.x, usable.y);
@@ -105,8 +105,13 @@ GUIFrontend::Init(Editor &ed)
ImGuiIO &io = ImGui::GetIO(); ImGuiIO &io = ImGui::GetIO();
(void) io; (void) io;
ImGui::StyleColorsDark(); 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);
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_)) if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
return false; return false;
@@ -135,7 +140,7 @@ GUIFrontend::Init(Editor &ed)
#endif #endif
// Initialize GUI font from embedded default (use configured size or compiled default) // 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; return true;
} }
@@ -214,7 +219,7 @@ GUIFrontend::Step(Editor &ed, bool &running)
float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h); float avail_h = std::max(0.0f, disp_h - 2.0f * pad_y - status_h);
// Visible content rows inside the scroll child // 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. // 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 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))); std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
@@ -264,11 +269,11 @@ GUIFrontend::Shutdown()
bool bool
GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px) GUIFrontend::LoadGuiFont_(const char * /*path*/, float size_px)
{ {
ImGuiIO &io = ImGui::GetIO(); const ImGuiIO &io = ImGui::GetIO();
io.Fonts->Clear(); io.Fonts->Clear();
ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF( const ImFont *font = io.Fonts->AddFontFromMemoryCompressedTTF(
(void *) DefaultFontBoldCompressedData, DefaultFontBoldCompressedData,
(int) DefaultFontBoldCompressedSize, DefaultFontBoldCompressedSize,
size_px); size_px);
if (!font) { if (!font) {
font = io.Fonts->AddFontDefault(); font = io.Fonts->AddFontDefault();

View File

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

View File

@@ -92,10 +92,14 @@ map_key(const SDL_Keycode key,
out = {true, CommandId::Backspace, "", 0}; out = {true, CommandId::Backspace, "", 0};
return true; return true;
case SDLK_TAB: case SDLK_TAB:
// Do not insert text on KEYDOWN; allow SDL_TEXTINPUT to deliver '\t' // Insert a literal tab character when not interpreting a k-prefix suffix.
// as printable input so that all printable characters flow via TEXTINPUT. // If k-prefix is active, let the k-prefix handler below consume the key
out.hasCommand = false; // (so Tab doesn't leave k-prefix stuck).
if (!k_prefix) {
out = {true, CommandId::InsertText, std::string("\t"), 0};
return true; return true;
}
break; // fall through so k-prefix handler can process
case SDLK_RETURN: case SDLK_RETURN:
case SDLK_KP_ENTER: case SDLK_KP_ENTER:
out = {true, CommandId::Newline, "", 0}; out = {true, CommandId::Newline, "", 0};
@@ -347,6 +351,12 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
uarg_text_, uarg_text_,
mi); 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, // 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. // suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) { if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {

View File

@@ -254,10 +254,12 @@ GUIRenderer::Draw(Editor &ed)
std::size_t rx_abs_draw = 0; // rendered column for drawing std::size_t rx_abs_draw = 0; // rendered column for drawing
// Compute search highlight ranges for this line in source indices // Compute search highlight ranges for this line in source indices
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); 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 (search_mode) {
// If we're in RegexSearch or RegexReplaceFind mode, compute ranges using regex; otherwise plain substring // 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 { try {
std::regex rx(ed.SearchQuery()); std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(line.begin(), line.end(), rx); for (auto it = std::sregex_iterator(line.begin(), line.end(), rx);
@@ -297,19 +299,23 @@ GUIRenderer::Draw(Editor &ed)
bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i; bool has_current = ed.SearchMatchLen() > 0 && ed.SearchMatchY() == i;
std::size_t cur_x = has_current ? ed.SearchMatchX() : 0; std::size_t cur_x = has_current ? ed.SearchMatchX() : 0;
std::size_t cur_end = has_current ? (ed.SearchMatchX() + ed.SearchMatchLen()) : 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 sx = rg.first, ex = rg.second;
std::size_t rx_start = src_to_rx(sx); std::size_t rx_start = src_to_rx(sx);
std::size_t rx_end = src_to_rx(ex); std::size_t rx_end = src_to_rx(ex);
// Apply horizontal scroll offset // 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 vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
std::size_t vx1 = rx_end - coloffs_now; std::size_t vx1 = rx_end - coloffs_now;
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, line_pos.y); 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 // Choose color: current match stronger
bool is_current = has_current && sx == cur_x && ex == cur_end; 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); ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
} }
} }
@@ -390,13 +396,18 @@ GUIRenderer::Draw(Editor &ed)
float max_px = std::max(0.0f, right_x - left_x); float max_px = std::max(0.0f, right_x - left_x);
std::string prefix; 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 // Compose showing right-end of filename portion when too long for space
std::string final_msg; std::string final_msg;
ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str()); ImVec2 prefix_sz = ImGui::CalcTextSize(prefix.c_str());
float avail_px = std::max(0.0f, max_px - prefix_sz.x); 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 // Trim from left until it fits by pixel width
std::string tail = ptext; std::string tail = ptext;
ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str()); ImVec2 tail_sz = ImGui::CalcTextSize(tail.c_str());
@@ -408,7 +419,11 @@ GUIRenderer::Draw(Editor &ed)
while (start < tail.size()) { while (start < tail.size()) {
// Estimate how many chars to skip based on ratio // Estimate how many chars to skip based on ratio
float ratio = tail_sz.x / avail_px; 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; start += skip;
std::string candidate = tail.substr(start); std::string candidate = tail.substr(start);
ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str()); ImVec2 cand_sz = ImGui::CalcTextSize(candidate.c_str());
@@ -425,7 +440,10 @@ GUIRenderer::Draw(Editor &ed)
while (lo < hi) { while (lo < hi) {
size_t mid = (lo + hi) / 2; size_t mid = (lo + hi) / 2;
std::string cand = tail.substr(mid); 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); tail = tail.substr(lo);
} }
@@ -529,7 +547,8 @@ GUIRenderer::Draw(Editor &ed)
} }
// Draw right // 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()); ImGui::TextUnformatted(right.c_str());
// Draw middle message centered in remaining space // Draw middle message centered in remaining space

1021
GUITheme.h

File diff suppressed because it is too large Load Diff

View File

@@ -15,24 +15,26 @@ HelpText::Text()
return std::string( return std::string(
"KTE - Kyle's Text Editor\n\n" "KTE - Kyle's Text Editor\n\n"
"About:\n" "About:\n"
" kte is Kyle's Text Editor and is probably ill-suited to everyone else. It was\n" " kte is Kyle's Text Editor. It keeps a small, fast core and uses a\n"
" inspired by Antirez' kilo text editor by way of someone's writeup of the\n" " WordStar/VDE-style command model with some emacs influences.\n"
" process of writing a text editor from scratch. It has keybindings inspired by\n"
" VDE (and the Wordstar family) and emacs; its spiritual parent is mg(1).\n"
"\n" "\n"
"Core keybindings:\n" "K-commands (prefix C-k):\n"
" C-k ' Toggle read-only\n" " C-k ' Toggle read-only\n"
" C-k - Unindent region\n" " C-k - Unindent region (mark required)\n"
" C-k = Indent region\n" " C-k = Indent region (mark required)\n"
" C-k ; Command prompt (:\\ )\n"
" C-k C-d Kill entire line\n" " C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\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 b Switch buffer\n"
" C-k c Close current buffer\n" " C-k c Close current buffer\n"
" C-k d Kill to end of line\n" " C-k d Kill to end of line\n"
" C-k e Open file (prompt)\n" " C-k e Open file (prompt)\n"
" C-k f Flush kill ring\n"
" C-k g Jump to line\n" " C-k g Jump to line\n"
" C-k h Show this help\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 l Reload buffer from disk\n"
" C-k n Previous buffer\n" " C-k n Previous buffer\n"
" C-k o Change working directory (prompt)\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 v Toggle visual file picker (GUI)\n"
" C-k w Show working directory\n" " C-k w Show working directory\n"
" C-k x Save and quit\n" " C-k x Save and quit\n"
" C-k y Yank\n"
"\n" "\n"
"ESC/Alt commands:\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 q Reflow paragraph\n"
" ESC BACKSPACE Delete previous word\n" "\n"
" ESC d Delete next word\n" "Control keys:\n"
" Alt-w Copy region to kill ring\n\n" " C-a C-e Line start / end\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle if you need to edit; C-k h restores it.\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"
); );
} }

View File

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

View File

@@ -48,11 +48,13 @@ TerminalRenderer::Draw(Editor &ed)
std::size_t src_i = 0; std::size_t src_i = 0;
// Compute matches for this line if search highlighting is active // Compute matches for this line if search highlighting is active
bool search_mode = ed.SearchActive() && !ed.SearchQuery().empty(); 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()) { if (search_mode && li < lines.size()) {
std::string sline = static_cast<std::string>(lines[li]); 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 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 { try {
std::regex rx(ed.SearchQuery()); std::regex rx(ed.SearchQuery());
for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx); for (auto it = std::sregex_iterator(sline.begin(), sline.end(), rx);
@@ -75,12 +77,15 @@ TerminalRenderer::Draw(Editor &ed)
} }
} }
auto is_src_in_hl = [&](std::size_t si) -> bool { 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 // ranges are non-overlapping and ordered by construction
// linear scan is fine for now // linear scan is fine for now
for (const auto &rg : ranges) { for (const auto &rg: ranges) {
if (si < rg.first) break; if (si < rg.first)
if (si >= rg.first && si < rg.second) return true; break;
if (si >= rg.first && si < rg.second)
return true;
} }
return false; return false;
}; };
@@ -119,15 +124,31 @@ TerminalRenderer::Draw(Editor &ed)
// Now render visible spaces // Now render visible spaces
while (next_tab > 0 && written < cols) { while (next_tab > 0 && written < cols) {
bool in_hl = search_mode && is_src_in_hl(src_i); 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 // Toggle highlight attributes
int attr = 0; int attr = 0;
if (in_hl) attr |= A_STANDOUT; if (in_hl)
if (in_cur) attr |= A_BOLD; attr |= A_STANDOUT;
if ((attr & A_STANDOUT) && !hl_on) { attron(A_STANDOUT); hl_on = true; } if (in_cur)
if (!(attr & A_STANDOUT) && hl_on) { attroff(A_STANDOUT); hl_on = false; } attr |= A_BOLD;
if ((attr & A_BOLD) && !cur_on) { attron(A_BOLD); cur_on = true; } if ((attr & A_STANDOUT) && !hl_on) {
if (!(attr & A_BOLD) && cur_on) { attroff(A_BOLD); cur_on = false; } attron(A_STANDOUT);
hl_on = true;
}
if (!(attr & A_STANDOUT) && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if ((attr & A_BOLD) && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!(attr & A_BOLD) && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
addch(' '); addch(' ');
++written; ++written;
++render_col; ++render_col;
@@ -151,11 +172,25 @@ TerminalRenderer::Draw(Editor &ed)
from_src = false; from_src = false;
} }
bool in_hl = search_mode && from_src && is_src_in_hl(src_i); 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; bool in_cur =
if (in_hl && !hl_on) { attron(A_STANDOUT); hl_on = true; } has_current && li == cur_my && from_src && src_i >= cur_mx && src_i <
if (!in_hl && hl_on) { attroff(A_STANDOUT); hl_on = false; } cur_mend;
if (in_cur && !cur_on) { attron(A_BOLD); cur_on = true; } if (in_hl && !hl_on) {
if (!in_cur && cur_on) { attroff(A_BOLD); cur_on = false; } attron(A_STANDOUT);
hl_on = true;
}
if (!in_hl && hl_on) {
attroff(A_STANDOUT);
hl_on = false;
}
if (in_cur && !cur_on) {
attron(A_BOLD);
cur_on = true;
}
if (!in_cur && cur_on) {
attroff(A_BOLD);
cur_on = false;
}
addch(static_cast<unsigned char>(ch)); addch(static_cast<unsigned char>(ch));
++written; ++written;
++render_col; ++render_col;
@@ -222,20 +257,23 @@ TerminalRenderer::Draw(Editor &ed)
} }
// Prefer keeping the tail of the filename visible when it exceeds the window // Prefer keeping the tail of the filename visible when it exceeds the window
std::string msg; std::string msg;
if (!label.empty()) { if (kind == Editor::PromptKind::Command) {
msg = ": ";
} else if (!label.empty()) {
msg = label + ": "; msg = label + ": ";
} }
// When dealing with file-related prompts, left-trim the filename text so the tail stays visible // 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()); int avail = cols - static_cast<int>(msg.size());
if (avail <= 0) { if (avail <= 0) {
// No room for label; fall back to showing the rightmost portion of the whole string // No room for label; fall back to showing the rightmost portion of the whole string
std::string whole = msg + ptext; std::string whole = msg + ptext;
if ((int)whole.size() > cols) if ((int) whole.size() > cols)
whole = whole.substr(whole.size() - cols); whole = whole.substr(whole.size() - cols);
msg = whole; msg = whole;
} else { } else {
if ((int)ptext.size() > avail) { if ((int) ptext.size() > avail) {
ptext = ptext.substr(ptext.size() - avail); ptext = ptext.substr(ptext.size() - avail);
} }
msg += ptext; msg += ptext;

View File

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

View File

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

View File

@@ -1,7 +1,7 @@
.\" kge(1) — Kyle's Graphical Editor (GUI-first) .\" kge(1) — Kyle's Graphical Editor (GUI-first)
.\" .\"
.\" Project homepage: https://github.com/wntrmute/kte .\" 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 .SH NAME
kge \- Kyle's Graphical Editor (GUI-first) kge \- Kyle's Graphical Editor (GUI-first)
.SH SYNOPSIS .SH SYNOPSIS
@@ -52,11 +52,8 @@ tree for the canonical reference and notes:
.PP .PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G. Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP .TP
.B C-k BACKSPACE .B C-k '
Delete from the cursor to the beginning of the line. Toggle read-only for the current buffer.
.TP
.B C-k SPACE
Toggle the mark.
.TP .TP
.B C-k - .B C-k -
If the mark is set, unindent the region. If the mark is set, unindent the region.
@@ -64,6 +61,9 @@ If the mark is set, unindent the region.
.B C-k = .B C-k =
If the mark is set, indent the region. If the mark is set, indent the region.
.TP .TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a .B C-k a
Set the mark at the beginning of the file, then jump to the end of the file. Set the mark at the beginning of the file, then jump to the end of the file.
.TP .TP
@@ -80,7 +80,7 @@ Delete from the cursor to the end of the line.
Delete the entire line. Delete the entire line.
.TP .TP
.B C-k e .B C-k e
Edit a new file. Edit (open) a new file.
.TP .TP
.B C-k f .B C-k f
Flush the kill ring. Flush the kill ring.
@@ -88,14 +88,20 @@ Flush the kill ring.
.B C-k g .B C-k g
Go to a specific line. Go to a specific line.
.TP .TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.TP
.B C-k j .B C-k j
Jump to the mark. Jump to the mark.
.TP .TP
.B C-k l .B C-k l
Reload the current buffer from disk. Reload the current buffer from disk.
.TP .TP
.B C-k m .B C-k n
Run make(1), reporting success or failure. Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP .TP
.B C-k p .B C-k p
Switch to the next buffer. 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 .B C-k C-q
Immediately exit the editor. Immediately exit the editor.
.TP .TP
.B C-k r
Redo changes.
.TP
.B C-k s .B C-k s
Save the file, prompting for a filename if needed. Save the file, prompting for a filename if needed.
.TP .TP
.B C-k u .B C-k u
Undo. Undo.
.TP .TP
.B C-k r .B C-k v
Redo changes. Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP .TP
.B C-k x .B C-k x
Save the file and exit. Also C-k C-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 .B C-k y
Yank the kill ring. Yank the kill ring.
.TP .TP
.B C-k \e .B C-k C-x
Dump core. Save the file and exit.
.SS Other keybindings .SS Other keybindings
.TP .TP
.B C-g .B C-g
Cancel the current operation. Cancel the current operation.
.TP .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 .B C-l
Refresh the display. Refresh the display.
.TP .TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r .B C-r
Regex search. Regex search.
.TP .TP
.B C-s .B C-s
Incremental find. Incremental find.
.TP .TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u .B C-u
Universal argument. C-u followed by numbers will repeat an operation n times. Universal argument. C-u followed by numbers will repeat an operation n times.
.TP .TP
@@ -147,6 +186,15 @@ Kill the region if the mark is set.
.B C-y .B C-y
Yank the kill ring. Yank the kill ring.
.TP .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 .B ESC BACKSPACE
Delete the previous word. Delete the previous word.
.TP .TP

View File

@@ -1,7 +1,7 @@
.\" kte(1) — Kyle's Text Editor (terminal-first) .\" kte(1) — Kyle's Text Editor (terminal-first)
.\" .\"
.\" Project homepage: https://github.com/wntrmute/kte .\" 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 .SH NAME
kte \- Kyle's Text Editor (terminal-first) kte \- Kyle's Text Editor (terminal-first)
.SH SYNOPSIS .SH SYNOPSIS
@@ -57,11 +57,8 @@ in the source tree for the canonical reference and notes.
.PP .PP
Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G. Enter K-command mode with Ctrl-K. Exit K-command mode with ESC or Ctrl-G.
.TP .TP
.B C-k BACKSPACE .B C-k '
Delete from the cursor to the beginning of the line. Toggle read-only for the current buffer.
.TP
.B C-k SPACE
Toggle the mark.
.TP .TP
.B C-k - .B C-k -
If the mark is set, unindent the region. If the mark is set, unindent the region.
@@ -69,6 +66,9 @@ If the mark is set, unindent the region.
.B C-k = .B C-k =
If the mark is set, indent the region. If the mark is set, indent the region.
.TP .TP
.B C-k ;
Open the generic command prompt (": ").
.TP
.B C-k a .B C-k a
Set the mark at the beginning of the file, then jump to the end of the file. Set the mark at the beginning of the file, then jump to the end of the file.
.TP .TP
@@ -85,7 +85,7 @@ Delete from the cursor to the end of the line.
Delete the entire line. Delete the entire line.
.TP .TP
.B C-k e .B C-k e
Edit a new file. Edit (open) a new file.
.TP .TP
.B C-k f .B C-k f
Flush the kill ring. Flush the kill ring.
@@ -93,14 +93,20 @@ Flush the kill ring.
.B C-k g .B C-k g
Go to a specific line. Go to a specific line.
.TP .TP
.B C-k h
Show the built-in help (+HELP+ buffer).
.TP
.B C-k j .B C-k j
Jump to the mark. Jump to the mark.
.TP .TP
.B C-k l .B C-k l
Reload the current buffer from disk. Reload the current buffer from disk.
.TP .TP
.B C-k m .B C-k n
Run make(1), reporting success or failure. Switch to the previous buffer.
.TP
.B C-k o
Change working directory (prompt).
.TP .TP
.B C-k p .B C-k p
Switch to the next buffer. 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 .B C-k C-q
Immediately exit the editor. Immediately exit the editor.
.TP .TP
.B C-k r
Redo changes.
.TP
.B C-k s .B C-k s
Save the file, prompting for a filename if needed. Save the file, prompting for a filename if needed.
.TP .TP
.B C-k u .B C-k u
Undo. Undo.
.TP .TP
.B C-k r .B C-k v
Redo changes. Toggle visual file picker (GUI).
.TP
.B C-k w
Show the current working directory.
.TP .TP
.B C-k x .B C-k x
Save the file and exit. Also C-k C-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 .B C-k y
Yank the kill ring. Yank the kill ring.
.TP .TP
.B C-k \e .B C-k C-x
Dump core. 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 .SS Other keybindings
.TP .TP
.B C-g .B C-g
Cancel the current operation. Cancel the current operation.
.TP .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 .B C-l
Refresh the display. Refresh the display.
.TP .TP
.B C-d
Delete the character at the cursor.
.TP
.B C-r .B C-r
Regex search. Regex search.
.TP .TP
.B C-s .B C-s
Incremental find. Incremental find.
.TP .TP
.B C-t
Regex search and replace.
.TP
.B C-h
Search and replace.
.TP
.B C-u .B C-u
Universal argument. C-u followed by numbers will repeat an operation n times. Universal argument. C-u followed by numbers will repeat an operation n times.
.TP .TP
@@ -152,6 +217,15 @@ Kill the region if the mark is set.
.B C-y .B C-y
Yank the kill ring. Yank the kill ring.
.TP .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 .B ESC BACKSPACE
Delete the previous word. Delete the previous word.
.TP .TP

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.