Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 655cc40162 |
28
.idea/workspace.xml
generated
28
.idea/workspace.xml
generated
@@ -7,6 +7,7 @@
|
||||
<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/LiveTemplatesHousekeeping/HotspotSessionHintIsShown/@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" />
|
||||
@@ -34,7 +35,29 @@
|
||||
</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$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Buffer.h" beforeDir="false" afterPath="$PROJECT_DIR$/Buffer.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/CMakeLists.txt" beforeDir="false" afterPath="$PROJECT_DIR$/CMakeLists.txt" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.cc" beforeDir="false" afterPath="$PROJECT_DIR$/Command.cc" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Command.h" beforeDir="false" afterPath="$PROJECT_DIR$/Command.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/Editor.h" beforeDir="false" afterPath="$PROJECT_DIR$/Editor.h" afterDir="false" />
|
||||
<change beforePath="$PROJECT_DIR$/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>
|
||||
<option name="SHOW_DIALOG" value="false" />
|
||||
<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" />
|
||||
</component>
|
||||
<component name="OptimizeOnSaveOptions">
|
||||
<option name="myRunOnSave" value="true" />
|
||||
</component>
|
||||
<component name="ProblemsViewState">
|
||||
<option name="selectedTabId" value="AISelfReview" />
|
||||
<option name="selectedTabId" value="CurrentFile" />
|
||||
</component>
|
||||
<component name="ProjectApplicationVersion">
|
||||
<option name="ide" value="CLion" />
|
||||
@@ -173,7 +197,7 @@
|
||||
<workItem from="1764539556448" duration="156000" />
|
||||
<workItem from="1764539725338" duration="1075000" />
|
||||
<workItem from="1764542392763" duration="3512000" />
|
||||
<workItem from="1764548345516" duration="39312000" />
|
||||
<workItem from="1764548345516" duration="50201000" />
|
||||
</task>
|
||||
<task id="LOCAL-00001" summary="Add undo/redo infrastructure and buffer management additions.">
|
||||
<option name="closed" value="true" />
|
||||
|
||||
4
Buffer.h
4
Buffer.h
@@ -262,6 +262,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 +283,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_;
|
||||
|
||||
@@ -4,7 +4,7 @@ project(kte)
|
||||
include(GNUInstallDirs)
|
||||
|
||||
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.
|
||||
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
|
||||
|
||||
359
Command.cc
359
Command.cc
@@ -4,12 +4,16 @@
|
||||
#include <regex>
|
||||
#include <fstream>
|
||||
#include <sstream>
|
||||
#include <cctype>
|
||||
|
||||
#include "Command.h"
|
||||
#include "Editor.h"
|
||||
#include "Buffer.h"
|
||||
#include "UndoSystem.h"
|
||||
#include "HelpText.h"
|
||||
#ifdef KTE_BUILD_GUI
|
||||
#include "GUITheme.h"
|
||||
#endif
|
||||
|
||||
|
||||
// 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)
|
||||
static bool is_mutating_command(CommandId id)
|
||||
static bool
|
||||
is_mutating_command(CommandId id)
|
||||
{
|
||||
switch (id) {
|
||||
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
|
||||
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
|
||||
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 ® = 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 +909,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 +932,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 +956,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 +980,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 +991,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 +1014,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 +1025,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 +1043,7 @@ buffer_display_name(const Buffer &b)
|
||||
{
|
||||
if (!b.Filename().empty())
|
||||
return b.Filename();
|
||||
return std::string("<untitled>");
|
||||
return {"<untitled>"};
|
||||
}
|
||||
|
||||
|
||||
@@ -904,7 +1052,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 +1061,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 +1078,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 +1095,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 +1264,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 ® = 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 +1450,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 +1486,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 +1505,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 +1530,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 +1560,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
|
||||
@@ -1329,7 +1577,10 @@ cmd_show_help(CommandContext &ctx)
|
||||
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
|
||||
@@ -1430,6 +1681,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);
|
||||
@@ -3275,7 +3566,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
|
||||
|
||||
11
Command.h
11
Command.h
@@ -69,6 +69,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 +89,12 @@ 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,
|
||||
};
|
||||
|
||||
|
||||
@@ -109,6 +118,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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
29
Editor.h
29
Editor.h
@@ -315,7 +315,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 +525,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_;
|
||||
|
||||
@@ -102,6 +102,15 @@ 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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,10 @@ 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";
|
||||
|
||||
// Load from default path: $HOME/.config/kte/kge.ini
|
||||
static GUIConfig Load();
|
||||
|
||||
@@ -32,8 +32,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 +47,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 +61,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 +86,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 +105,13 @@ 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);
|
||||
|
||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
||||
return false;
|
||||
@@ -135,7 +140,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;
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
// 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)));
|
||||
@@ -264,11 +269,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();
|
||||
|
||||
@@ -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_{};
|
||||
|
||||
@@ -92,10 +92,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};
|
||||
@@ -347,6 +351,12 @@ 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) {
|
||||
|
||||
@@ -257,7 +257,9 @@ GUIRenderer::Draw(Editor &ed)
|
||||
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);
|
||||
@@ -302,14 +304,18 @@ GUIRenderer::Draw(Editor &ed)
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -390,13 +396,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 +419,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 +440,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 +547,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
|
||||
|
||||
1021
GUITheme.h
1021
GUITheme.h
File diff suppressed because it is too large
Load Diff
50
HelpText.cc
50
HelpText.cc
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -52,7 +52,9 @@ TerminalRenderer::Draw(Editor &ed)
|
||||
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 +77,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;
|
||||
if (si < rg.first)
|
||||
break;
|
||||
if (si >= rg.first && si < rg.second)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
@@ -119,15 +124,31 @@ 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;
|
||||
}
|
||||
addch(' ');
|
||||
++written;
|
||||
++render_col;
|
||||
@@ -151,11 +172,25 @@ 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;
|
||||
}
|
||||
addch(static_cast<unsigned char>(ch));
|
||||
++written;
|
||||
++render_col;
|
||||
@@ -222,11 +257,14 @@ 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
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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();
|
||||
|
||||
74
docs/kge.1
74
docs/kge.1
@@ -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
|
||||
|
||||
100
docs/kte.1
100
docs/kte.1
@@ -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
|
||||
|
||||
102
docs/syntax on.md
Normal file
102
docs/syntax on.md
Normal file
@@ -0,0 +1,102 @@
|
||||
### Objective
|
||||
Introduce fast, minimal‑dependency 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 per‑line highlighting; handle single‑line comments and strings; defer multi‑line state to v2.
|
||||
- Toggle: `:syntax on|off` and per‑buffer 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; };` // 0‑based 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:
|
||||
- Per‑Buffer 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.
|
||||
- Z‑order 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 semi‑transparent 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/16‑color 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>` — per‑buffer 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 hand‑rolled 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 per‑row 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
|
||||
- Viewport‑first highlighting: compute only visible rows each frame; background task warms cache around viewport.
|
||||
- Reuse span buffers, avoid allocations; small‑vector 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 Tree‑sitter 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: feature‑detect 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: per‑buffer 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.
|
||||
Reference in New Issue
Block a user