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>
This commit is contained in:
47
Buffer.h
47
Buffer.h
@@ -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;
|
||||||
|
|||||||
@@ -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.10.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.
|
||||||
@@ -205,6 +205,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 +218,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
116
CONFIG.md
Normal 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).
|
||||||
68
Command.cc
68
Command.cc
@@ -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
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -117,6 +117,8 @@ enum class CommandId {
|
|||||||
FontZoomIn,
|
FontZoomIn,
|
||||||
FontZoomOut,
|
FontZoomOut,
|
||||||
FontZoomReset,
|
FontZoomReset,
|
||||||
|
// Edit mode (code/writing)
|
||||||
|
ToggleEditMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
114
GUIConfig.cc
114
GUIConfig.cc
@@ -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,32 +39,104 @@ 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]
|
||||||
|
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;
|
||||||
|
if (auto v = (*sec)["writing"].value<std::string>())
|
||||||
|
writing_font = *v;
|
||||||
|
}
|
||||||
|
|
||||||
|
// [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;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
GUIConfig::LoadFromINI(const std::string &path)
|
||||||
{
|
{
|
||||||
std::ifstream in(path);
|
std::ifstream in(path);
|
||||||
if (!in.good())
|
if (!in.good())
|
||||||
@@ -104,6 +196,10 @@ 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;
|
||||||
|
} else if (key == "writing_font") {
|
||||||
|
writing_font = val;
|
||||||
} else if (key == "theme") {
|
} else if (key == "theme") {
|
||||||
theme = val;
|
theme = val;
|
||||||
} else if (key == "background" || key == "bg") {
|
} else if (key == "background" || key == "bg") {
|
||||||
|
|||||||
20
GUIConfig.h
20
GUIConfig.h
@@ -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);
|
||||||
};
|
};
|
||||||
|
|||||||
23
HelpText.cc
23
HelpText.cc
@@ -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"
|
||||||
|
|||||||
@@ -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();
|
||||||
@@ -518,6 +523,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 +565,25 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
running = false;
|
running = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switch font based on current buffer's edit mode
|
||||||
|
{
|
||||||
|
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.LoadFont(expected, sz);
|
||||||
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw
|
// Draw
|
||||||
ws.renderer.Draw(wed);
|
ws.renderer.Draw(wed);
|
||||||
|
|
||||||
|
|||||||
192
ImGuiRenderer.cc
192
ImGuiRenderer.cc
@@ -175,29 +175,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::string line_clicked = static_cast<std::string>(lines[by]);
|
|
||||||
const std::size_t tabw = 8;
|
|
||||||
std::size_t rx = 0;
|
|
||||||
std::size_t best_col = 0;
|
std::size_t best_col = 0;
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
for (std::size_t ci = 0; ci <= line_clicked.size(); ++ci) {
|
||||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
std::size_t exp_col = src_to_exp[ci];
|
||||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
float px = 0.0f;
|
||||||
|
if (exp_col > 0 && !click_expanded.empty()) {
|
||||||
|
std::size_t end = std::min(click_expanded.size(), exp_col);
|
||||||
|
px = ImGui::CalcTextSize(click_expanded.c_str(),
|
||||||
|
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 +269,37 @@ 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 pixel x offset
|
||||||
|
// relative to the visible line start, using actual text measurement
|
||||||
|
// so proportional fonts render correctly.
|
||||||
|
auto rx_to_px = [&](std::size_t rx_col) -> float {
|
||||||
|
if (rx_col <= coloffs_now)
|
||||||
|
return 0.0f;
|
||||||
|
std::size_t start = coloffs_now;
|
||||||
|
std::size_t end = std::min(expanded.size(), rx_col);
|
||||||
|
if (start >= expanded.size() || end <= start)
|
||||||
|
return 0.0f;
|
||||||
|
return ImGui::CalcTextSize(expanded.c_str() + start,
|
||||||
|
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;
|
||||||
@@ -303,10 +354,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Apply horizontal scroll offset
|
// Apply horizontal scroll offset
|
||||||
if (rx_end <= coloffs_now)
|
if (rx_end <= coloffs_now)
|
||||||
continue; // fully left of view
|
continue; // fully left of view
|
||||||
std::size_t vx0 = (rx_start > coloffs_now) ? (rx_start - coloffs_now) : 0;
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start), line_pos.y);
|
||||||
std::size_t vx1 = rx_end - coloffs_now;
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
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;
|
||||||
@@ -344,13 +393,9 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
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) {
|
if (rx_end > coloffs_now) {
|
||||||
std::size_t vx0 = (rx_start > coloffs_now)
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
||||||
? (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);
|
line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
@@ -369,31 +414,14 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
rx_end = rx_start + 1;
|
rx_end = rx_start + 1;
|
||||||
}
|
}
|
||||||
if (rx_end > coloffs_now) {
|
if (rx_end > coloffs_now) {
|
||||||
std::size_t vx0 = (rx_start > coloffs_now)
|
ImVec2 p0 = ImVec2(line_pos.x + rx_to_px(rx_start),
|
||||||
? (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);
|
line_pos.y);
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
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(
|
||||||
@@ -445,10 +473,9 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
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
|
// Screen position via actual text measurement
|
||||||
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);
|
||||||
@@ -472,28 +499,8 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
|
|
||||||
// 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
|
||||||
@@ -539,29 +546,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 {
|
||||||
|
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;
|
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;
|
||||||
}
|
}
|
||||||
}
|
if (cursor_px_abs < scroll_x_now || cursor_px_abs > scroll_x_now + child_w_actual) {
|
||||||
long cxr = static_cast<long>(cursor_rx);
|
float target_x = cursor_px_abs - (child_w_actual / 2.0f);
|
||||||
if (cxr < first_col || cxr > last_col) {
|
|
||||||
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();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
17748
ext/tomlplusplus/toml.hpp
Normal file
File diff suppressed because it is too large
Load Diff
1768
fonts/CrimsonPro.h
Normal file
1768
fonts/CrimsonPro.h
Normal file
File diff suppressed because it is too large
Load Diff
1203
fonts/ETBook.h
Normal file
1203
fonts/ETBook.h
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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
3227
fonts/Spectral.h
Normal file
File diff suppressed because it is too large
Load Diff
24
kge.toml.example
Normal file
24
kge.toml.example
Normal 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
|
||||||
@@ -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 )
|
||||||
Reference in New Issue
Block a user