Compare commits

...

7 Commits

Author SHA1 Message Date
81a5c25071 Fix proportional font rendering with pixel-based horizontal scroll
The GUI renderer had two competing horizontal scroll systems: a
character-based one (coloffs × space_w) and a pixel-based one. For
proportional fonts the character-based system used "M" width to
calculate viewport columns, triggering premature scrolling at ~50%
of the actual display width.

Switch the GUI renderer to purely pixel-based horizontal scrolling:
- Remove coloffs↔ImGui scroll_x bidirectional sync
- Measure rx_to_px from column 0 (absolute) instead of from coloffs
- Draw full expanded lines; let ImGui clip via its scroll viewport
- Report content width via SetCursorPosX+Dummy for the scrollbar
- Use average character width for cols estimate (not "M" width)

The terminal renderer continues using coloffs correctly—no changes
needed there.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:18:53 -07:00
5667a6d7bd Fix Linux build: default static linking off, add Clang link flag
KTE_STATIC_LINK defaulted to ON, which fails on systems where ncurses
is only available as a shared library. The Nix build already passed
-DKTE_STATIC_LINK=OFF explicitly; this makes the default match.

Also add add_link_options("-stdlib=libc++") for Clang builds — without
it, compilation uses libc++ but the linker defaults to libstdc++,
causing undefined symbol errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:18:26 -07:00
99c4bb2066 Fix font atlas crash and make Nix build first-class
Defer edit-mode font switch to next frame via RequestLoadFont() to
avoid modifying the locked ImFontAtlas between NewFrame() and Render().

Rework Nix packaging: split nativeBuildInputs/buildInputs correctly,
add devShells for nix develop, desktop file for kge, per-variant pname
and meta.mainProgram, and an overlay for NixOS configs.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-31 14:18:26 -07:00
953fee97d7 Bump patch. 2026-03-25 07:43:49 -07:00
d7e35727f1 Fix segfault from mid-frame font atlas rebuild
The edit-mode font switcher called LoadFont() directly between
NewFrame() and Render(), invalidating the font atlas ImGui was
actively using. Use RequestLoadFont() to defer the change to
the safe inter-frame point, matching the existing zoom pattern.

Also default code_font/writing_font to the main font when not
explicitly configured, preventing a mismatch that triggered the
switch on every first frame.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-25 02:07:31 -07:00
23f04e4357 Add proportional fonts, edit modes, and TOML config
- Add three proportional serif fonts: Crimson Pro, ET Book, Spectral
- Fix text rendering for variable-width fonts: selection, cursor,
  mouse click mapping, search highlights, and syntax-colored text
  now use pixel-accurate measurement via ImGui::CalcTextSize()
- Add per-buffer edit mode (code/writing) with auto-detection from
  file extension (.txt, .md, .rst, .org, .tex default to writing)
- Add C-k m keybinding and :mode command to toggle edit modes
- Switch config format from INI to TOML (kge.toml), with legacy
  INI fallback; vendor toml++ v3.4.0
- New config keys: font.code and font.writing for per-mode defaults
- Add font tab completion for ImGui builds
- Add tab completion for :mode command
- Update help text, themes.md, and add CONFIG.md
- Bump version to 1.10.0

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 23:05:56 -07:00
0585edad9e Disable Qt build in make-app-release
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 21:22:11 -07:00
24 changed files with 24660 additions and 198 deletions

View File

@@ -35,9 +35,12 @@
*/ */
#pragma once #pragma once
#include <algorithm>
#include <cstddef> #include <cstddef>
#include <filesystem>
#include <memory> #include <memory>
#include <string> #include <string>
#include <unordered_set>
#include <vector> #include <vector>
#include <string_view> #include <string_view>
@@ -48,6 +51,26 @@
#include "Highlight.h" #include "Highlight.h"
#include <mutex> #include <mutex>
// Edit mode determines which font class is used for a buffer.
enum class EditMode { Code, Writing };
// Detect edit mode from a filename's extension.
inline EditMode
DetectEditMode(const std::string &filename)
{
std::string ext = std::filesystem::path(filename).extension().string();
std::transform(ext.begin(), ext.end(), ext.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
static const std::unordered_set<std::string> writing_exts = {
".txt", ".md", ".markdown", ".rst", ".org",
".tex", ".adoc", ".asciidoc",
};
if (writing_exts.count(ext))
return EditMode::Writing;
return EditMode::Code;
}
// Forward declaration for swap journal integration // Forward declaration for swap journal integration
namespace kte { namespace kte {
class SwapRecorder; class SwapRecorder;
@@ -484,6 +507,27 @@ public:
} }
// Edit mode (code vs writing)
[[nodiscard]] EditMode GetEditMode() const
{
return edit_mode_;
}
void SetEditMode(EditMode m)
{
edit_mode_ = m;
}
void ToggleEditMode()
{
edit_mode_ = (edit_mode_ == EditMode::Code)
? EditMode::Writing
: EditMode::Code;
}
void SetSyntaxEnabled(bool on) void SetSyntaxEnabled(bool on)
{ {
syntax_enabled_ = on; syntax_enabled_ = on;
@@ -614,6 +658,9 @@ private:
std::unique_ptr<struct UndoTree> undo_tree_; std::unique_ptr<struct UndoTree> undo_tree_;
std::unique_ptr<UndoSystem> undo_sys_; std::unique_ptr<UndoSystem> undo_sys_;
// Edit mode (code vs writing)
EditMode edit_mode_ = EditMode::Code;
// Syntax/highlighting state // Syntax/highlighting state
std::uint64_t version_ = 0; // increment on edits std::uint64_t version_ = 0; // increment on edits
bool syntax_enabled_ = true; bool syntax_enabled_ = true;

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs) include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD 20)
set(KTE_VERSION "1.9.1") set(KTE_VERSION "1.11.0")
# 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.
@@ -14,7 +14,7 @@ set(BUILD_TESTS ON CACHE BOOL "Enable building test programs.")
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI") set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF) option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF) option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
option(KTE_STATIC_LINK "Enable static linking on Linux" ON) option(KTE_STATIC_LINK "Enable static linking on Linux" OFF)
# Optionally enable AddressSanitizer (ASan) # Optionally enable AddressSanitizer (ASan)
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF) option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
@@ -51,6 +51,7 @@ else ()
) )
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang") if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
add_compile_options("-stdlib=libc++") add_compile_options("-stdlib=libc++")
add_link_options("-stdlib=libc++")
else () else ()
# nothing special for gcc at the moment # nothing special for gcc at the moment
endif () endif ()
@@ -205,6 +206,8 @@ set(FONT_HEADERS
fonts/FontList.h fonts/FontList.h
fonts/B612Mono.h fonts/B612Mono.h
fonts/BrassMono.h fonts/BrassMono.h
fonts/CrimsonPro.h
fonts/ETBook.h
fonts/BrassMonoCode.h fonts/BrassMonoCode.h
fonts/FiraCode.h fonts/FiraCode.h
fonts/Go.h fonts/Go.h
@@ -216,6 +219,7 @@ set(FONT_HEADERS
fonts/IosevkaExtended.h fonts/IosevkaExtended.h
fonts/ShareTech.h fonts/ShareTech.h
fonts/SpaceMono.h fonts/SpaceMono.h
fonts/Spectral.h
fonts/Syne.h fonts/Syne.h
fonts/Triplicate.h fonts/Triplicate.h
fonts/Unispace.h fonts/Unispace.h

116
CONFIG.md Normal file
View File

@@ -0,0 +1,116 @@
# kge Configuration
kge loads configuration from `~/.config/kte/kge.toml`. If no TOML file is
found, it falls back to the legacy `kge.ini` format.
## TOML Format
```toml
[window]
fullscreen = false
columns = 80
rows = 42
[font]
# Default font and size
name = "default"
size = 18.0
# Font used in code mode (monospace)
code = "default"
# Font used in writing mode (proportional)
writing = "crimsonpro"
[appearance]
theme = "nord"
# "dark" or "light" for themes with variants
background = "dark"
[editor]
syntax = true
```
## Sections
### `[window]`
| Key | Type | Default | Description |
|--------------|------|---------|---------------------------------|
| `fullscreen` | bool | false | Start in fullscreen mode |
| `columns` | int | 80 | Initial window width in columns |
| `rows` | int | 42 | Initial window height in rows |
### `[font]`
| Key | Type | Default | Description |
|-----------|--------|--------------|------------------------------------------|
| `name` | string | "default" | Default font loaded at startup |
| `size` | float | 18.0 | Font size in pixels |
| `code` | string | "default" | Font for code mode (monospace) |
| `writing` | string | "crimsonpro" | Font for writing mode (proportional) |
### `[appearance]`
| Key | Type | Default | Description |
|--------------|--------|---------|-----------------------------------------|
| `theme` | string | "nord" | Color theme |
| `background` | string | "dark" | Background mode: "dark" or "light" |
### `[editor]`
| Key | Type | Default | Description |
|----------|------|---------|------------------------------|
| `syntax` | bool | true | Enable syntax highlighting |
## Edit Modes
kge has two edit modes that control which font is used:
- **code** — Uses the monospace font (`font.code`). Default for source files.
- **writing** — Uses the proportional font (`font.writing`). Auto-detected
for `.txt`, `.md`, `.markdown`, `.rst`, `.org`, `.tex`, `.adoc`, and
`.asciidoc` files.
Toggle with `C-k m` or `: mode [code|writing]`.
## Available Fonts
### Monospace
b612, berkeley, berkeley-bold, brassmono, brassmono-bold, brassmonocode,
brassmonocode-bold, fira, go, ibm, idealist, inconsolata, inconsolataex,
iosevka, iosevkaex, sharetech, space, syne, triplicate, unispace
### Proportional (Serif)
crimsonpro, etbook, spectral
## Available Themes
amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord,
old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
Themes with light/dark variants: eink, gruvbox, leuchtturm, old-book,
solarized. Set `background = "light"` or use `: background light`.
## Migrating from kge.ini
If you have an existing `kge.ini`, kge will still read it but prints a
notice to stderr suggesting migration. To migrate, create `kge.toml` in the
same directory (`~/.config/kte/`) using the format above. The TOML file
takes priority when both exist.
The INI keys map to TOML as follows:
| INI key | TOML equivalent |
|---------------|--------------------------|
| `fullscreen` | `window.fullscreen` |
| `columns` | `window.columns` |
| `rows` | `window.rows` |
| `font` | `font.name` |
| `font_size` | `font.size` |
| `theme` | `appearance.theme` |
| `background` | `appearance.background` |
| `syntax` | `editor.syntax` |
New keys `font.code` and `font.writing` have no INI equivalent (the INI
parser accepts `code_font` and `writing_font` if needed).

View File

@@ -1335,6 +1335,40 @@ cmd_font_set_size(CommandContext &ctx)
#endif #endif
// Toggle edit mode (code/writing) for current buffer
static bool
cmd_toggle_edit_mode(const CommandContext &ctx)
{
Buffer *b = ctx.editor.CurrentBuffer();
if (!b)
return false;
std::string arg = ctx.arg;
std::transform(arg.begin(), arg.end(), arg.begin(), [](unsigned char c) {
return static_cast<char>(std::tolower(c));
});
// Trim whitespace
auto start = arg.find_first_not_of(" \t");
if (start != std::string::npos)
arg = arg.substr(start);
auto end = arg.find_last_not_of(" \t");
if (end != std::string::npos)
arg = arg.substr(0, end + 1);
if (arg == "code") {
b->SetEditMode(EditMode::Code);
} else if (arg == "writing") {
b->SetEditMode(EditMode::Writing);
} else {
b->ToggleEditMode();
}
const char *mode_str = (b->GetEditMode() == EditMode::Writing) ? "writing" : "code";
ctx.editor.SetStatus(std::string("Mode: ") + mode_str);
return true;
}
// Background set command (GUI, ImGui-only for now) // Background set command (GUI, ImGui-only for now)
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT) #if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
static bool static bool
@@ -1888,15 +1922,15 @@ cmd_insert_text(CommandContext &ctx)
#endif #endif
} }
if (cmd == "font") { if (cmd == "font") {
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
// Complete against installed font families (case-insensitive prefix)
std::vector<std::string> cands; std::vector<std::string> cands;
QStringList fams = QFontDatabase::families();
std::string apfx_lower = argprefix; std::string apfx_lower = argprefix;
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(), std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
[](unsigned char c) { [](unsigned char c) {
return (char) std::tolower(c); return (char) std::tolower(c);
}); });
#if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
// Qt: complete against system font families
QStringList fams = QFontDatabase::families();
for (const auto &fam: fams) { for (const auto &fam: fams) {
std::string n = fam.toStdString(); std::string n = fam.toStdString();
std::string nlower = n; std::string nlower = n;
@@ -1907,6 +1941,13 @@ cmd_insert_text(CommandContext &ctx)
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0) if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
cands.push_back(n); cands.push_back(n);
} }
#elif defined(KTE_BUILD_GUI)
// ImGui: complete against embedded font registry
for (const auto &n : kte::Fonts::FontRegistry::Instance().FontNames()) {
if (apfx_lower.empty() || n.rfind(apfx_lower, 0) == 0)
cands.push_back(n);
}
#endif
if (cands.empty()) { if (cands.empty()) {
// no change // no change
} else if (cands.size() == 1) { } else if (cands.size() == 1) {
@@ -1927,9 +1968,19 @@ cmd_insert_text(CommandContext &ctx)
} }
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true; return true;
#else }
(void) argprefix; if (cmd == "mode") {
#endif std::vector<std::string> modes = {"code", "writing"};
std::vector<std::string> cands;
for (const auto &m : modes) {
if (argprefix.empty() || m.rfind(argprefix, 0) == 0)
cands.push_back(m);
}
if (cands.size() == 1) {
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
}
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
return true;
} }
// default: no special arg completion // default: no special arg completion
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText()); ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
@@ -5025,6 +5076,11 @@ InstallDefaultCommands()
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window, CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
false, false false, false
}); });
// Edit mode toggle (public)
CommandRegistry::Register({
CommandId::ToggleEditMode, "mode", "Toggle or set edit mode: code|writing",
cmd_toggle_edit_mode, true, false
});
} }

View File

@@ -117,6 +117,8 @@ enum class CommandId {
FontZoomIn, FontZoomIn,
FontZoomOut, FontZoomOut,
FontZoomReset, FontZoomReset,
// Edit mode (code/writing)
ToggleEditMode,
}; };

View File

@@ -3,9 +3,29 @@
#include <fstream> #include <fstream>
#include <sstream> #include <sstream>
#include <algorithm> #include <algorithm>
#include <filesystem>
#include <iostream>
#include "GUIConfig.h" #include "GUIConfig.h"
// toml++ for TOML config parsing
#if defined(__clang__)
# pragma clang diagnostic push
# pragma clang diagnostic ignored "-Weverything"
#elif defined(__GNUC__)
# pragma GCC diagnostic push
# pragma GCC diagnostic ignored "-Wall"
# pragma GCC diagnostic ignored "-Wextra"
#endif
#include "ext/tomlplusplus/toml.hpp"
#if defined(__clang__)
# pragma clang diagnostic pop
#elif defined(__GNUC__)
# pragma GCC diagnostic pop
#endif
static void static void
trim(std::string &s) trim(std::string &s)
@@ -19,37 +39,124 @@ trim(std::string &s)
static std::string static std::string
default_config_path() config_dir()
{ {
const char *home = std::getenv("HOME"); const char *home = std::getenv("HOME");
if (!home || !*home) if (!home || !*home)
return {}; return {};
std::string path(home); return std::string(home) + "/.config/kte";
path += "/.config/kte/kge.ini";
return path;
} }
GUIConfig GUIConfig
GUIConfig::Load() GUIConfig::Load()
{ {
GUIConfig cfg; // defaults already set GUIConfig cfg;
const std::string path = default_config_path(); std::string dir = config_dir();
if (dir.empty())
return cfg;
if (!path.empty()) { // Try TOML first
cfg.LoadFromFile(path); std::string toml_path = dir + "/kge.toml";
if (cfg.LoadFromTOML(toml_path))
return cfg;
// Fall back to legacy INI
std::string ini_path = dir + "/kge.ini";
if (cfg.LoadFromINI(ini_path)) {
std::cerr << "kge: loaded legacy kge.ini; consider migrating to kge.toml\n";
return cfg;
} }
return cfg; return cfg;
} }
bool bool
GUIConfig::LoadFromFile(const std::string &path) GUIConfig::LoadFromTOML(const std::string &path)
{
if (!std::filesystem::exists(path))
return false;
toml::table tbl;
try {
tbl = toml::parse_file(path);
} catch (const toml::parse_error &err) {
std::cerr << "kge: TOML parse error in " << path << ": " << err.what() << "\n";
return false;
}
// [window]
if (auto win = tbl["window"].as_table()) {
if (auto v = (*win)["fullscreen"].value<bool>())
fullscreen = *v;
if (auto v = (*win)["columns"].value<int64_t>()) {
if (*v > 0) columns = static_cast<int>(*v);
}
if (auto v = (*win)["rows"].value<int64_t>()) {
if (*v > 0) rows = static_cast<int>(*v);
}
}
// [font]
bool explicit_code_font = false;
bool explicit_writing_font = false;
if (auto sec = tbl["font"].as_table()) {
if (auto v = (*sec)["name"].value<std::string>())
font = *v;
if (auto v = (*sec)["size"].value<double>()) {
if (*v > 0.0) font_size = static_cast<float>(*v);
}
if (auto v = (*sec)["code"].value<std::string>()) {
code_font = *v;
explicit_code_font = true;
}
if (auto v = (*sec)["writing"].value<std::string>()) {
writing_font = *v;
explicit_writing_font = true;
}
}
// [appearance]
if (auto sec = tbl["appearance"].as_table()) {
if (auto v = (*sec)["theme"].value<std::string>())
theme = *v;
if (auto v = (*sec)["background"].value<std::string>()) {
std::string bg = *v;
std::transform(bg.begin(), bg.end(), bg.begin(), [](unsigned char c) {
return (char) std::tolower(c);
});
if (bg == "light" || bg == "dark")
background = bg;
}
}
// [editor]
if (auto sec = tbl["editor"].as_table()) {
if (auto v = (*sec)["syntax"].value<bool>())
syntax = *v;
}
// Default code_font to the main font if not explicitly set
if (!explicit_code_font)
code_font = font;
if (!explicit_writing_font && writing_font == "crimsonpro" && font != "default")
writing_font = font;
return true;
}
bool
GUIConfig::LoadFromINI(const std::string &path)
{ {
std::ifstream in(path); std::ifstream in(path);
if (!in.good()) if (!in.good())
return false; return false;
bool explicit_code_font = false;
bool explicit_writing_font = false;
std::string line; std::string line;
while (std::getline(in, line)) { while (std::getline(in, line)) {
// Remove comments starting with '#' or ';' // Remove comments starting with '#' or ';'
@@ -104,6 +211,12 @@ GUIConfig::LoadFromFile(const std::string &path)
} }
} else if (key == "font") { } else if (key == "font") {
font = val; font = val;
} else if (key == "code_font") {
code_font = val;
explicit_code_font = true;
} else if (key == "writing_font") {
writing_font = val;
explicit_writing_font = true;
} else if (key == "theme") { } else if (key == "theme") {
theme = val; theme = val;
} else if (key == "background" || key == "bg") { } else if (key == "background" || key == "bg") {
@@ -126,5 +239,13 @@ GUIConfig::LoadFromFile(const std::string &path)
} }
} }
// If code_font was not explicitly set, default it to the main font
// so that the edit-mode font switcher doesn't immediately switch away
// from the font loaded during Init.
if (!explicit_code_font)
code_font = font;
if (!explicit_writing_font && writing_font == "crimsonpro" && font != "default")
writing_font = font;
return true; return true;
} }

View File

@@ -1,5 +1,7 @@
/* /*
* GUIConfig - loads simple GUI configuration from $HOME/.config/kte/kge.ini * GUIConfig - loads GUI configuration from $HOME/.config/kte/kge.toml
*
* Falls back to legacy kge.ini if no TOML config is found.
*/ */
#pragma once #pragma once
@@ -22,12 +24,18 @@ public:
std::string background = "dark"; std::string background = "dark";
// Default syntax highlighting state for GUI (kge): on/off // Default syntax highlighting state for GUI (kge): on/off
// Accepts: on/off/true/false/yes/no/1/0 in the ini file. bool syntax = true;
bool syntax = true; // default: enabled
// Load from default path: $HOME/.config/kte/kge.ini // Per-mode font defaults
std::string code_font = "default";
std::string writing_font = "crimsonpro";
// Load from default paths: try kge.toml first, fall back to kge.ini
static GUIConfig Load(); static GUIConfig Load();
// Load from explicit path. Returns true if file existed and was parsed. // Load from explicit TOML path. Returns true if file existed and was parsed.
bool LoadFromFile(const std::string &path); bool LoadFromTOML(const std::string &path);
// Load from explicit INI path (legacy). Returns true if file existed and was parsed.
bool LoadFromINI(const std::string &path);
}; };

View File

@@ -41,6 +41,7 @@ HelpText::Text()
" C-k j Jump to mark\n" " C-k j Jump to mark\n"
" C-k k Center viewport on cursor\n" " C-k k Center viewport on cursor\n"
" C-k l Reload buffer from disk\n" " C-k l Reload buffer from disk\n"
" C-k m Toggle edit mode (code/writing)\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"
" C-k p Next buffer\n" " C-k p Next buffer\n"
@@ -82,12 +83,24 @@ HelpText::Text()
"\n" "\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n" "Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
"\n" "\n"
"GUI appearance (command prompt):\n" "Edit modes:\n"
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n" " code Monospace font (default for source files)\n"
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n" " writing Proportional font (auto for .txt, .md, .rst, .org, .tex)\n"
" C-k m or : mode [code|writing] to toggle\n"
"\n" "\n"
"GUI config file options:\n" "GUI commands (command prompt):\n"
" font_size=NUM Set font size in pixels (default: 16; e.g., font_size=18)\n" " : theme NAME Set theme (amber, eink, everforest, gruvbox,\n"
" kanagawa-paper, lcars, leuchtturm, nord, old-book,\n"
" orbital, plan9, solarized, tufte, weyland-yutani,\n"
" zenburn)\n"
" : background MODE Background: light | dark\n"
" : font NAME Set font (tab completes)\n"
" : font-size NUM Set font size in pixels\n"
" : mode [code|writing] Toggle or set edit mode\n"
"\n"
"Configuration:\n"
" Config file: ~/.config/kte/kge.toml (see CONFIG.md)\n"
" Legacy kge.ini is also supported.\n"
"\n" "\n"
"GUI window management:\n" "GUI window management:\n"
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n" " Cmd+N (macOS) Open a new editor window sharing the same buffers\n"

View File

@@ -38,6 +38,11 @@ apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
{ {
if (!b) if (!b)
return; return;
// Auto-detect edit mode from file extension
if (!b->Filename().empty())
b->SetEditMode(DetectEditMode(b->Filename()));
if (cfg.syntax) { if (cfg.syntax) {
b->SetSyntaxEnabled(true); b->SetSyntaxEnabled(true);
b->EnsureHighlighter(); b->EnsureHighlighter();
@@ -71,7 +76,9 @@ static void
update_editor_dimensions(Editor &ed, float disp_w, float disp_h) update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
{ {
float row_h = ImGui::GetTextLineHeightWithSpacing(); float row_h = ImGui::GetTextLineHeightWithSpacing();
float ch_w = ImGui::CalcTextSize("M").x; // Use average character width rather than "M" (the widest character)
// so that column count is reasonable for proportional fonts too.
float ch_w = ImGui::CalcTextSize("abcdefghijklmnopqrstuvwxyz").x / 26.0f;
if (row_h <= 0.0f) if (row_h <= 0.0f)
row_h = 16.0f; row_h = 16.0f;
if (ch_w <= 0.0f) if (ch_w <= 0.0f)
@@ -518,6 +525,9 @@ GUIFrontend::Step(Editor &ed, bool &running)
// Allow deferred opens // Allow deferred opens
wed.ProcessPendingOpens(); wed.ProcessPendingOpens();
// Ensure newly opened buffers get syntax + edit mode detection
apply_syntax_to_buffer(wed.CurrentBuffer(), config_);
// Drain input queue // Drain input queue
for (;;) { for (;;) {
MappedInput mi; MappedInput mi;
@@ -557,6 +567,23 @@ GUIFrontend::Step(Editor &ed, bool &running)
running = false; running = false;
} }
// Switch font based on current buffer's edit mode (deferred to next frame)
{
Buffer *cur = wed.CurrentBuffer();
if (cur) {
auto &fr = kte::Fonts::FontRegistry::Instance();
const std::string &expected =
(cur->GetEditMode() == EditMode::Writing)
? config_.writing_font
: config_.code_font;
if (fr.CurrentFontName() != expected && fr.HasFont(expected)) {
float sz = fr.CurrentFontSize();
if (sz <= 0.0f) sz = config_.font_size;
fr.RequestLoadFont(expected, sz);
}
}
}
// Draw // Draw
ws.renderer.Draw(wed); ws.renderer.Draw(wed);

View File

@@ -85,11 +85,9 @@ ImGuiRenderer::Draw(Editor &ed)
float target_y = static_cast<float>(buf_rowoffs) * row_h; float target_y = static_cast<float>(buf_rowoffs) * row_h;
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y)); ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
} }
if (prev_buf_coloffs_ >= 0 && buf_coloffs != prev_buf_coloffs_) { // Horizontal scroll is handled purely in pixel space (see
float target_x = static_cast<float>(buf_coloffs) * space_w; // cursor-visibility block after the line loop) so we don't
float target_y = static_cast<float>(buf_rowoffs) * row_h; // convert the character-based coloffs to an ImGui scroll here.
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
}
// Reserve space for status bar at bottom. // Reserve space for status bar at bottom.
// We calculate a height that is an exact multiple of the line height // We calculate a height that is an exact multiple of the line height
@@ -114,26 +112,21 @@ ImGuiRenderer::Draw(Editor &ed)
bool forced_scroll = false; bool forced_scroll = false;
{ {
const long scroll_top = static_cast<long>(scroll_y / row_h); const long scroll_top = static_cast<long>(scroll_y / row_h);
const long scroll_left = static_cast<long>(scroll_x / space_w);
// Check if rowoffs was programmatically changed this frame // Check if rowoffs was programmatically changed this frame
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) { if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
forced_scroll = true; forced_scroll = true;
} }
// If user scrolled (not programmatic), update buffer offsets accordingly // If user scrolled vertically (not programmatic), update buffer row offset
if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) { if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) { if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)), mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
mbuf->Coloffs()); mbuf->Coloffs());
} }
} }
if (prev_scroll_x_ >= 0.0f && scroll_x != prev_scroll_x_ && !forced_scroll) { // Horizontal scroll is pixel-based and managed by the cursor
if (Buffer *mbuf = const_cast<Buffer *>(buf)) { // visibility block below; we don't sync it back to coloffs.
mbuf->SetOffsets(mbuf->Rowoffs(),
static_cast<std::size_t>(std::max(0L, scroll_left)));
}
}
// Update trackers for next frame // Update trackers for next frame
prev_scroll_y_ = scroll_y; prev_scroll_y_ = scroll_y;
@@ -141,8 +134,8 @@ ImGuiRenderer::Draw(Editor &ed)
} }
prev_buf_rowoffs_ = buf_rowoffs; prev_buf_rowoffs_ = buf_rowoffs;
prev_buf_coloffs_ = buf_coloffs; prev_buf_coloffs_ = buf_coloffs;
// Cache current horizontal offset in rendered columns for click handling // Track the widest line in pixels for ImGui content width
const std::size_t coloffs_now = buf->Coloffs(); float max_line_width_px = 0.0f;
// Mark selection state (mark -> cursor), in source coordinates // Mark selection state (mark -> cursor), in source coordinates
bool sel_active = false; bool sel_active = false;
@@ -175,29 +168,54 @@ ImGuiRenderer::Draw(Editor &ed)
if (by >= lines.size()) if (by >= lines.size())
by = lines.empty() ? 0 : (lines.size() - 1); by = lines.empty() ? 0 : (lines.size() - 1);
// Convert mouse pos to rendered x if (lines.empty())
return {0, 0};
// Expand tabs for the clicked line
std::string line_clicked = static_cast<std::string>(lines[by]);
const std::size_t tabw = 8;
std::string click_expanded;
click_expanded.reserve(line_clicked.size() + 16);
std::size_t click_rx = 0;
// Map: source column -> expanded column
std::vector<std::size_t> src_to_exp;
src_to_exp.reserve(line_clicked.size() + 1);
for (std::size_t ci = 0; ci < line_clicked.size(); ++ci) {
src_to_exp.push_back(click_rx);
if (line_clicked[ci] == '\t') {
std::size_t adv = (tabw - (click_rx % tabw));
click_expanded.append(adv, ' ');
click_rx += adv;
} else {
click_expanded.push_back(line_clicked[ci]);
click_rx += 1;
}
}
src_to_exp.push_back(click_rx); // past-end position
// Pixel x relative to the line start (accounting for scroll)
float visual_x = mp.x - child_window_pos.x; float visual_x = mp.x - child_window_pos.x;
if (visual_x < 0.0f) if (visual_x < 0.0f)
visual_x = 0.0f; visual_x = 0.0f;
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now; // Add scroll offset in pixels
visual_x += scroll_x;
// Convert rendered column to source column // Find the source column whose expanded position is closest
if (lines.empty()) // to the click pixel, using actual text measurement.
return {0, 0}; std::size_t best_col = 0;
std::string line_clicked = static_cast<std::string>(lines[by]); float best_dist = std::numeric_limits<float>::infinity();
const std::size_t tabw = 8; for (std::size_t ci = 0; ci <= line_clicked.size(); ++ci) {
std::size_t rx = 0; std::size_t exp_col = src_to_exp[ci];
std::size_t best_col = 0; float px = 0.0f;
float best_dist = std::numeric_limits<float>::infinity(); if (exp_col > 0 && !click_expanded.empty()) {
float clicked_rx_f = static_cast<float>(clicked_rx); std::size_t end = std::min(click_expanded.size(), exp_col);
for (std::size_t i = 0; i <= line_clicked.size(); ++i) { px = ImGui::CalcTextSize(click_expanded.c_str(),
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx)); click_expanded.c_str() + end).x;
}
float dist = std::fabs(visual_x - px);
if (dist < best_dist) { if (dist < best_dist) {
best_dist = dist; best_dist = dist;
best_col = i; best_col = ci;
}
if (i < line_clicked.size()) {
rx += (line_clicked[i] == '\t') ? (tabw - (rx % tabw)) : 1;
} }
} }
return {by, best_col}; return {by, best_col};
@@ -244,11 +262,34 @@ ImGuiRenderer::Draw(Editor &ed)
ImVec2 line_pos = ImGui::GetCursorScreenPos(); ImVec2 line_pos = ImGui::GetCursorScreenPos();
std::string line = static_cast<std::string>(lines[i]); std::string line = static_cast<std::string>(lines[i]);
// Expand tabs to spaces with width=8 and apply horizontal scroll offset // Expand tabs to spaces with width=8
const std::size_t tabw = 8; const std::size_t tabw = 8;
std::string expanded; std::string expanded;
expanded.reserve(line.size() + 16); expanded.reserve(line.size() + 16);
std::size_t rx_abs_draw = 0; // rendered column for drawing std::size_t rx_abs_draw = 0;
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Helper: convert a rendered column position to an absolute
// pixel x offset from the start of the line. ImGui's scroll
// handles viewport clipping so we measure from column 0.
auto rx_to_px = [&](std::size_t rx_col) -> float {
std::size_t end = std::min(expanded.size(), rx_col);
if (end == 0)
return 0.0f;
return ImGui::CalcTextSize(expanded.c_str(),
expanded.c_str() + end).x;
};
// 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;
@@ -300,13 +341,8 @@ ImGuiRenderer::Draw(Editor &ed)
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 ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start), line_pos.y);
if (rx_end <= coloffs_now) ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
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); 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;
@@ -343,18 +379,12 @@ ImGuiRenderer::Draw(Editor &ed)
if (line_has) { if (line_has) {
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);
if (rx_end > coloffs_now) { ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
std::size_t vx0 = (rx_start > coloffs_now) line_pos.y);
? (rx_start - coloffs_now) ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
: 0; line_pos.y + line_h);
std::size_t vx1 = rx_end - coloffs_now; ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
} }
} }
if (vsel_active && i >= vsel_sy && i <= vsel_ey) { if (vsel_active && i >= vsel_sy && i <= vsel_ey) {
@@ -368,32 +398,13 @@ ImGuiRenderer::Draw(Editor &ed)
// EOL spot: draw a 1-cell highlight just past the last character. // EOL spot: draw a 1-cell highlight just past the last character.
rx_end = rx_start + 1; rx_end = rx_start + 1;
} }
if (rx_end > coloffs_now) { ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
std::size_t vx0 = (rx_start > coloffs_now) line_pos.y);
? (rx_start - coloffs_now) ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
: 0; line_pos.y + line_h);
std::size_t vx1 = rx_end - coloffs_now; ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w, ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
line_pos.y);
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
line_pos.y + line_h);
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
}
} }
// Emit entire line to an expanded buffer (tabs -> spaces)
for (std::size_t src = 0; src < line.size(); ++src) {
char c = line[src];
if (c == '\t') {
std::size_t adv = (tabw - (rx_abs_draw % tabw));
expanded.append(adv, ' ');
rx_abs_draw += adv;
} else {
expanded.push_back(c);
rx_abs_draw += 1;
}
}
// Draw syntax-colored runs (text above background highlights) // Draw syntax-colored runs (text above background highlights)
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) { if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->HasHighlighter()) {
kte::LineHighlight lh = buf->Highlighter()->GetLine( kte::LineHighlight lh = buf->Highlighter()->GetLine(
@@ -436,19 +447,14 @@ ImGuiRenderer::Draw(Editor &ed)
for (const auto &sp: spans) { for (const auto &sp: spans) {
std::size_t rx_s = src_to_rx_full(sp.s); std::size_t rx_s = src_to_rx_full(sp.s);
std::size_t rx_e = src_to_rx_full(sp.e); std::size_t rx_e = src_to_rx_full(sp.e);
if (rx_e <= coloffs_now) std::size_t draw_start = rx_s;
continue; // fully left of viewport
// Clamp to visible portion and expanded length
std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now;
if (draw_start >= expanded.size()) if (draw_start >= expanded.size())
continue; // fully right of expanded text continue;
std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size()); std::size_t draw_end = std::min<std::size_t>(rx_e, expanded.size());
if (draw_end <= draw_start) if (draw_end <= draw_start)
continue; continue;
// Screen position is relative to coloffs_now
std::size_t screen_x = draw_start - coloffs_now;
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k)); ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.k));
ImVec2 p = ImVec2(line_pos.x + static_cast<float>(screen_x) * space_w, ImVec2 p = ImVec2(line_pos.x + rx_to_px(draw_start),
line_pos.y); line_pos.y);
ImGui::GetWindowDrawList()->AddText( ImGui::GetWindowDrawList()->AddText(
p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end); p, col, expanded.c_str() + draw_start, expanded.c_str() + draw_end);
@@ -457,48 +463,37 @@ ImGuiRenderer::Draw(Editor &ed)
// Use row_h (with spacing) to match click calculation and ensure consistent line positions. // Use row_h (with spacing) to match click calculation and ensure consistent line positions.
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h)); ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} else { } else {
// No syntax: draw as one run, accounting for horizontal scroll offset // No syntax: draw the full line; ImGui scroll handles clipping.
if (coloffs_now < expanded.size()) { if (!expanded.empty()) {
ImVec2 p = ImVec2(line_pos.x, line_pos.y); ImVec2 p = ImVec2(line_pos.x, line_pos.y);
ImGui::GetWindowDrawList()->AddText( ImGui::GetWindowDrawList()->AddText(
p, ImGui::GetColorU32(ImGuiCol_Text), p, ImGui::GetColorU32(ImGuiCol_Text),
expanded.c_str() + coloffs_now); expanded.c_str());
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} else {
// Line is fully scrolled out of view horizontally
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} }
ImGui::SetCursorScreenPos(ImVec2(line_pos.x, line_pos.y + row_h));
} }
// Draw a visible cursor indicator on the current line // Draw a visible cursor indicator on the current line
if (i == cy) { if (i == cy) {
// Compute rendered X (rx) from source column with tab expansion std::size_t rx_abs = src_to_rx(cx);
std::size_t rx_abs = 0; float cursor_px = rx_to_px(rx_abs);
for (std::size_t k = 0; k < std::min(cx, line.size()); ++k) {
if (line[k] == '\t')
rx_abs += (tabw - (rx_abs % tabw));
else
rx_abs += 1;
}
// Convert to viewport x by subtracting horizontal col offset
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
// For proportional fonts (Linux GUI), avoid accumulating drift by computing
// the exact pixel width of the expanded substring up to the cursor.
// expanded contains the line with tabs expanded to spaces and is what we draw.
float cursor_px = 0.0f;
if (rx_viewport > 0 && coloffs_now < expanded.size()) {
std::size_t start = coloffs_now;
std::size_t end = std::min(expanded.size(), start + rx_viewport);
// Measure substring width in pixels
ImVec2 sz = ImGui::CalcTextSize(expanded.c_str() + start,
expanded.c_str() + end);
cursor_px = sz.x;
}
ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y); ImVec2 p0 = ImVec2(line_pos.x + cursor_px, line_pos.y);
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h); ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col); ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
} }
// Track widest line for content width reporting
if (!expanded.empty()) {
float line_w = ImGui::CalcTextSize(expanded.c_str()).x;
if (line_w > max_line_width_px)
max_line_width_px = line_w;
}
}
// Report content width to ImGui so horizontal scrollbar works correctly.
if (max_line_width_px > 0.0f) {
ImGui::SetCursorPosX(max_line_width_px);
ImGui::Dummy(ImVec2(0, 0));
} }
// Synchronize cursor and scrolling after rendering all lines so content size is known. // Synchronize cursor and scrolling after rendering all lines so content size is known.
{ {
@@ -539,29 +534,40 @@ ImGuiRenderer::Draw(Editor &ed)
last_row = first_row + vis_rows - 1; last_row = first_row + vis_rows - 1;
} }
// Horizontal scroll: ensure cursor column is visible // Horizontal scroll: ensure cursor is visible (pixel-based for proportional fonts)
long vis_cols = static_cast<long>(std::round(child_w_actual / space_w)); float cursor_px_abs = 0.0f;
if (vis_cols < 1)
vis_cols = 1;
long first_col = static_cast<long>(scroll_x_now / space_w);
long last_col = first_col + vis_cols - 1;
std::size_t cursor_rx = 0;
if (cy < lines.size()) { if (cy < lines.size()) {
std::string cur_line = static_cast<std::string>(lines[cy]); std::string cur_line = static_cast<std::string>(lines[cy]);
const std::size_t tabw = 8; const std::size_t tabw = 8;
for (std::size_t i = 0; i < cx && i < cur_line.size(); ++i) { // Expand tabs for cursor line to measure pixel position
if (cur_line[i] == '\t') { std::string cur_expanded;
cursor_rx += tabw - (cursor_rx % tabw); cur_expanded.reserve(cur_line.size() + 16);
std::size_t cur_rx = 0;
for (std::size_t ci = 0; ci < cur_line.size(); ++ci) {
if (cur_line[ci] == '\t') {
std::size_t adv = tabw - (cur_rx % tabw);
cur_expanded.append(adv, ' ');
cur_rx += adv;
} else { } else {
cursor_rx += 1; cur_expanded.push_back(cur_line[ci]);
cur_rx += 1;
} }
} }
// Compute rendered column of cursor
std::size_t cursor_rx = 0;
for (std::size_t ci = 0; ci < cx && ci < cur_line.size(); ++ci) {
if (cur_line[ci] == '\t')
cursor_rx += tabw - (cursor_rx % tabw);
else
cursor_rx += 1;
}
std::size_t exp_end = std::min(cur_expanded.size(), cursor_rx);
if (exp_end > 0)
cursor_px_abs = ImGui::CalcTextSize(cur_expanded.c_str(),
cur_expanded.c_str() + exp_end).x;
} }
long cxr = static_cast<long>(cursor_rx); if (cursor_px_abs < scroll_x_now || cursor_px_abs > scroll_x_now + child_w_actual) {
if (cxr < first_col || cxr > last_col) { float target_x = cursor_px_abs - (child_w_actual / 2.0f);
float target_x = static_cast<float>(cxr) * space_w;
target_x -= (child_w_actual / 2.0f);
if (target_x < 0.f) if (target_x < 0.f)
target_x = 0.f; target_x = 0.f;
float max_x = ImGui::GetScrollMaxX(); float max_x = ImGui::GetScrollMaxX();

View File

@@ -84,6 +84,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
case 'l': case 'l':
out = CommandId::ReloadBuffer; out = CommandId::ReloadBuffer;
return true; return true;
case 'm':
out = CommandId::ToggleEditMode;
return true;
case 'n': case 'n':
out = CommandId::BufferPrev; out = CommandId::BufferPrev;
return true; return true;

View File

@@ -1,6 +1,5 @@
{ {
pkgs ? import <nixpkgs> {}, lib,
lib ? pkgs.lib,
stdenv, stdenv,
cmake, cmake,
ncurses, ncurses,
@@ -10,9 +9,10 @@
kdePackages, kdePackages,
qt6Packages ? kdePackages.qt6Packages, qt6Packages ? kdePackages.qt6Packages,
installShellFiles, installShellFiles,
copyDesktopItems,
makeDesktopItem,
graphical ? false, graphical ? false,
graphical-qt ? false, graphical-qt ? false,
...
}: }:
let let
cmakeContent = builtins.readFile ./CMakeLists.txt; cmakeContent = builtins.readFile ./CMakeLists.txt;
@@ -23,25 +23,29 @@ let
version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine); version = builtins.head (builtins.match ".*set\\(KTE_VERSION \"(.+)\"\\).*" versionLine);
in in
stdenv.mkDerivation { stdenv.mkDerivation {
pname = "kte"; pname = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte";
inherit version; inherit version;
src = lib.cleanSource ./.; src = lib.cleanSource ./.;
nativeBuildInputs = [ nativeBuildInputs = [
cmake cmake
ncurses
installShellFiles installShellFiles
] ] ++ lib.optionals graphical [
++ lib.optionals graphical [ copyDesktopItems
] ++ lib.optionals graphical-qt [
qt6Packages.wrapQtAppsHook
];
buildInputs = [
ncurses
] ++ lib.optionals graphical [
SDL2 SDL2
libGL libGL
xorg.libX11 xorg.libX11
] ] ++ lib.optionals graphical-qt [
++ lib.optionals graphical-qt [
kdePackages.qt6ct kdePackages.qt6ct
qt6Packages.qtbase qt6Packages.qtbase
qt6Packages.wrapQtAppsHook
]; ];
cmakeFlags = [ cmakeFlags = [
@@ -51,6 +55,30 @@ stdenv.mkDerivation {
"-DKTE_STATIC_LINK=OFF" "-DKTE_STATIC_LINK=OFF"
]; ];
desktopItems = lib.optionals graphical [
(makeDesktopItem {
name = "kge";
desktopName = "kge";
genericName = "Text Editor";
comment = "kyle's graphical text editor";
exec = if graphical-qt then "kge-qt %F" else "kge %F";
icon = "kge";
terminal = false;
categories = [ "Utility" "TextEditor" "Development" ];
mimeTypes = [
"text/plain"
"text/x-c"
"text/x-c++"
"text/x-python"
"text/x-go"
"text/x-rust"
"application/json"
"text/markdown"
"text/x-shellscript"
];
})
];
installPhase = '' installPhase = ''
runHook preInstall runHook preInstall
@@ -59,14 +87,11 @@ stdenv.mkDerivation {
installManPage ../docs/kte.1 installManPage ../docs/kte.1
${lib.optionalString graphical '' ${lib.optionalString graphical ''
mkdir -p $out/bin ${if graphical-qt then ''
${if graphical-qt then ''
cp kge $out/bin/kge-qt cp kge $out/bin/kge-qt
'' else '' '' else ''
cp kge $out/bin/kge cp kge $out/bin/kge
''} ''}
installManPage ../docs/kge.1 installManPage ../docs/kge.1
mkdir -p $out/share/icons/hicolor/256x256/apps mkdir -p $out/share/icons/hicolor/256x256/apps
@@ -75,4 +100,10 @@ stdenv.mkDerivation {
runHook postInstall runHook postInstall
''; '';
meta = {
description = "kyle's text editor" + lib.optionalString graphical " (graphical)";
platforms = lib.platforms.unix;
mainProgram = if graphical then (if graphical-qt then "kge-qt" else "kge") else "kte";
};
} }

View File

@@ -23,29 +23,34 @@ Current themes (alphabetically):
- **gruvbox** — Retro groove color scheme (light/dark variants) - **gruvbox** — Retro groove color scheme (light/dark variants)
- **kanagawa-paper** — Inspired by traditional Japanese art - **kanagawa-paper** — Inspired by traditional Japanese art
- **lcars** — Star Trek LCARS interface style - **lcars** — Star Trek LCARS interface style
- **leuchtturm** — Modern, clean theme (light/dark variants)
- **nord** — Arctic, north-bluish color palette - **nord** — Arctic, north-bluish color palette
- **old-book** — Sepia-toned vintage book aesthetic (light/dark - **old-book** — Sepia-toned vintage book aesthetic (light/dark
variants) variants)
- **orbital** — Space-themed dark palette - **orbital** — Space-themed dark palette
- **plan9** — Minimalist Plan 9 from Bell Labs inspired - **plan9** — Minimalist Plan 9 from Bell Labs inspired
- **solarized** — Ethan Schoonover's Solarized (light/dark variants) - **solarized** — Ethan Schoonover's Solarized (light/dark variants)
- **tufte** — Edward Tufte-inspired minimalist theme (light/dark variants)
- **weyland-yutani** — Alien franchise corporate aesthetic - **weyland-yutani** — Alien franchise corporate aesthetic
- **zenburn** — Low-contrast, easy-on-the-eyes theme - **zenburn** — Low-contrast, easy-on-the-eyes theme
Configuration Configuration
------------- -------------
Themes are configured via `$HOME/.config/kte/kge.ini`: Themes are configured via `$HOME/.config/kte/kge.toml`:
```ini ```toml
theme = nord [appearance]
background = dark theme = "nord"
background = "dark"
``` ```
- `theme` — The theme name (e.g., "nord", "gruvbox", "solarized") - `theme` — The theme name (e.g., "nord", "gruvbox", "solarized")
- `background` — Either "dark" or "light" (for themes supporting both - `background` — Either "dark" or "light" (for themes supporting both
variants) variants)
Legacy `kge.ini` format is also supported (see CONFIG.md).
Themes can also be switched at runtime using the `:theme <name>` Themes can also be switched at runtime using the `:theme <name>`
command. command.

17748
ext/tomlplusplus/toml.hpp Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,7 @@
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
outputs = outputs = { self, nixpkgs, ... }:
inputs@{ self, nixpkgs, ... }:
let let
eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed; eachSystem = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
pkgsFor = system: import nixpkgs { inherit system; }; pkgsFor = system: import nixpkgs { inherit system; };
@@ -17,5 +16,27 @@
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; }; kge = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = false; };
qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; }; qt = (pkgsFor system).callPackage ./default.nix { graphical = true; graphical-qt = true; };
}); });
devShells = eachSystem (system:
let pkgs = pkgsFor system;
in {
default = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.kge ];
packages = with pkgs; [ gdb valgrind ];
};
terminal = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.kte ];
};
qt = pkgs.mkShell {
inputsFrom = [ self.packages.${system}.qt ];
packages = with pkgs; [ gdb valgrind ];
};
}
);
overlays.default = final: prev: {
kte = self.packages.${final.system}.kte;
kge = self.packages.${final.system}.kge;
};
}; };
} }

1768
fonts/CrimsonPro.h Normal file

File diff suppressed because it is too large Load Diff

1203
fonts/ETBook.h Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -3,6 +3,8 @@
#include "BerkeleyMono.h" #include "BerkeleyMono.h"
#include "BrassMono.h" #include "BrassMono.h"
#include "BrassMonoCode.h" #include "BrassMonoCode.h"
#include "CrimsonPro.h"
#include "ETBook.h"
#include "FiraCode.h" #include "FiraCode.h"
#include "Go.h" #include "Go.h"
#include "IBMPlexMono.h" #include "IBMPlexMono.h"
@@ -13,6 +15,7 @@
#include "IosevkaExtended.h" #include "IosevkaExtended.h"
#include "ShareTech.h" #include "ShareTech.h"
#include "SpaceMono.h" #include "SpaceMono.h"
#include "Spectral.h"
#include "Syne.h" #include "Syne.h"
#include "Triplicate.h" #include "Triplicate.h"
#include "Unispace.h" #include "Unispace.h"

View File

@@ -45,6 +45,16 @@ InstallDefaultFonts()
BrassMonoCode::DefaultFontBoldCompressedData, BrassMonoCode::DefaultFontBoldCompressedData,
BrassMonoCode::DefaultFontBoldCompressedSize BrassMonoCode::DefaultFontBoldCompressedSize
)); ));
FontRegistry::Instance().Register(std::make_unique<Font>(
"crimsonpro",
CrimsonPro::DefaultFontRegularCompressedData,
CrimsonPro::DefaultFontRegularCompressedSize
));
FontRegistry::Instance().Register(std::make_unique<Font>(
"etbook",
ETBook::DefaultFontRegularCompressedData,
ETBook::DefaultFontRegularCompressedSize
));
FontRegistry::Instance().Register(std::make_unique<Font>( FontRegistry::Instance().Register(std::make_unique<Font>(
"fira", "fira",
FiraCode::DefaultFontRegularCompressedData, FiraCode::DefaultFontRegularCompressedData,
@@ -95,6 +105,11 @@ InstallDefaultFonts()
SpaceMono::DefaultFontRegularCompressedData, SpaceMono::DefaultFontRegularCompressedData,
SpaceMono::DefaultFontRegularCompressedSize SpaceMono::DefaultFontRegularCompressedSize
)); ));
FontRegistry::Instance().Register(std::make_unique<Font>(
"spectral",
Spectral::DefaultFontRegularCompressedData,
Spectral::DefaultFontRegularCompressedSize
));
FontRegistry::Instance().Register(std::make_unique<Font>( FontRegistry::Instance().Register(std::make_unique<Font>(
"syne", "syne",
Syne::DefaultFontRegularCompressedData, Syne::DefaultFontRegularCompressedData,

View File

@@ -1,10 +1,12 @@
#pragma once #pragma once
#include <algorithm>
#include <cassert> #include <cassert>
#include <memory> #include <memory>
#include <mutex> #include <mutex>
#include <string> #include <string>
#include <unordered_map> #include <unordered_map>
#include <vector>
#include "Font.h" #include "Font.h"
@@ -87,6 +89,19 @@ public:
} }
// Return all registered font names (sorted)
std::vector<std::string> FontNames() const
{
std::lock_guard lock(mutex_);
std::vector<std::string> names;
names.reserve(fonts_.size());
for (const auto &[name, _] : fonts_)
names.push_back(name);
std::sort(names.begin(), names.end());
return names;
}
// Current font name/size as last successfully loaded via LoadFont() // Current font name/size as last successfully loaded via LoadFont()
std::string CurrentFontName() const std::string CurrentFontName() const
{ {

3227
fonts/Spectral.h Normal file

File diff suppressed because it is too large Load Diff

24
kge.toml.example Normal file
View File

@@ -0,0 +1,24 @@
# kge configuration
# Place at ~/.config/kte/kge.toml
[window]
fullscreen = false
columns = 80
rows = 42
[font]
# Default font and size
name = "default"
size = 18.0
# Font used in code mode (monospace)
code = "default"
# Font used in writing mode (proportional) — for .txt, .md, .rst, .org, .tex, etc.
writing = "crimsonpro"
[appearance]
theme = "nord"
# "dark" or "light" for themes with variants
background = "dark"
[editor]
syntax = true

View File

@@ -15,20 +15,18 @@ sha256sum kge.app.zip
open . open .
cd .. cd ..
mkdir -p cmake-build-release-qt # Qt build disabled — ImGui frontend is the primary GUI.
cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF # mkdir -p cmake-build-release-qt
# cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
cd cmake-build-release-qt #
make clean # cd cmake-build-release-qt
rm -fr kge.app* kge-qt.app* # make clean
make # rm -fr kge.app* kge-qt.app*
mv -f kge.app kge-qt.app # make
# Use the same Qt's macdeployqt as used for building; ensure it overwrites in-bundle paths # mv -f kge.app kge-qt.app
macdeployqt kge-qt.app -always-overwrite -verbose=3 # macdeployqt kge-qt.app -always-overwrite -verbose=3
# cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
# Run CMake BundleUtilities fixup to internalize non-Qt dylibs and rewrite install names # zip -r kge-qt.app.zip kge-qt.app
cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake" # sha256sum kge-qt.app.zip
zip -r kge-qt.app.zip kge-qt.app # open .
sha256sum kge-qt.app.zip # cd ..
open .
cd ..

View File

@@ -22,5 +22,6 @@ fi
git tag "${KTE_VERSION}" git tag "${KTE_VERSION}"
git push && git push --tags git push && git push --tags
git push github && git push github --tags
( ./make-app-release ) ( ./make-app-release )