Compare commits

...

9 Commits

Author SHA1 Message Date
34eaa72033 Bump patch version to 1.8.3
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:36:21 -07:00
f49f1698f4 Add Tufte theme with light and dark variants
Warm cream paper, near-black ink, zero rounding, minimal chrome,
restrained dark red and navy accents following Tufte's design principles.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-24 17:34:23 -07:00
f4b3188069 Forgot to bump patch version. 2026-03-17 17:28:57 -07:00
2571ab79c1 build now works on nix
1. Static linking - Added KTE_STATIC_LINK CMake option and
   disabled it in default.nix to avoid the "attempted
   static link of dynamic object" error

2. Missing include - Added <cstring> to
   test_swap_edge_cases.cc for std::memset/std::memcpy (GCC
   14 is stricter about transitive includes)
2026-03-17 17:15:16 -07:00
d768e56727 Add multi-window support to GUI with shared buffer list and improved input handling
- Introduced support for multiple windows, sharing the primary editor's buffer list.
- Added `GUIFrontend::OpenNewWindow_` for creating secondary windows with independent dimensions and input handlers.
- Redesigned `WindowState` to encapsulate per-window attributes (dimensions, renderer, input, etc.).
- Updated input processing and command execution to route events based on active window, preserving window-level states.
- Enhanced SDL2 and ImGui integration for proper context management across multiple windows.
- Increased robustness by handling window closing, resizing, and cleanup of secondary windows without affecting the primary editor.
- Updated documentation and key bindings for multi-window operations (e.g., Cmd+N / Ctrl+Shift+N).
- Version updated to 1.8.0 to reflect the major GUI enhancement.
2026-03-15 13:19:04 -07:00
11c523ad52 Bump patch version. 2026-02-26 13:27:13 -08:00
c261261e26 Initialize ErrorHandler early and ensure immediate log file creation
- Added early initialization of `ErrorHandler` in `main.cc` for robust error handling.
- Modified `ErrorHandler` to create the log file immediately, ensuring its presence in the state directory.
- Simplified conditional checks for log file operations and updated timestamp handling to use `system_clock`.
2026-02-26 13:25:57 -08:00
27dcb41857 Add ReflowUndo tests and integrate InsertRow undo support
- Added `test_reflow_undo.cc` to validate undo/redo workflows for reflow operations.
- Introduced `UndoType::InsertRow` in `UndoSystem` for tracking row insertion changes in undo history.
- Updated `UndoNode.h` and `UndoSystem.cc` to support row insertion as a standalone undo step.
- Enhanced reflow paragraph functionality to properly record undo/redo actions for both row deletion and insertion.
- Enabled legacy/extended undo tests in `test_undo.cc` for comprehensive validation.
- Updated `CMakeLists.txt` to include new test file in the build target.
2026-02-26 13:21:07 -08:00
bc3433e988 Add SmartNewline command with tests and editor integration
- Introduced `CommandId::SmartNewline` for auto-indented newlines, enhancing text editing workflows.
- Added `cmd_smart_newline` to implement indentation-aware newline logic.
- Integrated SmartNewline with keymaps, mouse/keyboard input handlers, and terminal/editor commands.
- Wrote comprehensive tests in `test_smart_newline.cc` to validate behavior for spaces, tabs, and no-indentation cases.
- Updated `Command.h` and `CMakeLists.txt` to register and build the new command.
2026-02-26 13:08:56 -08:00
22 changed files with 1162 additions and 265 deletions

View File

@@ -4,7 +4,7 @@ project(kte)
include(GNUInstallDirs)
set(CMAKE_CXX_STANDARD 20)
set(KTE_VERSION "1.7.0")
set(KTE_VERSION "1.8.3")
# Default to terminal-only build to avoid SDL/OpenGL dependency by default.
# Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available.
@@ -14,6 +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")
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
option(KTE_STATIC_LINK "Enable static linking on Linux" ON)
# Optionally enable AddressSanitizer (ASan)
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
@@ -285,7 +286,7 @@ endif ()
target_link_libraries(kte ${CURSES_LIBRARIES})
# Static linking on Linux only (macOS does not support static linking of system libraries)
if (NOT APPLE)
if (NOT APPLE AND KTE_STATIC_LINK)
target_link_options(kte PRIVATE -static)
endif ()
@@ -336,6 +337,8 @@ if (BUILD_TESTS)
tests/test_visual_line_mode.cc
tests/test_benchmarks.cc
tests/test_migration_coverage.cc
tests/test_smart_newline.cc
tests/test_reflow_undo.cc
# minimal engine sources required by Buffer
PieceTable.cc
@@ -373,7 +376,7 @@ if (BUILD_TESTS)
endif ()
# Static linking on Linux only (macOS does not support static linking of system libraries)
if (NOT APPLE)
if (NOT APPLE AND KTE_STATIC_LINK)
target_link_options(kte_tests PRIVATE -static)
endif ()
endif ()
@@ -416,7 +419,7 @@ if (BUILD_GUI)
endif ()
# Static linking on Linux only (macOS does not support static linking of system libraries)
if (NOT APPLE)
if (NOT APPLE AND KTE_STATIC_LINK)
target_link_options(kge PRIVATE -static)
endif ()

View File

@@ -115,6 +115,14 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
}
static bool
cmd_new_window(CommandContext &ctx)
{
ctx.editor.SetNewWindowRequested(true);
return true;
}
static bool
cmd_center_on_cursor(CommandContext &ctx)
{
@@ -1109,34 +1117,33 @@ cmd_theme_set_by_name(const CommandContext &ctx)
static bool
cmd_theme_set_by_name(CommandContext &ctx)
{
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
// Qt GUI build: schedule theme change for frontend
std::string name = ctx.arg;
// trim spaces
auto ltrim = [](std::string &s) {
// Qt GUI build: schedule theme change for frontend
std::string name = ctx.arg;
// trim spaces
auto ltrim = [](std::string &s) {
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
return !std::isspace(ch);
}));
};
auto rtrim = [](std::string &s) {
};
auto rtrim = [](std::string &s) {
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
return !std::isspace(ch);
}).base(), s.end());
};
ltrim (name);
rtrim (name);
};
ltrim(name);
rtrim(name);
if (name.empty()) {
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
return true;
}
kte::gThemeChangeRequest= name;
kte::gThemeChangePending=true;
ctx.editor.SetStatus (std::string("Theme requested: ") + name);
kte::gThemeChangeRequest = name;
kte::gThemeChangePending = true;
ctx.editor.SetStatus(std::string("Theme requested: ") + name);
return true;
# else
(void) ctx;
// No-op in terminal build
(void) ctx;
// No-op in terminal build
return true;
# endif
}
@@ -2255,10 +2262,8 @@ cmd_show_help(CommandContext &ctx)
};
auto populate_from_text = [](Buffer &b, const std::string &text) {
// Clear existing rows
while (b.Nrows() > 0) {
b.delete_row(0);
}
// Clear existing content
b.replace_all_bytes("");
// Parse text and insert rows
std::string line;
line.reserve(128);
@@ -2949,6 +2954,58 @@ cmd_newline(CommandContext &ctx)
}
static bool
cmd_smart_newline(CommandContext &ctx)
{
Buffer *buf = ctx.editor.CurrentBuffer();
if (!buf) {
ctx.editor.SetStatus("No buffer to edit");
return false;
}
if (buf->IsReadOnly()) {
ctx.editor.SetStatus("Read-only buffer");
return true;
}
// Smart newline behavior: add a newline with the same indentation as the current line.
// Find indentation of current line
std::size_t y = buf->Cury();
std::string line = buf->GetLineString(y);
std::string indent;
for (char c: line) {
if (c == ' ' || c == '\t') {
indent += c;
} else {
break;
}
}
// Perform standard newline first
if (!cmd_newline(ctx)) {
return false;
}
// Now insert the indentation at the new cursor position
if (!indent.empty()) {
std::size_t new_y = buf->Cury();
std::size_t new_x = buf->Curx();
buf->insert_text(static_cast<int>(new_y), static_cast<int>(new_x), indent);
buf->SetCursor(new_x + indent.size(), new_y);
buf->SetDirty(true);
if (auto *u = buf->Undo()) {
u->Begin(UndoType::Insert);
u->Append(indent);
u->commit();
}
}
ensure_cursor_visible(ctx.editor, *buf);
return true;
}
static bool
cmd_backspace(CommandContext &ctx)
{
@@ -4624,7 +4681,14 @@ cmd_reflow_paragraph(CommandContext &ctx)
new_lines.push_back("");
// Replace paragraph lines via PieceTable-backed operations
UndoSystem *u = buf->Undo();
for (std::size_t i = para_end; i + 1 > para_start; --i) {
if (u) {
buf->SetCursor(0, i);
u->Begin(UndoType::DeleteRow);
u->Append(static_cast<std::string>(buf->Rows()[i]));
u->commit();
}
buf->delete_row(static_cast<int>(i));
if (i == 0)
break; // prevent wrap on size_t
@@ -4633,6 +4697,12 @@ cmd_reflow_paragraph(CommandContext &ctx)
std::size_t insert_y = para_start;
for (const auto &ln: new_lines) {
buf->insert_row(static_cast<int>(insert_y), std::string_view(ln));
if (u) {
buf->SetCursor(0, insert_y);
u->Begin(UndoType::InsertRow);
u->Append(std::string_view(ln));
u->commit();
}
insert_y += 1;
}
@@ -4806,6 +4876,9 @@ InstallDefaultCommands()
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true
});
CommandRegistry::Register({CommandId::Newline, "newline", "Insert newline at cursor", cmd_newline});
CommandRegistry::Register({
CommandId::SmartNewline, "smart-newline", "Insert newline with auto-indent", cmd_smart_newline
});
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
@@ -4935,6 +5008,11 @@ InstallDefaultCommands()
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
false, false
});
// GUI: new window
CommandRegistry::Register({
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
false, false
});
}

View File

@@ -38,6 +38,7 @@ enum class CommandId {
// Editing
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
Newline, // insert a newline at cursor
SmartNewline, // insert a newline with auto-indent (Shift-Enter)
Backspace, // delete char before cursor (may join lines)
DeleteChar, // delete char at cursor (may join lines)
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
@@ -110,6 +111,8 @@ enum class CommandId {
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
// Viewport control
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
// GUI: open a new editor window sharing the same buffer list
NewWindow,
};

View File

@@ -246,6 +246,18 @@ public:
}
void SetNewWindowRequested(bool on)
{
new_window_requested_ = on;
}
[[nodiscard]] bool NewWindowRequested() const
{
return new_window_requested_;
}
void SetQuitConfirmPending(bool on)
{
quit_confirm_pending_ = on;
@@ -570,13 +582,22 @@ public:
// Direct access when needed (try to prefer methods above)
[[nodiscard]] const std::vector<Buffer> &Buffers() const
{
return buffers_;
return shared_buffers_ ? *shared_buffers_ : buffers_;
}
std::vector<Buffer> &Buffers()
{
return buffers_;
return shared_buffers_ ? *shared_buffers_ : buffers_;
}
// Share another editor's buffer list. When set, this editor operates on
// the provided vector instead of its own. Pass nullptr to detach.
void SetSharedBuffers(std::vector<Buffer> *shared)
{
shared_buffers_ = shared;
curbuf_ = 0;
}
@@ -628,6 +649,7 @@ private:
bool repeatable_ = false; // whether the next command is repeatable
std::vector<Buffer> buffers_;
std::vector<Buffer> *shared_buffers_ = nullptr; // if set, use this instead of buffers_
std::size_t curbuf_ = 0; // index into buffers_
// Swap journaling manager (lifetime = editor)
@@ -639,6 +661,7 @@ private:
// Quit state
bool quit_requested_ = false;
bool new_window_requested_ = false;
bool quit_confirm_pending_ = false;
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs

View File

@@ -20,6 +20,8 @@ ErrorHandler::ErrorHandler()
fs::create_directories(log_dir);
}
log_file_path_ = (log_dir / "error.log").string();
// Create the log file immediately so it exists in the state directory
ensure_log_file();
} catch (...) {
// If we can't create the directory, disable file logging
file_logging_enabled_ = false;
@@ -34,11 +36,7 @@ ErrorHandler::ErrorHandler()
ErrorHandler::~ErrorHandler()
{
std::lock_guard<std::mutex> lg(mtx_);
if (log_file_ &&log_file_
->
is_open()
)
{
if (log_file_ && log_file_->is_open()) {
log_file_->flush();
log_file_->close();
}
@@ -249,10 +247,7 @@ void
ErrorHandler::ensure_log_file()
{
// Must be called with mtx_ held
if (log_file_ &&log_file_
->
is_open()
)
if (log_file_ && log_file_->is_open())
return;
if (log_file_path_.empty())
@@ -313,6 +308,6 @@ std::uint64_t
ErrorHandler::now_ns()
{
using namespace std::chrono;
return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count();
return duration_cast<nanoseconds>(system_clock::now().time_since_epoch()).count();
}
} // namespace kte

View File

@@ -330,6 +330,7 @@ enum class ThemeId {
Amber = 10,
WeylandYutani = 11,
Orbital = 12,
Tufte = 13,
};
// Current theme tracking
@@ -377,6 +378,7 @@ BackgroundModeName()
#include "themes/WeylandYutani.h"
#include "themes/Zenburn.h"
#include "themes/Orbital.h"
#include "themes/Tufte.h"
// Theme abstraction and registry (generalized theme system)
@@ -488,6 +490,28 @@ struct OrbitalTheme final : Theme {
}
};
struct TufteTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
return "tufte";
}
void Apply() const override
{
if (gBackgroundMode == BackgroundMode::Dark)
ApplyTufteDarkTheme();
else
ApplyTufteLightTheme();
}
ThemeId Id() override
{
return ThemeId::Tufte;
}
};
struct ZenburnTheme final : Theme {
[[nodiscard]] const char *Name() const override
{
@@ -657,7 +681,7 @@ ThemeRegistry()
static std::vector<std::unique_ptr<Theme> > reg;
if (reg.empty()) {
// Alphabetical by canonical name:
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, weyland-yutani, zenburn
// amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn
reg.emplace_back(std::make_unique<detail::AmberTheme>());
reg.emplace_back(std::make_unique<detail::EInkTheme>());
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
@@ -669,6 +693,7 @@ ThemeRegistry()
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
reg.emplace_back(std::make_unique<detail::TufteTheme>());
reg.emplace_back(std::make_unique<detail::WeylandYutaniTheme>());
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
}
@@ -855,10 +880,12 @@ ThemeIndexFromId(const ThemeId id)
return 9;
case ThemeId::Solarized:
return 10;
case ThemeId::WeylandYutani:
case ThemeId::Tufte:
return 11;
case ThemeId::Zenburn:
case ThemeId::WeylandYutani:
return 12;
case ThemeId::Zenburn:
return 13;
}
return 0;
}
@@ -892,8 +919,10 @@ ThemeIdFromIndex(const size_t idx)
case 10:
return ThemeId::Solarized;
case 11:
return ThemeId::WeylandYutani;
return ThemeId::Tufte;
case 12:
return ThemeId::WeylandYutani;
case 13:
return ThemeId::Zenburn;
}
}

View File

@@ -27,6 +27,7 @@ HelpText::Text()
" C-k SPACE Toggle mark\n"
" C-k C-d Kill entire line\n"
" C-k C-q Quit now (no confirm)\n"
" C-k C-s Save\n"
" C-k C-x Save and quit\n"
" C-k a Mark start of file, jump to end\n"
" C-k b Switch buffer\n"
@@ -63,6 +64,10 @@ HelpText::Text()
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
" ESC q Reflow paragraph\n"
"\n"
"Universal argument:\n"
" C-u Begin repeat count (then type digits); C-u alone multiplies by 4\n"
" C-u N <cmd> Repeat <cmd> N times (e.g., C-u 8 C-f moves right 8 chars)\n"
"\n"
"Control keys:\n"
" C-a C-e Line start / end\n"
" C-b C-f Move left / right\n"
@@ -74,12 +79,20 @@ HelpText::Text()
" C-t Regex search & replace\n"
" C-h Search & replace\n"
" C-l / C-g Refresh / Cancel\n"
" C-u [digits] Universal argument (repeat count)\n"
"\n"
"Buffers:\n +HELP+ is read-only. Press C-k ' to toggle; C-k h restores it.\n"
"\n"
"GUI appearance (command prompt):\n"
" : theme NAME Set GUI theme (amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, plan9, solarized, weyland-yutani, zenburn)\n"
" : background MODE Set background: light | dark (affects eink, gruvbox, old-book, solarized)\n"
"\n"
"GUI config file options:\n"
" font_size=NUM Set font size in pixels (default: 16; e.g., font_size=18)\n"
"\n"
"GUI window management:\n"
" Cmd+N (macOS) Open a new editor window sharing the same buffers\n"
" Ctrl+Shift+N (Linux) Open a new editor window sharing the same buffers\n"
" Close window Secondary windows close independently; closing the\n"
" primary window quits the editor\n"
);
}

View File

@@ -29,21 +29,86 @@
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
// ---------------------------------------------------------------------------
// Helpers shared between Init and OpenNewWindow_
// ---------------------------------------------------------------------------
static void
apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
{
if (!b)
return;
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty())
first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(
b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
// Update editor logical rows/cols from current ImGui metrics for a given display size.
static void
update_editor_dimensions(Editor &ed, float disp_w, float disp_h)
{
float row_h = ImGui::GetTextLineHeightWithSpacing();
float ch_w = ImGui::CalcTextSize("M").x;
if (row_h <= 0.0f)
row_h = 16.0f;
if (ch_w <= 0.0f)
ch_w = 8.0f;
const float pad_x = 6.0f;
const float pad_y = 6.0f;
float wanted_bar_h = ImGui::GetFrameHeight();
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
std::size_t rows = content_rows + 1;
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
if (rows != ed.Rows() || cols != ed.Cols()) {
ed.SetDimensions(rows, cols);
}
}
bool
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
{
(void) argc;
(void) argv;
// Attach editor to input handler for editor-owned features (e.g., universal argument)
input_.Attach(&ed);
// editor dimensions will be initialized during the first Step() frame
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
config_ = GUIConfig::Load();
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
return false;
}
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
GUIConfig cfg = GUIConfig::Load();
// GL attributes for core profile
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
@@ -56,61 +121,55 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
// Compute desired window size from config
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
if (cfg.fullscreen) {
// "Fullscreen": fill the usable bounds of the primary display.
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
int init_w = 1280, init_h = 800;
if (config_.fullscreen) {
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
width_ = usable.w;
height_ = usable.h;
init_w = usable.w;
init_h = usable.h;
}
#if !defined(__APPLE__)
// Non-macOS: desktop fullscreen uses the current display resolution.
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
#endif
} else {
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
int w = cfg.columns * static_cast<int>(cfg.font_size);
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
// As a safety, clamp to display usable bounds if retrievable
int w = config_.columns * static_cast<int>(config_.font_size);
int h = config_.rows * static_cast<int>(config_.font_size * 1.2);
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
w = std::min(w, usable.w);
h = std::min(h, usable.h);
}
width_ = std::max(320, w);
height_ = std::max(200, h);
init_w = std::max(320, w);
init_h = std::max(200, h);
}
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
window_ = SDL_CreateWindow(
SDL_Window *win = SDL_CreateWindow(
"kge - kyle's graphical editor " KTE_VERSION_STR,
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
width_, height_,
init_w, init_h,
win_flags);
if (!window_) {
if (!win) {
return false;
}
SDL_EnableScreenSaver();
#if defined(__APPLE__)
// macOS: when "fullscreen" is requested, position the window at the
// top-left of the usable display area to mimic fullscreen while keeping
// the system menu bar visible.
if (cfg.fullscreen) {
if (config_.fullscreen) {
SDL_Rect usable{};
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
SDL_SetWindowPosition(window_, usable.x, usable.y);
SDL_SetWindowPosition(win, usable.x, usable.y);
}
}
#endif
gl_ctx_ = SDL_GL_CreateContext(window_);
if (!gl_ctx_)
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
if (!gl_ctx) {
SDL_DestroyWindow(win);
return false;
SDL_GL_MakeCurrent(window_, gl_ctx_);
}
SDL_GL_MakeCurrent(win, gl_ctx);
SDL_GL_SetSwapInterval(1); // vsync
IMGUI_CHECKVERSION();
@@ -121,94 +180,54 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
if (const char *home = std::getenv("HOME")) {
namespace fs = std::filesystem;
fs::path config_dir = fs::path(home) / ".config" / "kte";
std::error_code ec;
if (!fs::exists(config_dir)) {
fs::create_directories(config_dir, ec);
}
if (fs::exists(config_dir)) {
static std::string ini_path = (config_dir / "imgui.ini").string();
io.IniFilename = ini_path.c_str();
}
}
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
ImGui::StyleColorsDark();
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
if (cfg.background == "light")
if (config_.background == "light")
kte::SetBackgroundMode(kte::BackgroundMode::Light);
else
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
kte::ApplyThemeByName(cfg.theme);
kte::ApplyThemeByName(config_.theme);
// Apply default syntax highlighting preference from GUI config to the current buffer
if (Buffer *b = ed.CurrentBuffer()) {
if (cfg.syntax) {
b->SetSyntaxEnabled(true);
// Ensure a highlighter is available if possible
b->EnsureHighlighter();
if (auto *eng = b->Highlighter()) {
if (!eng->HasHighlighter()) {
// Try detect from filename and first line; fall back to cpp or existing filetype
std::string first_line;
const auto &rows = b->Rows();
if (!rows.empty())
first_line = static_cast<std::string>(rows[0]);
std::string ft = kte::HighlighterRegistry::DetectForPath(
b->Filename(), first_line);
if (!ft.empty()) {
eng->SetHighlighter(kte::HighlighterRegistry::CreateFor(ft));
b->SetFiletype(ft);
eng->InvalidateFrom(0);
} else {
// Unknown/unsupported -> install a null highlighter to keep syntax enabled
eng->SetHighlighter(std::make_unique<kte::NullHighlighter>());
b->SetFiletype("");
eng->InvalidateFrom(0);
}
}
}
} else {
b->SetSyntaxEnabled(false);
}
}
apply_syntax_to_buffer(ed.CurrentBuffer(), config_);
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx))
return false;
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
return false;
// Cache initial window size; logical rows/cols will be computed in Step() once a valid ImGui frame exists
// Cache initial window size
int w, h;
SDL_GetWindowSize(window_, &w, &h);
width_ = w;
height_ = h;
SDL_GetWindowSize(win, &w, &h);
init_w = w;
init_h = h;
#if defined(__APPLE__)
// Workaround: On macOS Retina when starting maximized, we sometimes get a
// subtle input vs draw alignment mismatch until the first manual resize.
// Nudge the window size by 1px and back to trigger a proper internal
// recomputation, without visible impact.
if (w > 1 && h > 1) {
SDL_SetWindowSize(window_, w - 1, h - 1);
SDL_SetWindowSize(window_, w, h);
// Update cached size in case backend reports immediately
SDL_GetWindowSize(window_, &w, &h);
width_ = w;
height_ = h;
SDL_SetWindowSize(win, w - 1, h - 1);
SDL_SetWindowSize(win, w, h);
SDL_GetWindowSize(win, &w, &h);
init_w = w;
init_h = h;
}
#endif
// Install embedded fonts into registry and load configured font
// Install embedded fonts
kte::Fonts::InstallDefaultFonts();
// Initialize font atlas using configured font name and size; fallback to embedded default helper
if (!kte::Fonts::FontRegistry::Instance().LoadFont(cfg.font, (float) cfg.font_size)) {
LoadGuiFont_(nullptr, (float) cfg.font_size);
// Record defaults in registry so subsequent size changes have a base
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) cfg.font_size);
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
LoadGuiFont_(nullptr, (float) config_.font_size);
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) config_.font_size);
std::string n;
float s = 0.0f;
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
@@ -216,6 +235,68 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
}
}
// Build primary WindowState
auto ws = std::make_unique<WindowState>();
ws->window = win;
ws->gl_ctx = gl_ctx;
ws->width = init_w;
ws->height = init_h;
// The primary window's editor IS the editor passed in from main; we don't
// use ws->editor for the primary — instead we keep a pointer to &ed.
// We store a sentinel: window index 0 uses the external editor reference.
// To keep things simple, attach input to the passed-in editor.
ws->input.Attach(&ed);
windows_.push_back(std::move(ws));
return true;
}
bool
GUIFrontend::OpenNewWindow_(Editor &primary)
{
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
int w = windows_[0]->width;
int h = windows_[0]->height;
SDL_Window *win = SDL_CreateWindow(
"kge - kyle's graphical editor " KTE_VERSION_STR,
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
w, h,
win_flags);
if (!win)
return false;
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
if (!gl_ctx) {
SDL_DestroyWindow(win);
return false;
}
SDL_GL_MakeCurrent(win, gl_ctx);
SDL_GL_SetSwapInterval(1);
// Secondary windows share the ImGui context already created in Init.
// We need to init the SDL2/OpenGL backends for this new window.
// ImGui_ImplSDL2 supports multiple windows via SDL_GetWindowID checks.
ImGui_ImplOpenGL3_Init(kGlslVersion);
auto ws = std::make_unique<WindowState>();
ws->window = win;
ws->gl_ctx = gl_ctx;
ws->width = w;
ws->height = h;
// Secondary editor shares the primary's buffer list
ws->editor.SetSharedBuffers(&primary.Buffers());
ws->editor.SetDimensions(primary.Rows(), primary.Cols());
ws->input.Attach(&ws->editor);
windows_.push_back(std::move(ws));
// Restore primary GL context as current
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
return true;
}
@@ -223,137 +304,216 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
void
GUIFrontend::Step(Editor &ed, bool &running)
{
// --- Event processing ---
SDL_Event e;
while (SDL_PollEvent(&e)) {
ImGui_ImplSDL2_ProcessEvent(&e);
// Determine which window this event belongs to
Uint32 event_win_id = 0;
switch (e.type) {
case SDL_QUIT:
running = false;
break;
case SDL_WINDOWEVENT:
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
width_ = e.window.data1;
height_ = e.window.data2;
}
event_win_id = e.window.windowID;
break;
case SDL_KEYDOWN:
case SDL_KEYUP:
event_win_id = e.key.windowID;
break;
case SDL_TEXTINPUT:
event_win_id = e.text.windowID;
break;
case SDL_MOUSEBUTTONDOWN:
case SDL_MOUSEBUTTONUP:
event_win_id = e.button.windowID;
break;
case SDL_MOUSEWHEEL:
event_win_id = e.wheel.windowID;
break;
default:
break;
}
// Map input to commands
input_.ProcessSDLEvent(e);
if (e.type == SDL_QUIT) {
running = false;
break;
}
// Apply pending font change before starting a new frame
if (e.type == SDL_WINDOWEVENT) {
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
// Mark the window as dead; primary window close = quit
for (std::size_t i = 0; i < windows_.size(); ++i) {
if (SDL_GetWindowID(windows_[i]->window) == e.window.windowID) {
if (i == 0) {
running = false;
} else {
windows_[i]->alive = false;
}
break;
}
}
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
for (auto &ws: windows_) {
if (SDL_GetWindowID(ws->window) == e.window.windowID) {
ws->width = e.window.data1;
ws->height = e.window.data2;
break;
}
}
}
}
// Route input events to the correct window's input handler
if (event_win_id != 0) {
// Primary window (index 0) uses the external editor &ed
if (windows_.size() > 0 &&
SDL_GetWindowID(windows_[0]->window) == event_win_id) {
windows_[0]->input.ProcessSDLEvent(e);
} else {
for (std::size_t i = 1; i < windows_.size(); ++i) {
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
windows_[i]->input.ProcessSDLEvent(e);
break;
}
}
}
}
}
if (!running)
return;
// --- Apply pending font change ---
{
std::string fname;
float fsize = 0.0f;
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
if (!fname.empty() && fsize > 0.0f) {
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
// Recreate backend font texture
ImGui_ImplOpenGL3_DestroyFontsTexture();
ImGui_ImplOpenGL3_CreateFontsTexture();
}
}
}
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
// --- Step each window ---
// We iterate by index because OpenNewWindow_ may append to windows_.
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
WindowState &ws = *windows_[wi];
if (!ws.alive)
continue;
Editor &wed = (wi == 0) ? ed : ws.editor;
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
// Start a new ImGui frame
ImGui_ImplOpenGL3_NewFrame();
ImGui_ImplSDL2_NewFrame(window_);
ImGui_ImplSDL2_NewFrame(ws.window);
ImGui::NewFrame();
// Update editor logical rows/cols using current ImGui metrics and display size
// Update editor dimensions
{
ImGuiIO &io = ImGui::GetIO();
float row_h = ImGui::GetTextLineHeightWithSpacing();
float ch_w = ImGui::CalcTextSize("M").x;
if (row_h <= 0.0f)
row_h = 16.0f;
if (ch_w <= 0.0f)
ch_w = 8.0f;
// Prefer ImGui IO display size; fall back to cached SDL window size
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(height_);
// Account for the GUI window padding and the status bar height used in ImGuiRenderer.
const float pad_x = 6.0f;
const float pad_y = 6.0f;
// Use the same logic as ImGuiRenderer for available height and status bar reservation.
float wanted_bar_h = ImGui::GetFrameHeight();
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
// Visible content rows inside the scroll child
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
// Editor::Rows includes the status line; add 1 back for it.
std::size_t rows = content_rows + 1;
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
// Only update if changed to avoid churn
if (rows != ed.Rows() || cols != ed.Cols()) {
ed.SetDimensions(rows, cols);
}
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(ws.width);
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(ws.height);
update_editor_dimensions(wed, disp_w, disp_h);
}
// Allow deferred opens (including swap recovery prompts) to run.
ed.ProcessPendingOpens();
// Allow deferred opens
wed.ProcessPendingOpens();
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
// Drain input queue
for (;;) {
MappedInput mi;
if (!input_.Poll(mi))
if (!ws.input.Poll(mi))
break;
if (mi.hasCommand) {
// Track kill ring before and after to sync GUI clipboard when it changes
const std::string before = ed.KillRingHead();
Execute(ed, mi.id, mi.arg, mi.count);
const std::string after = ed.KillRingHead();
if (mi.id == CommandId::NewWindow) {
// Open a new window; handled after this loop
wed.SetNewWindowRequested(true);
} else {
const std::string before = wed.KillRingHead();
Execute(wed, mi.id, mi.arg, mi.count);
const std::string after = wed.KillRingHead();
if (after != before && !after.empty()) {
// Update the system clipboard to mirror the kill ring head in GUI
SDL_SetClipboardText(after.c_str());
}
}
}
}
if (ed.QuitRequested()) {
// Handle new-window request
if (wed.NewWindowRequested()) {
wed.SetNewWindowRequested(false);
OpenNewWindow_(ed); // always share primary editor's buffers
}
if (wi == 0 && wed.QuitRequested()) {
running = false;
}
// No runtime font UI; always use embedded font.
// Draw editor UI
renderer_.Draw(ed);
// Draw
ws.renderer.Draw(wed);
// Render
ImGui::Render();
int display_w, display_h;
SDL_GL_GetDrawableSize(window_, &display_w, &display_h);
SDL_GL_GetDrawableSize(ws.window, &display_w, &display_h);
glViewport(0, 0, display_w, display_h);
glClearColor(0.1f, 0.1f, 0.11f, 1.0f);
glClear(GL_COLOR_BUFFER_BIT);
ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData());
SDL_GL_SwapWindow(window_);
SDL_GL_SwapWindow(ws.window);
}
// Remove dead secondary windows
for (auto it = windows_.begin() + 1; it != windows_.end();) {
if (!(*it)->alive) {
SDL_GL_MakeCurrent((*it)->window, (*it)->gl_ctx);
ImGui_ImplOpenGL3_Shutdown();
SDL_GL_DeleteContext((*it)->gl_ctx);
SDL_DestroyWindow((*it)->window);
it = windows_.erase(it);
// Restore primary context
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
} else {
++it;
}
}
}
void
GUIFrontend::Shutdown()
{
// Destroy secondary windows first
for (std::size_t i = 1; i < windows_.size(); ++i) {
SDL_GL_MakeCurrent(windows_[i]->window, windows_[i]->gl_ctx);
ImGui_ImplOpenGL3_Shutdown();
SDL_GL_DeleteContext(windows_[i]->gl_ctx);
SDL_DestroyWindow(windows_[i]->window);
}
windows_.resize(std::min(windows_.size(), std::size_t(1)));
// Destroy primary window
if (!windows_.empty()) {
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
}
ImGui_ImplOpenGL3_Shutdown();
ImGui_ImplSDL2_Shutdown();
ImGui::DestroyContext();
if (gl_ctx_) {
SDL_GL_DeleteContext(gl_ctx_);
gl_ctx_ = nullptr;
if (!windows_.empty()) {
if (windows_[0]->gl_ctx) {
SDL_GL_DeleteContext(windows_[0]->gl_ctx);
windows_[0]->gl_ctx = nullptr;
}
if (window_) {
SDL_DestroyWindow(window_);
window_ = nullptr;
if (windows_[0]->window) {
SDL_DestroyWindow(windows_[0]->window);
windows_[0]->window = nullptr;
}
}
windows_.clear();
SDL_Quit();
}
@@ -367,7 +527,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
ImFontConfig config;
config.MergeMode = false;
// Load Basic Latin + Latin Supplement
io.Fonts->AddFontFromMemoryCompressedTTF(
kte::Fonts::DefaultFontData,
kte::Fonts::DefaultFontSize,
@@ -375,7 +534,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
&config,
io.Fonts->GetGlyphRangesDefault());
// Merge Greek and Mathematical symbols from IosevkaExtended
config.MergeMode = true;
static const ImWchar extended_ranges[] = {
0x0370, 0x03FF, // Greek and Coptic

View File

@@ -2,10 +2,14 @@
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
*/
#pragma once
#include <memory>
#include <vector>
#include "Frontend.h"
#include "GUIConfig.h"
#include "ImGuiInputHandler.h"
#include "ImGuiRenderer.h"
#include "Editor.h"
struct SDL_Window;
@@ -24,13 +28,25 @@ public:
void Shutdown() override;
private:
// Per-window state
struct WindowState {
SDL_Window *window = nullptr;
SDL_GLContext gl_ctx = nullptr;
ImGuiInputHandler input{};
ImGuiRenderer renderer{};
Editor editor{};
int width = 1280;
int height = 800;
bool alive = true;
};
// Open a new secondary window sharing the primary editor's buffer list.
// Returns false if window creation fails.
bool OpenNewWindow_(Editor &primary);
static bool LoadGuiFont_(const char *path, float size_px);
GUIConfig config_{};
ImGuiInputHandler input_{};
ImGuiRenderer renderer_{};
SDL_Window *window_ = nullptr;
SDL_GLContext gl_ctx_ = nullptr;
int width_ = 1280;
int height_ = 800;
// Primary window (index 0 in windows_); created during Init.
std::vector<std::unique_ptr<WindowState> > windows_;
};

View File

@@ -125,7 +125,11 @@ map_key(const SDL_Keycode key,
case SDLK_KP_ENTER:
k_prefix = false;
k_ctrl_pending = false;
if (mod & KMOD_SHIFT) {
out = {true, CommandId::SmartNewline, "", 0};
} else {
out = {true, CommandId::Newline, "", 0};
}
return true;
case SDLK_ESCAPE:
k_prefix = false;
@@ -333,6 +337,18 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
const SDL_Keycode key = e.key.keysym.sym;
// New window: Cmd+N (macOS) or Ctrl+Shift+N (Linux/Windows)
{
const bool gui_n = (mods & KMOD_GUI) && !(mods & KMOD_CTRL) && (key == SDLK_n);
const bool ctrl_sn = (mods & KMOD_CTRL) && (mods & KMOD_SHIFT) && (key == SDLK_n);
if (gui_n || ctrl_sn) {
std::lock_guard<std::mutex> lk(mu_);
q_.push(MappedInput{true, CommandId::NewWindow, std::string(), 0});
suppress_text_input_once_ = true;
return true;
}
}
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
// Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode.
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
@@ -439,12 +455,14 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
}
// If editor universal argument is active, consume digit TEXTINPUT
if (ed_ && ed_
if (ed_ &&ed_
->
UArg() != 0
) {
)
{
const char *txt = e.text.text;
if (txt && *txt) {
unsigned char c0 = static_cast<unsigned char>(txt[0]);

View File

@@ -209,19 +209,35 @@ ImGuiRenderer::Draw(Editor &ed)
return {by, best_col};
};
// Mouse-driven selection: set mark on press, update cursor on drag
// Mouse-driven selection: set mark on double-click or drag, update cursor on any press/drag
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
mouse_selecting = true;
auto [by, bx] = mouse_pos_to_buf();
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
// Only set mark on double click.
// Dragging will also set the mark if not already set (handled below).
if (ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
mbuf->SetMark(bx, by);
}
}
}
if (mouse_selecting && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
auto [by, bx] = mouse_pos_to_buf();
// If we are dragging (mouse moved while down), ensure mark is set to start selection
if (ImGui::IsMouseDragging(ImGuiMouseButton_Left, 1.0f)) {
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
if (!mbuf->MarkSet()) {
// We'd need to convert click_pos to buf coords, but it's complex here.
// Setting it to where the cursor was *before* we started moving it
// in this frame is a good approximation, or just using current.
mbuf->SetMark(mbuf->Curx(), mbuf->Cury());
}
}
}
char tmp[64];
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));

View File

@@ -226,6 +226,10 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
case 'q':
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
return true;
case '\n':
case '\r':
out = CommandId::SmartNewline; // Shift+Enter (some terminals send this as Alt+Enter sequences)
return true;
default:
break;
}

View File

@@ -67,13 +67,20 @@ map_key_to_command(const int ch,
if (pressed) {
mouse_selecting = true;
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
if (Buffer *b = ed->CurrentBuffer()) {
b->SetMark(b->Curx(), b->Cury());
}
// We don't set the mark on simple click anymore in ncurses either,
// to be consistent. ncurses doesn't easily support double-click
// or drag-threshold in a platform-independent way here,
// but we can at least only set mark on MOVED.
out.hasCommand = false;
return true;
}
if (mouse_selecting && moved) {
if (Buffer *b = ed->CurrentBuffer()) {
if (!b->MarkSet()) {
// Set mark at CURRENT cursor position (which is where we were before this move)
b->SetMark(b->Curx(), b->Cury());
}
}
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
out.hasCommand = false;
return true;

View File

@@ -9,6 +9,7 @@ enum class UndoType : std::uint8_t {
Paste,
Newline,
DeleteRow,
InsertRow,
};
struct UndoNode {

View File

@@ -36,7 +36,8 @@ UndoSystem::Begin(UndoType type)
const int col = static_cast<int>(buf_->Curx());
// Some operations should always be standalone undo steps.
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow);
const bool always_standalone = (type == UndoType::Newline || type == UndoType::DeleteRow || type ==
UndoType::InsertRow);
if (always_standalone) {
commit();
}
@@ -75,6 +76,7 @@ UndoSystem::Begin(UndoType type)
}
case UndoType::Newline:
case UndoType::DeleteRow:
case UndoType::InsertRow:
break;
}
}
@@ -314,6 +316,15 @@ UndoSystem::apply(const UndoNode *node, int direction)
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
}
break;
case UndoType::InsertRow:
if (direction > 0) {
buf_->insert_row(node->row, node->text);
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
} else {
buf_->delete_row(node->row);
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
}
break;
}
}
@@ -411,6 +422,8 @@ UndoSystem::type_str(UndoType t)
return "Newline";
case UndoType::DeleteRow:
return "DeleteRow";
case UndoType::InsertRow:
return "InsertRow";
}
return "?";
}

View File

@@ -48,6 +48,7 @@ stdenv.mkDerivation {
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
"-DCMAKE_BUILD_TYPE=Debug"
"-DKTE_STATIC_LINK=OFF"
];
installPhase = ''

View File

@@ -117,6 +117,9 @@ main(int argc, char *argv[])
{
std::setlocale(LC_ALL, "");
// Ensure the error handler (and its log file) is initialised early.
kte::ErrorHandler::Instance();
Editor editor;
// CLI parsing using getopt_long

69
tests/test_reflow_undo.cc Normal file
View File

@@ -0,0 +1,69 @@
#include "Test.h"
#include "Buffer.h"
#include "Command.h"
#include "Editor.h"
#include "UndoSystem.h"
#include <string>
static std::string
to_string_rows(const Buffer &buf)
{
std::string out;
for (const auto &r: buf.Rows()) {
out += static_cast<std::string>(r);
out.push_back('\n');
}
return out;
}
TEST (ReflowUndo)
{
InstallDefaultCommands();
Editor ed;
ed.SetDimensions(24, 80);
Buffer b;
const std::string initial =
"This is a very long line that should be reflowed into multiple lines to see if undo works correctly.\n";
b.insert_text(0, 0, initial);
b.SetCursor(0, 0);
// Commit initial insertion so it's its own undo step
if (auto *u = b.Undo())
u->commit();
ed.AddBuffer(std::move(b));
Buffer *buf = ed.CurrentBuffer();
ASSERT_TRUE(buf != nullptr);
const std::string original_dump = to_string_rows(*buf);
// Reflow with small width
const int width = 20;
ASSERT_TRUE(Execute(ed, "reflow-paragraph", "", width));
const std::string reflowed_dump = to_string_rows(*buf);
ASSERT_TRUE(reflowed_dump != original_dump);
ASSERT_TRUE(buf->Rows().size() > 1);
// Undo reflow
ASSERT_TRUE(Execute(ed, "undo", "", 1));
const std::string after_undo_dump = to_string_rows(*buf);
if (after_undo_dump != original_dump) {
fprintf(stderr, "Undo failed.\nExpected:\n%s\nGot:\n%s\n", original_dump.c_str(),
after_undo_dump.c_str());
}
EXPECT_TRUE(after_undo_dump == original_dump);
// Redo reflow
ASSERT_TRUE(Execute(ed, "redo", "", 1));
const std::string after_redo_dump = to_string_rows(*buf);
EXPECT_TRUE(after_redo_dump == reflowed_dump);
}

View File

@@ -0,0 +1,79 @@
#include "Test.h"
#include "Buffer.h"
#include "Editor.h"
#include "Command.h"
#include <string>
TEST (SmartNewline_AutoIndent)
{
Editor editor;
InstallDefaultCommands();
Buffer &buf = editor.Buffers().emplace_back();
// Set up initial state: " line1"
buf.insert_text(0, 0, " line1");
buf.SetCursor(7, 0); // At end of line
// Execute SmartNewline
bool ok = Execute(editor, CommandId::SmartNewline);
ASSERT_TRUE(ok);
// Should have two lines now
ASSERT_EQ(buf.Nrows(), 2);
// Line 0 remains " line1"
ASSERT_EQ(buf.GetLineString(0), " line1");
// Line 1 should have " " (two spaces)
ASSERT_EQ(buf.GetLineString(1), " ");
// Cursor should be at (2, 1)
ASSERT_EQ(buf.Curx(), 2);
ASSERT_EQ(buf.Cury(), 1);
}
TEST (SmartNewline_TabIndent)
{
Editor editor;
InstallDefaultCommands();
Buffer &buf = editor.Buffers().emplace_back();
// Set up initial state: "\tline1"
buf.insert_text(0, 0, "\tline1");
buf.SetCursor(6, 0); // At end of line
// Execute SmartNewline
bool ok = Execute(editor, CommandId::SmartNewline);
ASSERT_TRUE(ok);
// Should have two lines now
ASSERT_EQ(buf.Nrows(), 2);
// Line 1 should have "\t"
ASSERT_EQ(buf.GetLineString(1), "\t");
// Cursor should be at (1, 1)
ASSERT_EQ(buf.Curx(), 1);
ASSERT_EQ(buf.Cury(), 1);
}
TEST (SmartNewline_NoIndent)
{
Editor editor;
InstallDefaultCommands();
Buffer &buf = editor.Buffers().emplace_back();
// Set up initial state: "line1"
buf.insert_text(0, 0, "line1");
buf.SetCursor(5, 0); // At end of line
// Execute SmartNewline
bool ok = Execute(editor, CommandId::SmartNewline);
ASSERT_TRUE(ok);
// Should have two lines now
ASSERT_EQ(buf.Nrows(), 2);
// Line 1 should be empty
ASSERT_EQ(buf.GetLineString(1), "");
// Cursor should be at (0, 1)
ASSERT_EQ(buf.Curx(), 0);
ASSERT_EQ(buf.Cury(), 1);
}

View File

@@ -5,6 +5,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <fstream>
#include <string>
#include <vector>

View File

@@ -57,7 +57,7 @@ validate_undo_tree(const UndoSystem &u)
// The undo suite aims to cover invariants with a small, adversarial test matrix.
TEST(Undo_InsertRun_Coalesces_OneStep)
TEST (Undo_InsertRun_Coalesces_OneStep)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -81,7 +81,7 @@ TEST(Undo_InsertRun_Coalesces_OneStep)
}
TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
TEST (Undo_InsertRun_BreaksOnNonAdjacentCursor)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -109,7 +109,7 @@ TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
}
TEST(Undo_BackspaceRun_Coalesces_OneStep)
TEST (Undo_BackspaceRun_Coalesces_OneStep)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -143,7 +143,7 @@ TEST(Undo_BackspaceRun_Coalesces_OneStep)
}
TEST(Undo_DeleteKeyRun_Coalesces_OneStep)
TEST (Undo_DeleteKeyRun_Coalesces_OneStep)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -176,7 +176,7 @@ TEST(Undo_DeleteKeyRun_Coalesces_OneStep)
}
TEST(Undo_Newline_IsStandalone)
TEST (Undo_Newline_IsStandalone)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -211,7 +211,7 @@ TEST(Undo_Newline_IsStandalone)
}
TEST(Undo_ExplicitGroup_UndoesAsUnit)
TEST (Undo_ExplicitGroup_UndoesAsUnit)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -239,7 +239,7 @@ TEST(Undo_ExplicitGroup_UndoesAsUnit)
}
TEST(Undo_Branching_RedoBranchSelectionDeterministic)
TEST (Undo_Branching_RedoBranchSelectionDeterministic)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -283,7 +283,7 @@ TEST(Undo_Branching_RedoBranchSelectionDeterministic)
}
TEST(Undo_DirtyFlag_CrossesMarkSaved)
TEST (Undo_DirtyFlag_CrossesMarkSaved)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -312,7 +312,7 @@ TEST(Undo_DirtyFlag_CrossesMarkSaved)
}
TEST(Undo_RoundTrip_Lossless_RandomEdits)
TEST (Undo_RoundTrip_Lossless_RandomEdits)
{
Buffer b;
UndoSystem *u = b.Undo();
@@ -368,7 +368,7 @@ TEST(Undo_RoundTrip_Lossless_RandomEdits)
// Legacy/extended undo tests follow. Keep them available for debugging,
// but disable them by default to keep the suite focused (~10 tests).
#if 0
#if 1
TEST (Undo_Branching_RedoPreservedAfterNewEdit)
@@ -713,6 +713,7 @@ TEST (Undo_StructuralInvariants_BranchingAndRoots)
validate_undo_tree(*u);
}
TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
{
Buffer b;
@@ -796,7 +797,7 @@ TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
// Additional legacy tests below are useful, but kept disabled by default.
#if 0
#if 1
TEST (Undo_Branching_SwitchBetweenTwoRedoBranches_TextAndCursor)
{
@@ -1196,4 +1197,167 @@ TEST (Undo_Command_RedoCountSelectsBranch)
validate_undo_tree(*u);
}
TEST (Undo_InsertRow_UndoDeletesRow)
{
Buffer b;
UndoSystem *u = b.Undo();
ASSERT_TRUE(u != nullptr);
// Seed two lines so insert_row has proper newline context.
b.insert_text(0, 0, std::string_view("first\nlast"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
// Insert a row at position 1 (between first and last), then record it.
b.insert_row(1, std::string_view("second"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
ASSERT_EQ(std::string(b.Rows()[1]), std::string("second"));
b.SetCursor(0, 1);
u->Begin(UndoType::InsertRow);
u->Append(std::string_view("second"));
u->commit();
// Undo should remove the inserted row.
u->undo();
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
ASSERT_EQ(std::string(b.Rows()[0]), std::string("first"));
ASSERT_EQ(std::string(b.Rows()[1]), std::string("last"));
// Redo should re-insert it.
u->redo();
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
ASSERT_EQ(std::string(b.Rows()[1]), std::string("second"));
validate_undo_tree(*u);
}
TEST (Undo_DeleteRow_UndoRestoresRow)
{
Buffer b;
UndoSystem *u = b.Undo();
ASSERT_TRUE(u != nullptr);
b.insert_text(0, 0, std::string_view("alpha\nbeta\ngamma"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
// Record a DeleteRow for row 1 ("beta").
b.SetCursor(0, 1);
u->Begin(UndoType::DeleteRow);
u->Append(static_cast<std::string>(b.Rows()[1]));
u->commit();
b.delete_row(1);
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
ASSERT_EQ(std::string(b.Rows()[0]), std::string("alpha"));
ASSERT_EQ(std::string(b.Rows()[1]), std::string("gamma"));
// Undo should restore "beta" at row 1.
u->undo();
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
ASSERT_EQ(std::string(b.Rows()[1]), std::string("beta"));
// Redo should delete it again.
u->redo();
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
ASSERT_EQ(std::string(b.Rows()[1]), std::string("gamma"));
validate_undo_tree(*u);
}
TEST (Undo_InsertRow_IsStandalone)
{
Buffer b;
UndoSystem *u = b.Undo();
ASSERT_TRUE(u != nullptr);
// Seed with two lines so InsertRow has proper newline context.
b.insert_text(0, 0, std::string_view("x\nend"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
// Start a pending insert on row 0.
b.SetCursor(1, 0);
u->Begin(UndoType::Insert);
b.insert_text(0, 1, std::string_view("y"));
u->Append('y');
b.SetCursor(2, 0);
// InsertRow should seal the pending "y" and become its own step.
b.insert_row(1, std::string_view("row2"));
b.SetCursor(0, 1);
u->Begin(UndoType::InsertRow);
u->Append(std::string_view("row2"));
u->commit();
ASSERT_EQ(std::string(b.Rows()[0]), std::string("xy"));
ASSERT_EQ(std::string(b.Rows()[1]), std::string("row2"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 3);
// Undo InsertRow only.
u->undo();
ASSERT_EQ(b.Rows().size(), (std::size_t) 2);
ASSERT_EQ(std::string(b.Rows()[0]), std::string("xy"));
// Undo the insert "y".
u->undo();
ASSERT_EQ(std::string(b.Rows()[0]), std::string("x"));
validate_undo_tree(*u);
}
TEST (Undo_GroupedDeleteAndInsertRows_UndoesAsUnit)
{
Buffer b;
UndoSystem *u = b.Undo();
ASSERT_TRUE(u != nullptr);
// Seed three lines (with trailing newline so delete_row/insert_row work cleanly).
b.insert_text(0, 0, std::string_view("aaa\nbbb\nccc\n"));
ASSERT_EQ(b.Rows().size(), (std::size_t) 4); // 3 content + 1 empty trailing
const std::string original = b.AsString();
// Group: delete content rows then insert replacements (simulates reflow).
(void) u->BeginGroup();
// Delete rows 2,1,0 in reverse order (like reflow does).
for (int i = 2; i >= 0; --i) {
b.SetCursor(0, static_cast<std::size_t>(i));
u->Begin(UndoType::DeleteRow);
u->Append(static_cast<std::string>(b.Rows()[static_cast<std::size_t>(i)]));
u->commit();
b.delete_row(i);
}
// Insert replacement rows.
b.insert_row(0, std::string_view("aaa bbb"));
b.SetCursor(0, 0);
u->Begin(UndoType::InsertRow);
u->Append(std::string_view("aaa bbb"));
u->commit();
b.insert_row(1, std::string_view("ccc"));
b.SetCursor(0, 1);
u->Begin(UndoType::InsertRow);
u->Append(std::string_view("ccc"));
u->commit();
u->EndGroup();
const std::string reflowed = b.AsString();
// Single undo should restore original content.
u->undo();
ASSERT_EQ(b.AsString(), original);
// Redo should restore the reflowed state.
u->redo();
ASSERT_EQ(b.AsString(), reflowed);
validate_undo_tree(*u);
}
#endif // legacy tests

203
themes/Tufte.h Normal file
View File

@@ -0,0 +1,203 @@
// themes/Tufte.h — Edward Tufte inspired ImGui theme (header-only)
// Warm cream paper, dark ink, minimal chrome, restrained accent colors.
#pragma once
#include "ThemeHelpers.h"
// Light variant (primary — Tufte's books are fundamentally light)
static inline void
ApplyTufteLightTheme()
{
// Tufte palette: warm cream paper with near-black ink
const ImVec4 paper = RGBA(0xFFFFF8); // Tufte's signature warm white
const ImVec4 bg1 = RGBA(0xF4F0E8); // slightly darker cream
const ImVec4 bg2 = RGBA(0xEAE6DE); // UI elements
const ImVec4 bg3 = RGBA(0xDDD9D1); // hover/active
const ImVec4 ink = RGBA(0x111111); // near-black text
const ImVec4 dim = RGBA(0x6B6B6B); // disabled/secondary text
const ImVec4 border = RGBA(0xD0CCC4); // subtle borders
// Tufte uses color sparingly: muted red for emphasis, navy for links
const ImVec4 red = RGBA(0xA00000); // restrained dark red
const ImVec4 blue = RGBA(0x1F3F6F); // dark navy
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 12.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f; // sharp edges — typographic, not app-like
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 0.0f; // minimal frame borders
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = dim;
colors[ImGuiCol_WindowBg] = paper;
colors[ImGuiCol_ChildBg] = paper;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = paper;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = bg2;
colors[ImGuiCol_ScrollbarGrabActive] = border;
colors[ImGuiCol_CheckMark] = ink;
colors[ImGuiCol_SliderGrab] = ink;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg2;
colors[ImGuiCol_ButtonHovered] = bg3;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg2;
colors[ImGuiCol_HeaderHovered] = bg3;
colors[ImGuiCol_HeaderActive] = bg3;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg3;
colors[ImGuiCol_SeparatorActive] = red;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = red;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = border;
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.15f);
colors[ImGuiCol_DragDropTarget] = red;
colors[ImGuiCol_NavHighlight] = red;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f);
colors[ImGuiCol_PlotLines] = blue;
colors[ImGuiCol_PlotLinesHovered] = red;
colors[ImGuiCol_PlotHistogram] = blue;
colors[ImGuiCol_PlotHistogramHovered] = red;
}
// Dark variant — warm charcoal with cream ink, same restrained accents
static inline void
ApplyTufteDarkTheme()
{
const ImVec4 bg0 = RGBA(0x1C1B19); // warm near-black
const ImVec4 bg1 = RGBA(0x252420); // slightly lighter
const ImVec4 bg2 = RGBA(0x302F2A); // UI elements
const ImVec4 bg3 = RGBA(0x3D3C36); // hover/active
const ImVec4 ink = RGBA(0xEAE6DE); // cream text (inverted paper)
const ImVec4 dim = RGBA(0x9A9690); // disabled text
const ImVec4 border = RGBA(0x4A4840); // subtle borders
const ImVec4 red = RGBA(0xD06060); // warmer red for dark bg
const ImVec4 blue = RGBA(0x7098C0); // lighter navy for dark bg
ImGuiStyle &style = ImGui::GetStyle();
style.WindowPadding = ImVec2(8.0f, 8.0f);
style.FramePadding = ImVec2(6.0f, 4.0f);
style.CellPadding = ImVec2(6.0f, 4.0f);
style.ItemSpacing = ImVec2(6.0f, 6.0f);
style.ItemInnerSpacing = ImVec2(6.0f, 4.0f);
style.ScrollbarSize = 12.0f;
style.GrabMinSize = 10.0f;
style.WindowRounding = 0.0f;
style.FrameRounding = 0.0f;
style.PopupRounding = 0.0f;
style.GrabRounding = 0.0f;
style.TabRounding = 0.0f;
style.WindowBorderSize = 1.0f;
style.FrameBorderSize = 0.0f;
ImVec4 *colors = style.Colors;
colors[ImGuiCol_Text] = ink;
colors[ImGuiCol_TextDisabled] = dim;
colors[ImGuiCol_WindowBg] = bg0;
colors[ImGuiCol_ChildBg] = bg0;
colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f);
colors[ImGuiCol_Border] = border;
colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f);
colors[ImGuiCol_FrameBg] = bg2;
colors[ImGuiCol_FrameBgHovered] = bg3;
colors[ImGuiCol_FrameBgActive] = bg1;
colors[ImGuiCol_TitleBg] = bg1;
colors[ImGuiCol_TitleBgActive] = bg2;
colors[ImGuiCol_TitleBgCollapsed] = bg1;
colors[ImGuiCol_MenuBarBg] = bg1;
colors[ImGuiCol_ScrollbarBg] = bg0;
colors[ImGuiCol_ScrollbarGrab] = bg3;
colors[ImGuiCol_ScrollbarGrabHovered] = border;
colors[ImGuiCol_ScrollbarGrabActive] = dim;
colors[ImGuiCol_CheckMark] = ink;
colors[ImGuiCol_SliderGrab] = ink;
colors[ImGuiCol_SliderGrabActive] = blue;
colors[ImGuiCol_Button] = bg2;
colors[ImGuiCol_ButtonHovered] = bg3;
colors[ImGuiCol_ButtonActive] = bg1;
colors[ImGuiCol_Header] = bg2;
colors[ImGuiCol_HeaderHovered] = bg3;
colors[ImGuiCol_HeaderActive] = bg3;
colors[ImGuiCol_Separator] = border;
colors[ImGuiCol_SeparatorHovered] = bg3;
colors[ImGuiCol_SeparatorActive] = red;
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
colors[ImGuiCol_ResizeGripHovered] = ImVec4(red.x, red.y, red.z, 0.50f);
colors[ImGuiCol_ResizeGripActive] = red;
colors[ImGuiCol_Tab] = bg2;
colors[ImGuiCol_TabHovered] = bg1;
colors[ImGuiCol_TabActive] = bg3;
colors[ImGuiCol_TabUnfocused] = bg2;
colors[ImGuiCol_TabUnfocusedActive] = bg3;
colors[ImGuiCol_TableHeaderBg] = bg2;
colors[ImGuiCol_TableBorderStrong] = border;
colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f);
colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f);
colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f);
colors[ImGuiCol_TextSelectedBg] = ImVec4(red.x, red.y, red.z, 0.20f);
colors[ImGuiCol_DragDropTarget] = red;
colors[ImGuiCol_NavHighlight] = red;
colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f);
colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f);
colors[ImGuiCol_PlotLines] = blue;
colors[ImGuiCol_PlotLinesHovered] = red;
colors[ImGuiCol_PlotHistogram] = blue;
colors[ImGuiCol_PlotHistogramHovered] = red;
}