Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 23f04e4357 | |||
| 0585edad9e | |||
| 8712ea673d | |||
| 3148e16cf8 | |||
| 34eaa72033 | |||
| f49f1698f4 | |||
| f4b3188069 | |||
| 2571ab79c1 | |||
| d768e56727 | |||
| 11c523ad52 | |||
| c261261e26 | |||
| 27dcb41857 | |||
| bc3433e988 | |||
| 690c51b0f3 | |||
| 0d87bc0b25 | |||
| daeeecb342 | |||
| a428b204a0 | |||
| a21409e689 |
80
Buffer.cc
80
Buffer.cc
@@ -18,6 +18,9 @@
|
|||||||
#include "SwapRecorder.h"
|
#include "SwapRecorder.h"
|
||||||
#include "UndoSystem.h"
|
#include "UndoSystem.h"
|
||||||
#include "UndoTree.h"
|
#include "UndoTree.h"
|
||||||
|
#include "ErrorHandler.h"
|
||||||
|
#include "SyscallWrappers.h"
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
// For reconstructing highlighter state on copies
|
// For reconstructing highlighter state on copies
|
||||||
#include "syntax/HighlighterRegistry.h"
|
#include "syntax/HighlighterRegistry.h"
|
||||||
#include "syntax/NullHighlighter.h"
|
#include "syntax/NullHighlighter.h"
|
||||||
@@ -122,11 +125,11 @@ best_effort_fsync_dir(const std::string &path)
|
|||||||
std::filesystem::path dir = p.parent_path();
|
std::filesystem::path dir = p.parent_path();
|
||||||
if (dir.empty())
|
if (dir.empty())
|
||||||
return;
|
return;
|
||||||
int dfd = ::open(dir.c_str(), O_RDONLY);
|
int dfd = kte::syscall::Open(dir.c_str(), O_RDONLY);
|
||||||
if (dfd < 0)
|
if (dfd < 0)
|
||||||
return;
|
return;
|
||||||
(void) ::fsync(dfd);
|
(void) kte::syscall::Fsync(dfd);
|
||||||
(void) ::close(dfd);
|
(void) kte::syscall::Close(dfd);
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
// best-effort
|
// best-effort
|
||||||
}
|
}
|
||||||
@@ -146,9 +149,21 @@ atomic_write_file(const std::string &path, const char *data, std::size_t len, st
|
|||||||
// mkstemp requires a mutable buffer.
|
// mkstemp requires a mutable buffer.
|
||||||
std::vector<char> buf(tmpl_s.begin(), tmpl_s.end());
|
std::vector<char> buf(tmpl_s.begin(), tmpl_s.end());
|
||||||
buf.push_back('\0');
|
buf.push_back('\0');
|
||||||
int fd = ::mkstemp(buf.data());
|
|
||||||
if (fd < 0) {
|
// Retry on transient errors for temp file creation
|
||||||
err = std::string("Failed to create temp file for save: ") + std::strerror(errno);
|
int fd = -1;
|
||||||
|
auto mkstemp_fn = [&]() -> bool {
|
||||||
|
// Reset buffer for each retry attempt
|
||||||
|
buf.assign(tmpl_s.begin(), tmpl_s.end());
|
||||||
|
buf.push_back('\0');
|
||||||
|
fd = kte::syscall::Mkstemp(buf.data());
|
||||||
|
return fd >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!kte::RetryOnTransientError(mkstemp_fn, kte::RetryPolicy::Aggressive(), err)) {
|
||||||
|
if (fd < 0) {
|
||||||
|
err = std::string("Failed to create temp file for save: ") + std::strerror(errno) + err;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
std::string tmp_path(buf.data());
|
std::string tmp_path(buf.data());
|
||||||
@@ -156,17 +171,23 @@ atomic_write_file(const std::string &path, const char *data, std::size_t len, st
|
|||||||
// If the destination exists, carry over its permissions.
|
// If the destination exists, carry over its permissions.
|
||||||
struct stat dst_st{};
|
struct stat dst_st{};
|
||||||
if (::stat(path.c_str(), &dst_st) == 0) {
|
if (::stat(path.c_str(), &dst_st) == 0) {
|
||||||
(void) ::fchmod(fd, dst_st.st_mode);
|
(void) kte::syscall::Fchmod(fd, dst_st.st_mode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool ok = write_all_fd(fd, data, len, err);
|
bool ok = write_all_fd(fd, data, len, err);
|
||||||
if (ok) {
|
if (ok) {
|
||||||
if (::fsync(fd) != 0) {
|
// Retry fsync on transient errors
|
||||||
err = std::string("fsync failed: ") + std::strerror(errno);
|
auto fsync_fn = [&]() -> bool {
|
||||||
|
return kte::syscall::Fsync(fd) == 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::string fsync_err;
|
||||||
|
if (!kte::RetryOnTransientError(fsync_fn, kte::RetryPolicy::Aggressive(), fsync_err)) {
|
||||||
|
err = std::string("fsync failed: ") + std::strerror(errno) + fsync_err;
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(void) ::close(fd);
|
(void) kte::syscall::Close(fd);
|
||||||
|
|
||||||
if (ok) {
|
if (ok) {
|
||||||
if (::rename(tmp_path.c_str(), path.c_str()) != 0) {
|
if (::rename(tmp_path.c_str(), path.c_str()) != 0) {
|
||||||
@@ -411,17 +432,46 @@ Buffer::OpenFromFile(const std::string &path, std::string &err)
|
|||||||
std::ifstream in(norm, std::ios::in | std::ios::binary);
|
std::ifstream in(norm, std::ios::in | std::ios::binary);
|
||||||
if (!in) {
|
if (!in) {
|
||||||
err = "Failed to open file: " + norm;
|
err = "Failed to open file: " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read entire file into PieceTable as-is
|
// Read entire file into PieceTable as-is
|
||||||
std::string data;
|
std::string data;
|
||||||
in.seekg(0, std::ios::end);
|
in.seekg(0, std::ios::end);
|
||||||
|
if (!in) {
|
||||||
|
err = "Failed to seek to end of file: " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
auto sz = in.tellg();
|
auto sz = in.tellg();
|
||||||
|
if (sz < 0) {
|
||||||
|
err = "Failed to get file size: " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (sz > 0) {
|
if (sz > 0) {
|
||||||
data.resize(static_cast<std::size_t>(sz));
|
data.resize(static_cast<std::size_t>(sz));
|
||||||
in.seekg(0, std::ios::beg);
|
in.seekg(0, std::ios::beg);
|
||||||
|
if (!in) {
|
||||||
|
err = "Failed to seek to beginning of file: " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
in.read(data.data(), static_cast<std::streamsize>(data.size()));
|
||||||
|
if (!in && !in.eof()) {
|
||||||
|
err = "Failed to read file: " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// Validate we read the expected number of bytes
|
||||||
|
const std::streamsize bytes_read = in.gcount();
|
||||||
|
if (bytes_read != static_cast<std::streamsize>(data.size())) {
|
||||||
|
err = "Partial read of file (expected " + std::to_string(data.size()) +
|
||||||
|
" bytes, got " + std::to_string(bytes_read) + "): " + norm;
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, norm);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
content_.Clear();
|
content_.Clear();
|
||||||
if (!data.empty())
|
if (!data.empty())
|
||||||
@@ -464,8 +514,10 @@ Buffer::Save(std::string &err) const
|
|||||||
err = "Internal error: buffer materialization failed";
|
err = "Internal error: buffer materialization failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!atomic_write_file(filename_, data ? data : "", sz, err))
|
if (!atomic_write_file(filename_, data ? data : "", sz, err)) {
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, filename_);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
// Update observed on-disk identity after a successful save.
|
// Update observed on-disk identity after a successful save.
|
||||||
const_cast<Buffer *>(this)->RefreshOnDiskIdentity();
|
const_cast<Buffer *>(this)->RefreshOnDiskIdentity();
|
||||||
// Note: const method cannot change dirty_. Intentionally const to allow UI code
|
// Note: const method cannot change dirty_. Intentionally const to allow UI code
|
||||||
@@ -502,8 +554,10 @@ Buffer::SaveAs(const std::string &path, std::string &err)
|
|||||||
err = "Internal error: buffer materialization failed";
|
err = "Internal error: buffer materialization failed";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (!atomic_write_file(out_path, data ? data : "", sz, err))
|
if (!atomic_write_file(out_path, data ? data : "", sz, err)) {
|
||||||
|
kte::ErrorHandler::Instance().Error("Buffer", err, out_path);
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
filename_ = out_path;
|
filename_ = out_path;
|
||||||
is_file_backed_ = true;
|
is_file_backed_ = true;
|
||||||
@@ -760,4 +814,4 @@ const UndoSystem *
|
|||||||
Buffer::Undo() const
|
Buffer::Undo() const
|
||||||
{
|
{
|
||||||
return undo_sys_.get();
|
return undo_sys_.get();
|
||||||
}
|
}
|
||||||
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.6.6")
|
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.
|
||||||
@@ -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")
|
set(KTE_FONT_SIZE "18.0" CACHE STRING "Default font size for GUI")
|
||||||
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
option(KTE_UNDO_DEBUG "Enable undo instrumentation logs" OFF)
|
||||||
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
option(KTE_ENABLE_TREESITTER "Enable optional Tree-sitter highlighter adapter" OFF)
|
||||||
|
option(KTE_STATIC_LINK "Enable static linking on Linux" ON)
|
||||||
|
|
||||||
# Optionally enable AddressSanitizer (ASan)
|
# Optionally enable AddressSanitizer (ASan)
|
||||||
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
||||||
@@ -39,7 +40,6 @@ if (MSVC)
|
|||||||
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
add_compile_options("/W4" "$<$<CONFIG:RELEASE>:/O2>")
|
||||||
else ()
|
else ()
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
"-static"
|
|
||||||
"-Wall"
|
"-Wall"
|
||||||
"-Wextra"
|
"-Wextra"
|
||||||
"-Werror"
|
"-Werror"
|
||||||
@@ -142,6 +142,9 @@ set(COMMON_SOURCES
|
|||||||
HelpText.cc
|
HelpText.cc
|
||||||
KKeymap.cc
|
KKeymap.cc
|
||||||
Swap.cc
|
Swap.cc
|
||||||
|
ErrorHandler.cc
|
||||||
|
SyscallWrappers.cc
|
||||||
|
ErrorRecovery.cc
|
||||||
TerminalInputHandler.cc
|
TerminalInputHandler.cc
|
||||||
TerminalRenderer.cc
|
TerminalRenderer.cc
|
||||||
TerminalFrontend.cc
|
TerminalFrontend.cc
|
||||||
@@ -202,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
|
||||||
@@ -213,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
|
||||||
@@ -282,6 +288,11 @@ endif ()
|
|||||||
|
|
||||||
target_link_libraries(kte ${CURSES_LIBRARIES})
|
target_link_libraries(kte ${CURSES_LIBRARIES})
|
||||||
|
|
||||||
|
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||||
|
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||||
|
target_link_options(kte PRIVATE -static)
|
||||||
|
endif ()
|
||||||
|
|
||||||
if (KTE_ENABLE_TREESITTER)
|
if (KTE_ENABLE_TREESITTER)
|
||||||
# Users can provide their own tree-sitter include/lib via cache variables
|
# Users can provide their own tree-sitter include/lib via cache variables
|
||||||
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
set(TREESITTER_INCLUDE_DIR "" CACHE PATH "Path to tree-sitter include directory")
|
||||||
@@ -316,8 +327,10 @@ if (BUILD_TESTS)
|
|||||||
tests/test_swap_recorder.cc
|
tests/test_swap_recorder.cc
|
||||||
tests/test_swap_writer.cc
|
tests/test_swap_writer.cc
|
||||||
tests/test_swap_replay.cc
|
tests/test_swap_replay.cc
|
||||||
|
tests/test_swap_edge_cases.cc
|
||||||
tests/test_swap_recovery_prompt.cc
|
tests/test_swap_recovery_prompt.cc
|
||||||
tests/test_swap_cleanup.cc
|
tests/test_swap_cleanup.cc
|
||||||
|
tests/test_swap_cleanup2.cc
|
||||||
tests/test_swap_git_editor.cc
|
tests/test_swap_git_editor.cc
|
||||||
tests/test_piece_table.cc
|
tests/test_piece_table.cc
|
||||||
tests/test_search.cc
|
tests/test_search.cc
|
||||||
@@ -328,6 +341,8 @@ if (BUILD_TESTS)
|
|||||||
tests/test_visual_line_mode.cc
|
tests/test_visual_line_mode.cc
|
||||||
tests/test_benchmarks.cc
|
tests/test_benchmarks.cc
|
||||||
tests/test_migration_coverage.cc
|
tests/test_migration_coverage.cc
|
||||||
|
tests/test_smart_newline.cc
|
||||||
|
tests/test_reflow_undo.cc
|
||||||
|
|
||||||
# minimal engine sources required by Buffer
|
# minimal engine sources required by Buffer
|
||||||
PieceTable.cc
|
PieceTable.cc
|
||||||
@@ -336,6 +351,9 @@ if (BUILD_TESTS)
|
|||||||
Command.cc
|
Command.cc
|
||||||
HelpText.cc
|
HelpText.cc
|
||||||
Swap.cc
|
Swap.cc
|
||||||
|
ErrorHandler.cc
|
||||||
|
SyscallWrappers.cc
|
||||||
|
ErrorRecovery.cc
|
||||||
KKeymap.cc
|
KKeymap.cc
|
||||||
SwapRecorder.h
|
SwapRecorder.h
|
||||||
OptimizedSearch.cc
|
OptimizedSearch.cc
|
||||||
@@ -360,6 +378,11 @@ if (BUILD_TESTS)
|
|||||||
target_link_libraries(kte_tests ${TREESITTER_LIBRARY})
|
target_link_libraries(kte_tests ${TREESITTER_LIBRARY})
|
||||||
endif ()
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||||
|
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||||
|
target_link_options(kte_tests PRIVATE -static)
|
||||||
|
endif ()
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
if (BUILD_GUI)
|
if (BUILD_GUI)
|
||||||
@@ -399,6 +422,11 @@ if (BUILD_GUI)
|
|||||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
|
# Static linking on Linux only (macOS does not support static linking of system libraries)
|
||||||
|
if (NOT APPLE AND KTE_STATIC_LINK)
|
||||||
|
target_link_options(kge PRIVATE -static)
|
||||||
|
endif ()
|
||||||
|
|
||||||
# On macOS, build kge as a proper .app bundle
|
# On macOS, build kge as a proper .app bundle
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
# Define the icon file
|
# Define the icon file
|
||||||
|
|||||||
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).
|
||||||
210
Command.cc
210
Command.cc
@@ -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
|
static bool
|
||||||
cmd_center_on_cursor(CommandContext &ctx)
|
cmd_center_on_cursor(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -744,6 +752,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
if (buf->IsFileBacked()) {
|
if (buf->IsFileBacked()) {
|
||||||
if (buf->Save(err)) {
|
if (buf->Save(err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -751,6 +761,8 @@ cmd_save_and_quit(CommandContext &ctx)
|
|||||||
} else if (!buf->Filename().empty()) {
|
} else if (!buf->Filename().empty()) {
|
||||||
if (buf->SaveAs(buf->Filename(), err)) {
|
if (buf->SaveAs(buf->Filename(), err)) {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap())
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
return false;
|
return false;
|
||||||
@@ -1109,34 +1121,33 @@ cmd_theme_set_by_name(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_theme_set_by_name(CommandContext &ctx)
|
cmd_theme_set_by_name(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
|
|
||||||
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
# if defined(KTE_BUILD_GUI) && defined(KTE_USE_QT)
|
||||||
// Qt GUI build: schedule theme change for frontend
|
// Qt GUI build: schedule theme change for frontend
|
||||||
std::string name = ctx.arg;
|
std::string name = ctx.arg;
|
||||||
// trim spaces
|
// trim spaces
|
||||||
auto ltrim = [](std::string &s) {
|
auto ltrim = [](std::string &s) {
|
||||||
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
return !std::isspace(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) {
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
return !std::isspace(ch);
|
return !std::isspace(ch);
|
||||||
}).base(), s.end());
|
}).base(), s.end());
|
||||||
};
|
};
|
||||||
ltrim (name);
|
ltrim(name);
|
||||||
rtrim (name);
|
rtrim(name);
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
|
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
kte::gThemeChangeRequest= name;
|
kte::gThemeChangeRequest = name;
|
||||||
kte::gThemeChangePending=true;
|
kte::gThemeChangePending = true;
|
||||||
ctx.editor.SetStatus (std::string("Theme requested: ") + name);
|
ctx.editor.SetStatus(std::string("Theme requested: ") + name);
|
||||||
return true;
|
return true;
|
||||||
# else
|
# else
|
||||||
(void) ctx;
|
(void) ctx;
|
||||||
// No-op in terminal build
|
// No-op in terminal build
|
||||||
return true;
|
return true;
|
||||||
# endif
|
# endif
|
||||||
}
|
}
|
||||||
@@ -1324,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
|
||||||
@@ -1346,6 +1391,10 @@ cmd_background_set(const CommandContext &ctx)
|
|||||||
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) {
|
||||||
return (char) std::tolower(c);
|
return (char) std::tolower(c);
|
||||||
});
|
});
|
||||||
|
if (mode.empty()) {
|
||||||
|
ctx.editor.SetStatus(std::string("Background: ") + kte::BackgroundModeName());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (mode != "light" && mode != "dark") {
|
if (mode != "light" && mode != "dark") {
|
||||||
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
ctx.editor.SetStatus("background: expected 'light' or 'dark'");
|
||||||
return true;
|
return true;
|
||||||
@@ -1873,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;
|
||||||
@@ -1892,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) {
|
||||||
@@ -1912,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());
|
||||||
@@ -2255,10 +2321,8 @@ cmd_show_help(CommandContext &ctx)
|
|||||||
};
|
};
|
||||||
|
|
||||||
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
auto populate_from_text = [](Buffer &b, const std::string &text) {
|
||||||
// Clear existing rows
|
// Clear existing content
|
||||||
while (b.Nrows() > 0) {
|
b.replace_all_bytes("");
|
||||||
b.delete_row(0);
|
|
||||||
}
|
|
||||||
// Parse text and insert rows
|
// Parse text and insert rows
|
||||||
std::string line;
|
std::string line;
|
||||||
line.reserve(128);
|
line.reserve(128);
|
||||||
@@ -2563,6 +2627,10 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus(err);
|
ctx.editor.SetStatus(err);
|
||||||
} else {
|
} else {
|
||||||
buf->SetDirty(false);
|
buf->SetDirty(false);
|
||||||
|
if (auto *sm = ctx.editor.Swap()) {
|
||||||
|
sm->NotifyFilenameChanged(*buf);
|
||||||
|
sm->ResetJournal(*buf);
|
||||||
|
}
|
||||||
ctx.editor.SetStatus("Saved as " + value);
|
ctx.editor.SetStatus("Saved as " + value);
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->mark_saved();
|
u->mark_saved();
|
||||||
@@ -2949,6 +3017,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
|
static bool
|
||||||
cmd_backspace(CommandContext &ctx)
|
cmd_backspace(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -4624,7 +4744,14 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
new_lines.push_back("");
|
new_lines.push_back("");
|
||||||
|
|
||||||
// Replace paragraph lines via PieceTable-backed operations
|
// Replace paragraph lines via PieceTable-backed operations
|
||||||
|
UndoSystem *u = buf->Undo();
|
||||||
for (std::size_t i = para_end; i + 1 > para_start; --i) {
|
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));
|
buf->delete_row(static_cast<int>(i));
|
||||||
if (i == 0)
|
if (i == 0)
|
||||||
break; // prevent wrap on size_t
|
break; // prevent wrap on size_t
|
||||||
@@ -4633,6 +4760,12 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
std::size_t insert_y = para_start;
|
std::size_t insert_y = para_start;
|
||||||
for (const auto &ln: new_lines) {
|
for (const auto &ln: new_lines) {
|
||||||
buf->insert_row(static_cast<int>(insert_y), std::string_view(ln));
|
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;
|
insert_y += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4806,6 +4939,9 @@ InstallDefaultCommands()
|
|||||||
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text, false, true
|
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::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::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
|
||||||
CommandRegistry::Register({CommandId::DeleteChar, "delete-char", "Delete char at cursor", cmd_delete_char});
|
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});
|
CommandRegistry::Register({CommandId::KillToEOL, "kill-to-eol", "Delete to end of line", cmd_kill_to_eol});
|
||||||
@@ -4935,6 +5071,16 @@ InstallDefaultCommands()
|
|||||||
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
||||||
false, false
|
false, false
|
||||||
});
|
});
|
||||||
|
// GUI: new window
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::NewWindow, "new-window", "Open a new editor window (GUI only)", cmd_new_window,
|
||||||
|
false, false
|
||||||
|
});
|
||||||
|
// Edit mode toggle (public)
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::ToggleEditMode, "mode", "Toggle or set edit mode: code|writing",
|
||||||
|
cmd_toggle_edit_mode, true, false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -4991,4 +5137,4 @@ Execute(Editor &ed, const std::string &name, const std::string &arg, int count)
|
|||||||
return false;
|
return false;
|
||||||
CommandContext ctx{ed, arg, count};
|
CommandContext ctx{ed, arg, count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
11
Command.h
11
Command.h
@@ -38,6 +38,7 @@ enum class CommandId {
|
|||||||
// Editing
|
// Editing
|
||||||
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
InsertText, // arg: text to insert at cursor (UTF-8, no newlines)
|
||||||
Newline, // insert a newline at cursor
|
Newline, // insert a newline at cursor
|
||||||
|
SmartNewline, // insert a newline with auto-indent (Shift-Enter)
|
||||||
Backspace, // delete char before cursor (may join lines)
|
Backspace, // delete char before cursor (may join lines)
|
||||||
DeleteChar, // delete char at 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
|
KillToEOL, // delete from cursor to end of line; if at EOL, delete newline
|
||||||
@@ -110,6 +111,14 @@ enum class CommandId {
|
|||||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||||
// Viewport control
|
// Viewport control
|
||||||
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||||
|
// GUI: open a new editor window sharing the same buffer list
|
||||||
|
NewWindow,
|
||||||
|
// GUI: font size controls
|
||||||
|
FontZoomIn,
|
||||||
|
FontZoomOut,
|
||||||
|
FontZoomReset,
|
||||||
|
// Edit mode (code/writing)
|
||||||
|
ToggleEditMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -164,4 +173,4 @@ void InstallDefaultCommands();
|
|||||||
// Returns true if the command executed successfully.
|
// Returns true if the command executed successfully.
|
||||||
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
bool Execute(Editor &ed, CommandId id, const std::string &arg = std::string(), int count = 0);
|
||||||
|
|
||||||
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
bool Execute(Editor &ed, const std::string &name, const std::string &arg = std::string(), int count = 0);
|
||||||
75
Editor.cc
75
Editor.cc
@@ -69,20 +69,22 @@ Editor::SetStatus(const std::string &message)
|
|||||||
Buffer *
|
Buffer *
|
||||||
Editor::CurrentBuffer()
|
Editor::CurrentBuffer()
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const Buffer *
|
const Buffer *
|
||||||
Editor::CurrentBuffer() const
|
Editor::CurrentBuffer() const
|
||||||
{
|
{
|
||||||
if (buffers_.empty() || curbuf_ >= buffers_.size()) {
|
const auto &bufs = Buffers();
|
||||||
|
if (bufs.empty() || curbuf_ >= bufs.size()) {
|
||||||
return nullptr;
|
return nullptr;
|
||||||
}
|
}
|
||||||
return &buffers_[curbuf_];
|
return &bufs[curbuf_];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -117,8 +119,9 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
|
|
||||||
// Prepare list of other buffer paths
|
// Prepare list of other buffer paths
|
||||||
std::vector<std::vector<std::filesystem::path> > others;
|
std::vector<std::vector<std::filesystem::path> > others;
|
||||||
others.reserve(buffers_.size());
|
const auto &bufs = Buffers();
|
||||||
for (const auto &b: buffers_) {
|
others.reserve(bufs.size());
|
||||||
|
for (const auto &b: bufs) {
|
||||||
if (&b == &buf)
|
if (&b == &buf)
|
||||||
continue;
|
continue;
|
||||||
if (b.Filename().empty())
|
if (b.Filename().empty())
|
||||||
@@ -161,41 +164,44 @@ Editor::DisplayNameFor(const Buffer &buf) const
|
|||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(const Buffer &buf)
|
Editor::AddBuffer(const Buffer &buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(buf);
|
auto &bufs = Buffers();
|
||||||
|
bufs.push_back(buf);
|
||||||
// Attach swap recorder
|
// Attach swap recorder
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
swap_->Attach(&bufs.back());
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::size_t
|
std::size_t
|
||||||
Editor::AddBuffer(Buffer &&buf)
|
Editor::AddBuffer(Buffer &&buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(std::move(buf));
|
auto &bufs = Buffers();
|
||||||
|
bufs.push_back(std::move(buf));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->Attach(&buffers_.back());
|
swap_->Attach(&bufs.back());
|
||||||
buffers_.back().SetSwapRecorder(swap_->RecorderFor(&buffers_.back()));
|
bufs.back().SetSwapRecorder(swap_->RecorderFor(&bufs.back()));
|
||||||
}
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (bufs.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
return buffers_.size() - 1;
|
return bufs.size() - 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
Editor::OpenFile(const std::string &path, std::string &err)
|
Editor::OpenFile(const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
// If there is exactly one unnamed, empty, clean buffer, reuse it instead
|
// If the current buffer is an unnamed, empty, clean scratch buffer, reuse
|
||||||
// of creating a new one.
|
// it instead of creating a new one.
|
||||||
if (buffers_.size() == 1) {
|
auto &bufs_ref = Buffers();
|
||||||
Buffer &cur = buffers_[curbuf_];
|
if (!bufs_ref.empty() && curbuf_ < bufs_ref.size()) {
|
||||||
|
Buffer &cur = bufs_ref[curbuf_];
|
||||||
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
const bool unnamed = cur.Filename().empty() && !cur.IsFileBacked();
|
||||||
const bool clean = !cur.Dirty();
|
const bool clean = !cur.Dirty();
|
||||||
const std::size_t nrows = cur.Nrows();
|
const std::size_t nrows = cur.Nrows();
|
||||||
@@ -268,7 +274,7 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
// Add as a new buffer and switch to it
|
// Add as a new buffer and switch to it
|
||||||
std::size_t idx = AddBuffer(std::move(b));
|
std::size_t idx = AddBuffer(std::move(b));
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
swap_->NotifyFilenameChanged(buffers_[idx]);
|
swap_->NotifyFilenameChanged(Buffers()[idx]);
|
||||||
}
|
}
|
||||||
SwitchTo(idx);
|
SwitchTo(idx);
|
||||||
// Defensive: ensure any active prompt is closed after a successful open
|
// Defensive: ensure any active prompt is closed after a successful open
|
||||||
@@ -446,12 +452,13 @@ Editor::ProcessPendingOpens()
|
|||||||
bool
|
bool
|
||||||
Editor::SwitchTo(std::size_t index)
|
Editor::SwitchTo(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
curbuf_ = index;
|
curbuf_ = index;
|
||||||
// Robustness: ensure a valid highlighter is installed when switching buffers
|
// Robustness: ensure a valid highlighter is installed when switching buffers
|
||||||
Buffer &b = buffers_[curbuf_];
|
Buffer &b = bufs[curbuf_];
|
||||||
if (b.SyntaxEnabled()) {
|
if (b.SyntaxEnabled()) {
|
||||||
b.EnsureHighlighter();
|
b.EnsureHighlighter();
|
||||||
if (auto *eng = b.Highlighter()) {
|
if (auto *eng = b.Highlighter()) {
|
||||||
@@ -478,21 +485,22 @@ Editor::SwitchTo(std::size_t index)
|
|||||||
bool
|
bool
|
||||||
Editor::CloseBuffer(std::size_t index)
|
Editor::CloseBuffer(std::size_t index)
|
||||||
{
|
{
|
||||||
if (index >= buffers_.size()) {
|
auto &bufs = Buffers();
|
||||||
|
if (index >= bufs.size()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (swap_) {
|
if (swap_) {
|
||||||
// Always remove swap file when closing a buffer on normal exit.
|
// Always remove swap file when closing a buffer on normal exit.
|
||||||
// Swap files are for crash recovery; on clean close, we don't need them.
|
// Swap files are for crash recovery; on clean close, we don't need them.
|
||||||
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
// This prevents stale swap files from accumulating (e.g., when used as git editor).
|
||||||
swap_->Detach(&buffers_[index], true);
|
swap_->Detach(&bufs[index], true);
|
||||||
buffers_[index].SetSwapRecorder(nullptr);
|
bufs[index].SetSwapRecorder(nullptr);
|
||||||
}
|
}
|
||||||
buffers_.erase(buffers_.begin() + static_cast<std::ptrdiff_t>(index));
|
bufs.erase(bufs.begin() + static_cast<std::ptrdiff_t>(index));
|
||||||
if (buffers_.empty()) {
|
if (bufs.empty()) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
} else if (curbuf_ >= buffers_.size()) {
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
curbuf_ = buffers_.size() - 1;
|
curbuf_ = bufs.size() - 1;
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -516,7 +524,12 @@ Editor::Reset()
|
|||||||
// Reset close-confirm/save state
|
// Reset close-confirm/save state
|
||||||
close_confirm_pending_ = false;
|
close_confirm_pending_ = false;
|
||||||
close_after_save_ = false;
|
close_after_save_ = false;
|
||||||
buffers_.clear();
|
auto &bufs = Buffers();
|
||||||
|
if (swap_) {
|
||||||
|
for (auto &buf : bufs)
|
||||||
|
swap_->Detach(&buf, true);
|
||||||
|
}
|
||||||
|
bufs.clear();
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
44
Editor.h
44
Editor.h
@@ -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)
|
void SetQuitConfirmPending(bool on)
|
||||||
{
|
{
|
||||||
quit_confirm_pending_ = on;
|
quit_confirm_pending_ = on;
|
||||||
@@ -509,7 +521,7 @@ public:
|
|||||||
// Buffers
|
// Buffers
|
||||||
[[nodiscard]] std::size_t BufferCount() const
|
[[nodiscard]] std::size_t BufferCount() const
|
||||||
{
|
{
|
||||||
return buffers_.size();
|
return Buffers().size();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -519,6 +531,19 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Clamp curbuf_ to valid range. Call when the shared buffer list may
|
||||||
|
// have been modified by another editor (e.g., buffer closed in another window).
|
||||||
|
void ValidateBufferIndex()
|
||||||
|
{
|
||||||
|
const auto &bufs = Buffers();
|
||||||
|
if (bufs.empty()) {
|
||||||
|
curbuf_ = 0;
|
||||||
|
} else if (curbuf_ >= bufs.size()) {
|
||||||
|
curbuf_ = bufs.size() - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Buffer *CurrentBuffer();
|
Buffer *CurrentBuffer();
|
||||||
|
|
||||||
const Buffer *CurrentBuffer() const;
|
const Buffer *CurrentBuffer() const;
|
||||||
@@ -570,13 +595,22 @@ public:
|
|||||||
// Direct access when needed (try to prefer methods above)
|
// Direct access when needed (try to prefer methods above)
|
||||||
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
[[nodiscard]] const std::vector<Buffer> &Buffers() const
|
||||||
{
|
{
|
||||||
return buffers_;
|
return shared_buffers_ ? *shared_buffers_ : buffers_;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
std::vector<Buffer> &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,7 +662,8 @@ private:
|
|||||||
bool repeatable_ = false; // whether the next command is repeatable
|
bool repeatable_ = false; // whether the next command is repeatable
|
||||||
|
|
||||||
std::vector<Buffer> buffers_;
|
std::vector<Buffer> buffers_;
|
||||||
std::size_t curbuf_ = 0; // index into 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)
|
// Swap journaling manager (lifetime = editor)
|
||||||
std::unique_ptr<kte::SwapManager> swap_;
|
std::unique_ptr<kte::SwapManager> swap_;
|
||||||
@@ -639,6 +674,7 @@ private:
|
|||||||
|
|
||||||
// Quit state
|
// Quit state
|
||||||
bool quit_requested_ = false;
|
bool quit_requested_ = false;
|
||||||
|
bool new_window_requested_ = false;
|
||||||
bool quit_confirm_pending_ = false;
|
bool quit_confirm_pending_ = false;
|
||||||
bool close_confirm_pending_ = false; // awaiting y/N to save-before-close
|
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
|
bool close_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
||||||
|
|||||||
313
ErrorHandler.cc
Normal file
313
ErrorHandler.cc
Normal file
@@ -0,0 +1,313 @@
|
|||||||
|
#include "ErrorHandler.h"
|
||||||
|
#include <chrono>
|
||||||
|
#include <ctime>
|
||||||
|
#include <iomanip>
|
||||||
|
#include <sstream>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
ErrorHandler::ErrorHandler()
|
||||||
|
{
|
||||||
|
// Determine log file path: ~/.local/state/kte/error.log
|
||||||
|
const char *home = std::getenv("HOME");
|
||||||
|
if (home) {
|
||||||
|
fs::path log_dir = fs::path(home) / ".local" / "state" / "kte";
|
||||||
|
try {
|
||||||
|
if (!fs::exists(log_dir)) {
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No HOME, disable file logging
|
||||||
|
file_logging_enabled_ = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ErrorHandler::~ErrorHandler()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (log_file_ && log_file_->is_open()) {
|
||||||
|
log_file_->flush();
|
||||||
|
log_file_->close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ErrorHandler &
|
||||||
|
ErrorHandler::Instance()
|
||||||
|
{
|
||||||
|
static ErrorHandler instance;
|
||||||
|
return instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::Report(ErrorSeverity severity, const std::string &component,
|
||||||
|
const std::string &message, const std::string &context)
|
||||||
|
{
|
||||||
|
ErrorRecord record;
|
||||||
|
record.timestamp_ns = now_ns();
|
||||||
|
record.severity = severity;
|
||||||
|
record.component = component;
|
||||||
|
record.message = message;
|
||||||
|
record.context = context;
|
||||||
|
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
|
||||||
|
// Add to in-memory queue
|
||||||
|
errors_.push_back(record);
|
||||||
|
while (errors_.size() > 100) {
|
||||||
|
errors_.pop_front();
|
||||||
|
}
|
||||||
|
|
||||||
|
++total_error_count_;
|
||||||
|
if (severity == ErrorSeverity::Critical) {
|
||||||
|
++critical_error_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Write to log file if enabled
|
||||||
|
if (file_logging_enabled_) {
|
||||||
|
write_to_log(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::Info(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context)
|
||||||
|
{
|
||||||
|
Report(ErrorSeverity::Info, component, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::Warning(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context)
|
||||||
|
{
|
||||||
|
Report(ErrorSeverity::Warning, component, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::Error(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context)
|
||||||
|
{
|
||||||
|
Report(ErrorSeverity::Error, component, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::Critical(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context)
|
||||||
|
{
|
||||||
|
Report(ErrorSeverity::Critical, component, message, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
ErrorHandler::HasErrors() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return !errors_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
ErrorHandler::HasCriticalErrors() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return critical_error_count_ > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
ErrorHandler::GetLastError() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (errors_.empty())
|
||||||
|
return "";
|
||||||
|
|
||||||
|
const ErrorRecord &e = errors_.back();
|
||||||
|
std::string result = "[" + severity_to_string(e.severity) + "] ";
|
||||||
|
result += e.component;
|
||||||
|
if (!e.context.empty()) {
|
||||||
|
result += " (" + e.context + ")";
|
||||||
|
}
|
||||||
|
result += ": " + e.message;
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
ErrorHandler::GetErrorCount() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return total_error_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
ErrorHandler::GetErrorCount(ErrorSeverity severity) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
std::size_t count = 0;
|
||||||
|
for (const auto &e: errors_) {
|
||||||
|
if (e.severity == severity) {
|
||||||
|
++count;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return count;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::vector<ErrorHandler::ErrorRecord>
|
||||||
|
ErrorHandler::GetRecentErrors(std::size_t max_count) const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
std::vector<ErrorRecord> result;
|
||||||
|
result.reserve(std::min(max_count, errors_.size()));
|
||||||
|
|
||||||
|
// Return most recent first
|
||||||
|
auto it = errors_.rbegin();
|
||||||
|
for (std::size_t i = 0; i < max_count && it != errors_.rend(); ++i, ++it) {
|
||||||
|
result.push_back(*it);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::ClearErrors()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
errors_.clear();
|
||||||
|
total_error_count_ = 0;
|
||||||
|
critical_error_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::SetFileLoggingEnabled(bool enabled)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
file_logging_enabled_ = enabled;
|
||||||
|
if (!enabled && log_file_ && log_file_->is_open()) {
|
||||||
|
log_file_->flush();
|
||||||
|
log_file_->close();
|
||||||
|
log_file_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
ErrorHandler::GetLogFilePath() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return log_file_path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::write_to_log(const ErrorRecord &record)
|
||||||
|
{
|
||||||
|
// Must be called with mtx_ held
|
||||||
|
if (log_file_path_.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
ensure_log_file();
|
||||||
|
if (!log_file_ || !log_file_->is_open())
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Format: [timestamp] [SEVERITY] component (context): message
|
||||||
|
std::string timestamp = format_timestamp(record.timestamp_ns);
|
||||||
|
std::string severity = severity_to_string(record.severity);
|
||||||
|
|
||||||
|
*log_file_ << "[" << timestamp << "] [" << severity << "] " << record.component;
|
||||||
|
if (!record.context.empty()) {
|
||||||
|
*log_file_ << " (" << record.context << ")";
|
||||||
|
}
|
||||||
|
*log_file_ << ": " << record.message << "\n";
|
||||||
|
log_file_->flush();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
ErrorHandler::ensure_log_file()
|
||||||
|
{
|
||||||
|
// Must be called with mtx_ held
|
||||||
|
if (log_file_ && log_file_->is_open())
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (log_file_path_.empty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
log_file_ = std::make_unique<std::ofstream>(log_file_path_,
|
||||||
|
std::ios::app | std::ios::out);
|
||||||
|
if (!log_file_->is_open()) {
|
||||||
|
log_file_.reset();
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
log_file_.reset();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
ErrorHandler::format_timestamp(std::uint64_t timestamp_ns) const
|
||||||
|
{
|
||||||
|
// Convert nanoseconds to time_t (seconds)
|
||||||
|
std::time_t seconds = static_cast<std::time_t>(timestamp_ns / 1000000000ULL);
|
||||||
|
std::uint64_t nanos = timestamp_ns % 1000000000ULL;
|
||||||
|
|
||||||
|
std::tm tm_buf{};
|
||||||
|
#if defined(_WIN32)
|
||||||
|
localtime_s(&tm_buf, &seconds);
|
||||||
|
#else
|
||||||
|
localtime_r(&seconds, &tm_buf);
|
||||||
|
#endif
|
||||||
|
|
||||||
|
std::ostringstream oss;
|
||||||
|
oss << std::put_time(&tm_buf, "%Y-%m-%d %H:%M:%S");
|
||||||
|
oss << "." << std::setfill('0') << std::setw(3) << (nanos / 1000000ULL);
|
||||||
|
return oss.str();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
ErrorHandler::severity_to_string(ErrorSeverity severity) const
|
||||||
|
{
|
||||||
|
switch (severity) {
|
||||||
|
case ErrorSeverity::Info:
|
||||||
|
return "INFO";
|
||||||
|
case ErrorSeverity::Warning:
|
||||||
|
return "WARNING";
|
||||||
|
case ErrorSeverity::Error:
|
||||||
|
return "ERROR";
|
||||||
|
case ErrorSeverity::Critical:
|
||||||
|
return "CRITICAL";
|
||||||
|
default:
|
||||||
|
return "UNKNOWN";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::uint64_t
|
||||||
|
ErrorHandler::now_ns()
|
||||||
|
{
|
||||||
|
using namespace std::chrono;
|
||||||
|
return duration_cast<nanoseconds>(system_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
106
ErrorHandler.h
Normal file
106
ErrorHandler.h
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
// ErrorHandler.h - Centralized error handling and logging for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
#include <deque>
|
||||||
|
#include <mutex>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <memory>
|
||||||
|
#include <fstream>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
enum class ErrorSeverity {
|
||||||
|
Info, // Informational messages
|
||||||
|
Warning, // Non-critical issues
|
||||||
|
Error, // Errors that affect functionality but allow continuation
|
||||||
|
Critical // Critical errors that may cause data loss or crashes
|
||||||
|
};
|
||||||
|
|
||||||
|
// Centralized error handler with logging and in-memory error tracking
|
||||||
|
class ErrorHandler {
|
||||||
|
public:
|
||||||
|
struct ErrorRecord {
|
||||||
|
std::uint64_t timestamp_ns{0};
|
||||||
|
ErrorSeverity severity{ErrorSeverity::Error};
|
||||||
|
std::string component; // e.g., "SwapManager", "Buffer", "main"
|
||||||
|
std::string message;
|
||||||
|
std::string context; // e.g., filename, buffer name, operation
|
||||||
|
};
|
||||||
|
|
||||||
|
// Get the global ErrorHandler instance
|
||||||
|
static ErrorHandler &Instance();
|
||||||
|
|
||||||
|
// Report an error with severity, component, message, and optional context
|
||||||
|
void Report(ErrorSeverity severity, const std::string &component,
|
||||||
|
const std::string &message, const std::string &context = "");
|
||||||
|
|
||||||
|
// Convenience methods for common severity levels
|
||||||
|
void Info(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context = "");
|
||||||
|
|
||||||
|
void Warning(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context = "");
|
||||||
|
|
||||||
|
void Error(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context = "");
|
||||||
|
|
||||||
|
void Critical(const std::string &component, const std::string &message,
|
||||||
|
const std::string &context = "");
|
||||||
|
|
||||||
|
// Query error state (thread-safe)
|
||||||
|
bool HasErrors() const;
|
||||||
|
|
||||||
|
bool HasCriticalErrors() const;
|
||||||
|
|
||||||
|
std::string GetLastError() const;
|
||||||
|
|
||||||
|
std::size_t GetErrorCount() const;
|
||||||
|
|
||||||
|
std::size_t GetErrorCount(ErrorSeverity severity) const;
|
||||||
|
|
||||||
|
// Get recent errors (up to max_count, most recent first)
|
||||||
|
std::vector<ErrorRecord> GetRecentErrors(std::size_t max_count = 10) const;
|
||||||
|
|
||||||
|
// Clear in-memory error history (does not affect log file)
|
||||||
|
void ClearErrors();
|
||||||
|
|
||||||
|
// Enable/disable file logging (enabled by default)
|
||||||
|
void SetFileLoggingEnabled(bool enabled);
|
||||||
|
|
||||||
|
// Get the path to the error log file
|
||||||
|
std::string GetLogFilePath() const;
|
||||||
|
|
||||||
|
private:
|
||||||
|
ErrorHandler();
|
||||||
|
|
||||||
|
~ErrorHandler();
|
||||||
|
|
||||||
|
// Non-copyable, non-movable
|
||||||
|
ErrorHandler(const ErrorHandler &) = delete;
|
||||||
|
|
||||||
|
ErrorHandler &operator=(const ErrorHandler &) = delete;
|
||||||
|
|
||||||
|
ErrorHandler(ErrorHandler &&) = delete;
|
||||||
|
|
||||||
|
ErrorHandler &operator=(ErrorHandler &&) = delete;
|
||||||
|
|
||||||
|
void write_to_log(const ErrorRecord &record);
|
||||||
|
|
||||||
|
void ensure_log_file();
|
||||||
|
|
||||||
|
std::string format_timestamp(std::uint64_t timestamp_ns) const;
|
||||||
|
|
||||||
|
std::string severity_to_string(ErrorSeverity severity) const;
|
||||||
|
|
||||||
|
static std::uint64_t now_ns();
|
||||||
|
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
std::deque<ErrorRecord> errors_; // bounded to max 100 entries
|
||||||
|
std::size_t total_error_count_{0};
|
||||||
|
std::size_t critical_error_count_{0};
|
||||||
|
bool file_logging_enabled_{true};
|
||||||
|
std::string log_file_path_;
|
||||||
|
std::unique_ptr<std::ofstream> log_file_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
157
ErrorRecovery.cc
Normal file
157
ErrorRecovery.cc
Normal file
@@ -0,0 +1,157 @@
|
|||||||
|
// ErrorRecovery.cc - Error recovery mechanisms implementation
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
|
#include <mutex>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
CircuitBreaker::CircuitBreaker(const Config &cfg)
|
||||||
|
: config_(cfg), state_(State::Closed), failure_count_(0), success_count_(0),
|
||||||
|
last_failure_time_(std::chrono::steady_clock::time_point::min()),
|
||||||
|
state_change_time_(std::chrono::steady_clock::now()) {}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
CircuitBreaker::AllowRequest()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
switch (state_) {
|
||||||
|
case State::Closed:
|
||||||
|
// Normal operation, allow all requests
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case State::Open: {
|
||||||
|
// Check if timeout has elapsed to transition to HalfOpen
|
||||||
|
const auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - state_change_time_
|
||||||
|
);
|
||||||
|
if (elapsed >= config_.open_timeout) {
|
||||||
|
TransitionTo(State::HalfOpen);
|
||||||
|
return true; // Allow one request to test recovery
|
||||||
|
}
|
||||||
|
return false; // Circuit is open, reject request
|
||||||
|
}
|
||||||
|
|
||||||
|
case State::HalfOpen:
|
||||||
|
// Allow limited requests to test recovery
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
CircuitBreaker::RecordSuccess()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
|
||||||
|
switch (state_) {
|
||||||
|
case State::Closed:
|
||||||
|
// Reset failure count on success in normal operation
|
||||||
|
failure_count_ = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::HalfOpen:
|
||||||
|
++success_count_;
|
||||||
|
if (success_count_ >= config_.success_threshold) {
|
||||||
|
// Enough successes, close the circuit
|
||||||
|
TransitionTo(State::Closed);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Open:
|
||||||
|
// Shouldn't happen (requests rejected), but handle gracefully
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
CircuitBreaker::RecordFailure()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
last_failure_time_ = now;
|
||||||
|
|
||||||
|
switch (state_) {
|
||||||
|
case State::Closed:
|
||||||
|
// Check if we need to reset the failure count (window expired)
|
||||||
|
if (IsWindowExpired()) {
|
||||||
|
failure_count_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
++failure_count_;
|
||||||
|
if (failure_count_ >= config_.failure_threshold) {
|
||||||
|
// Too many failures, open the circuit
|
||||||
|
TransitionTo(State::Open);
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::HalfOpen:
|
||||||
|
// Failure during recovery test, reopen the circuit
|
||||||
|
TransitionTo(State::Open);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Open:
|
||||||
|
// Already open, just track the failure
|
||||||
|
++failure_count_;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
CircuitBreaker::Reset()
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
TransitionTo(State::Closed);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
CircuitBreaker::TransitionTo(State new_state)
|
||||||
|
{
|
||||||
|
if (state_ == new_state) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state_ = new_state;
|
||||||
|
state_change_time_ = std::chrono::steady_clock::now();
|
||||||
|
|
||||||
|
switch (new_state) {
|
||||||
|
case State::Closed:
|
||||||
|
failure_count_ = 0;
|
||||||
|
success_count_ = 0;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::Open:
|
||||||
|
success_count_ = 0;
|
||||||
|
// Keep failure_count_ for diagnostics
|
||||||
|
break;
|
||||||
|
|
||||||
|
case State::HalfOpen:
|
||||||
|
success_count_ = 0;
|
||||||
|
// Keep failure_count_ for diagnostics
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
CircuitBreaker::IsWindowExpired() const
|
||||||
|
{
|
||||||
|
if (failure_count_ == 0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
const auto elapsed = std::chrono::duration_cast<std::chrono::seconds>(
|
||||||
|
now - last_failure_time_
|
||||||
|
);
|
||||||
|
|
||||||
|
return elapsed >= config_.window;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
170
ErrorRecovery.h
Normal file
170
ErrorRecovery.h
Normal file
@@ -0,0 +1,170 @@
|
|||||||
|
// ErrorRecovery.h - Error recovery mechanisms for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <functional>
|
||||||
|
#include <string>
|
||||||
|
#include <thread>
|
||||||
|
#include <mutex>
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Classify errno values as transient (retryable) or permanent
|
||||||
|
inline bool
|
||||||
|
IsTransientError(int err)
|
||||||
|
{
|
||||||
|
switch (err) {
|
||||||
|
case EAGAIN:
|
||||||
|
#if EAGAIN != EWOULDBLOCK
|
||||||
|
case EWOULDBLOCK:
|
||||||
|
#endif
|
||||||
|
case EBUSY:
|
||||||
|
case EIO: // I/O error (may be transient on network filesystems)
|
||||||
|
case ETIMEDOUT:
|
||||||
|
case ENOSPC: // Disk full (may become available)
|
||||||
|
case EDQUOT: // Quota exceeded (may become available)
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RetryPolicy defines retry behavior for transient failures
|
||||||
|
struct RetryPolicy {
|
||||||
|
std::size_t max_attempts{3}; // Maximum retry attempts
|
||||||
|
std::chrono::milliseconds initial_delay{100}; // Initial delay before first retry
|
||||||
|
double backoff_multiplier{2.0}; // Exponential backoff multiplier
|
||||||
|
std::chrono::milliseconds max_delay{5000}; // Maximum delay between retries
|
||||||
|
|
||||||
|
// Default policy: 3 attempts, 100ms initial, 2x backoff, 5s max
|
||||||
|
static RetryPolicy Default()
|
||||||
|
{
|
||||||
|
return RetryPolicy{};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Aggressive policy for critical operations: more attempts, faster retries
|
||||||
|
static RetryPolicy Aggressive()
|
||||||
|
{
|
||||||
|
return RetryPolicy{5, std::chrono::milliseconds(50), 1.5, std::chrono::milliseconds(2000)};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Conservative policy for non-critical operations: fewer attempts, slower retries
|
||||||
|
static RetryPolicy Conservative()
|
||||||
|
{
|
||||||
|
return RetryPolicy{2, std::chrono::milliseconds(200), 2.5, std::chrono::milliseconds(10000)};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Retry a function with exponential backoff for transient errors
|
||||||
|
// Returns true on success, false on permanent failure or exhausted retries
|
||||||
|
// The function `fn` should return true on success, false on failure, and set errno on failure
|
||||||
|
template<typename Func>
|
||||||
|
bool
|
||||||
|
RetryOnTransientError(Func fn, const RetryPolicy &policy, std::string &err)
|
||||||
|
{
|
||||||
|
std::size_t attempt = 0;
|
||||||
|
std::chrono::milliseconds delay = policy.initial_delay;
|
||||||
|
|
||||||
|
while (attempt < policy.max_attempts) {
|
||||||
|
++attempt;
|
||||||
|
errno = 0;
|
||||||
|
if (fn()) {
|
||||||
|
return true; // Success
|
||||||
|
}
|
||||||
|
|
||||||
|
int saved_errno = errno;
|
||||||
|
if (!IsTransientError(saved_errno)) {
|
||||||
|
// Permanent error, don't retry
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt >= policy.max_attempts) {
|
||||||
|
// Exhausted retries
|
||||||
|
err += " (exhausted " + std::to_string(policy.max_attempts) + " retry attempts)";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sleep before retry
|
||||||
|
std::this_thread::sleep_for(delay);
|
||||||
|
|
||||||
|
// Exponential backoff
|
||||||
|
delay = std::chrono::milliseconds(
|
||||||
|
static_cast<long long>(delay.count() * policy.backoff_multiplier)
|
||||||
|
);
|
||||||
|
if (delay > policy.max_delay) {
|
||||||
|
delay = policy.max_delay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// CircuitBreaker prevents repeated attempts to failing operations
|
||||||
|
// States: Closed (normal), Open (failing, reject immediately), HalfOpen (testing recovery)
|
||||||
|
class CircuitBreaker {
|
||||||
|
public:
|
||||||
|
enum class State {
|
||||||
|
Closed, // Normal operation, allow all requests
|
||||||
|
Open, // Failing, reject requests immediately
|
||||||
|
HalfOpen // Testing recovery, allow limited requests
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Config {
|
||||||
|
std::size_t failure_threshold; // Failures before opening circuit
|
||||||
|
std::chrono::seconds open_timeout; // Time before attempting recovery (Open → HalfOpen)
|
||||||
|
std::size_t success_threshold; // Successes in HalfOpen before closing
|
||||||
|
std::chrono::seconds window; // Time window for counting failures
|
||||||
|
|
||||||
|
Config()
|
||||||
|
: failure_threshold(5), open_timeout(30), success_threshold(2), window(60) {}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
explicit CircuitBreaker(const Config &cfg = Config());
|
||||||
|
|
||||||
|
|
||||||
|
// Check if operation is allowed (returns false if circuit is Open)
|
||||||
|
bool AllowRequest();
|
||||||
|
|
||||||
|
// Record successful operation
|
||||||
|
void RecordSuccess();
|
||||||
|
|
||||||
|
// Record failed operation
|
||||||
|
void RecordFailure();
|
||||||
|
|
||||||
|
// Get current state
|
||||||
|
State GetState() const
|
||||||
|
{
|
||||||
|
return state_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Get failure count in current window
|
||||||
|
std::size_t GetFailureCount() const
|
||||||
|
{
|
||||||
|
return failure_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Reset circuit to Closed state (for testing or manual intervention)
|
||||||
|
void Reset();
|
||||||
|
|
||||||
|
private:
|
||||||
|
void TransitionTo(State new_state);
|
||||||
|
|
||||||
|
bool IsWindowExpired() const;
|
||||||
|
|
||||||
|
Config config_;
|
||||||
|
State state_;
|
||||||
|
std::size_t failure_count_;
|
||||||
|
std::size_t success_count_;
|
||||||
|
std::chrono::steady_clock::time_point last_failure_time_;
|
||||||
|
std::chrono::steady_clock::time_point state_change_time_;
|
||||||
|
mutable std::mutex mtx_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
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);
|
||||||
};
|
};
|
||||||
|
|||||||
206
GUITheme.h
206
GUITheme.h
@@ -312,7 +312,7 @@ namespace kte {
|
|||||||
enum class BackgroundMode { Light, Dark };
|
enum class BackgroundMode { Light, Dark };
|
||||||
|
|
||||||
// Global background mode; default to Dark to match prior defaults
|
// Global background mode; default to Dark to match prior defaults
|
||||||
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||||
|
|
||||||
// Basic theme identifier (kept minimal; some ids are aliases)
|
// Basic theme identifier (kept minimal; some ids are aliases)
|
||||||
enum class ThemeId {
|
enum class ThemeId {
|
||||||
@@ -330,11 +330,13 @@ enum class ThemeId {
|
|||||||
Amber = 10,
|
Amber = 10,
|
||||||
WeylandYutani = 11,
|
WeylandYutani = 11,
|
||||||
Orbital = 12,
|
Orbital = 12,
|
||||||
|
Tufte = 13,
|
||||||
|
Leuchtturm = 14,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Current theme tracking
|
// Current theme tracking
|
||||||
static inline auto gCurrentTheme = ThemeId::Nord;
|
inline auto gCurrentTheme = ThemeId::Nord;
|
||||||
static inline std::size_t gCurrentThemeIndex = 6; // Nord index
|
inline std::size_t gCurrentThemeIndex = 7; // Nord index
|
||||||
|
|
||||||
// Forward declarations for helpers used below
|
// Forward declarations for helpers used below
|
||||||
static size_t ThemeIndexFromId(ThemeId id);
|
static size_t ThemeIndexFromId(ThemeId id);
|
||||||
@@ -372,11 +374,13 @@ BackgroundModeName()
|
|||||||
#include "themes/Everforest.h"
|
#include "themes/Everforest.h"
|
||||||
#include "themes/KanagawaPaper.h"
|
#include "themes/KanagawaPaper.h"
|
||||||
#include "themes/LCARS.h"
|
#include "themes/LCARS.h"
|
||||||
|
#include "themes/Leuchtturm.h"
|
||||||
#include "themes/OldBook.h"
|
#include "themes/OldBook.h"
|
||||||
#include "themes/Amber.h"
|
#include "themes/Amber.h"
|
||||||
#include "themes/WeylandYutani.h"
|
#include "themes/WeylandYutani.h"
|
||||||
#include "themes/Zenburn.h"
|
#include "themes/Zenburn.h"
|
||||||
#include "themes/Orbital.h"
|
#include "themes/Orbital.h"
|
||||||
|
#include "themes/Tufte.h"
|
||||||
|
|
||||||
|
|
||||||
// Theme abstraction and registry (generalized theme system)
|
// Theme abstraction and registry (generalized theme system)
|
||||||
@@ -409,6 +413,28 @@ struct LCARSTheme final : Theme {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
struct LeuchtturmTheme final : Theme {
|
||||||
|
[[nodiscard]] const char *Name() const override
|
||||||
|
{
|
||||||
|
return "leuchtturm";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Apply() const override
|
||||||
|
{
|
||||||
|
if (gBackgroundMode == BackgroundMode::Dark)
|
||||||
|
ApplyLeuchtturmDarkTheme();
|
||||||
|
else
|
||||||
|
ApplyLeuchtturmLightTheme();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
ThemeId Id() override
|
||||||
|
{
|
||||||
|
return ThemeId::Leuchtturm;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
struct EverforestTheme final : Theme {
|
struct EverforestTheme final : Theme {
|
||||||
[[nodiscard]] const char *Name() const override
|
[[nodiscard]] const char *Name() const override
|
||||||
{
|
{
|
||||||
@@ -488,6 +514,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 {
|
struct ZenburnTheme final : Theme {
|
||||||
[[nodiscard]] const char *Name() const override
|
[[nodiscard]] const char *Name() const override
|
||||||
{
|
{
|
||||||
@@ -657,18 +705,20 @@ ThemeRegistry()
|
|||||||
static std::vector<std::unique_ptr<Theme> > reg;
|
static std::vector<std::unique_ptr<Theme> > reg;
|
||||||
if (reg.empty()) {
|
if (reg.empty()) {
|
||||||
// Alphabetical by canonical name:
|
// 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, leuchtturm, 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::AmberTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
reg.emplace_back(std::make_unique<detail::EInkTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
reg.emplace_back(std::make_unique<detail::EverforestTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
reg.emplace_back(std::make_unique<detail::GruvboxTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::KanagawaPaperTheme>());
|
reg.emplace_back(std::make_unique<detail::KanagawaPaperTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::LCARSTheme>());
|
reg.emplace_back(std::make_unique<detail::LCARSTheme>());
|
||||||
|
reg.emplace_back(std::make_unique<detail::LeuchtturmTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
reg.emplace_back(std::make_unique<detail::NordTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::OldBookTheme>());
|
reg.emplace_back(std::make_unique<detail::OldBookTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
reg.emplace_back(std::make_unique<detail::OrbitalTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
reg.emplace_back(std::make_unique<detail::Plan9Theme>());
|
||||||
reg.emplace_back(std::make_unique<detail::SolarizedTheme>());
|
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::WeylandYutaniTheme>());
|
||||||
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
reg.emplace_back(std::make_unique<detail::ZenburnTheme>());
|
||||||
}
|
}
|
||||||
@@ -845,20 +895,24 @@ ThemeIndexFromId(const ThemeId id)
|
|||||||
return 4;
|
return 4;
|
||||||
case ThemeId::LCARS:
|
case ThemeId::LCARS:
|
||||||
return 5;
|
return 5;
|
||||||
case ThemeId::Nord:
|
case ThemeId::Leuchtturm:
|
||||||
return 6;
|
return 6;
|
||||||
case ThemeId::OldBook:
|
case ThemeId::Nord:
|
||||||
return 7;
|
return 7;
|
||||||
case ThemeId::Orbital:
|
case ThemeId::OldBook:
|
||||||
return 8;
|
return 8;
|
||||||
case ThemeId::Plan9:
|
case ThemeId::Orbital:
|
||||||
return 9;
|
return 9;
|
||||||
case ThemeId::Solarized:
|
case ThemeId::Plan9:
|
||||||
return 10;
|
return 10;
|
||||||
case ThemeId::WeylandYutani:
|
case ThemeId::Solarized:
|
||||||
return 11;
|
return 11;
|
||||||
case ThemeId::Zenburn:
|
case ThemeId::Tufte:
|
||||||
return 12;
|
return 12;
|
||||||
|
case ThemeId::WeylandYutani:
|
||||||
|
return 13;
|
||||||
|
case ThemeId::Zenburn:
|
||||||
|
return 14;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -882,30 +936,144 @@ ThemeIdFromIndex(const size_t idx)
|
|||||||
case 5:
|
case 5:
|
||||||
return ThemeId::LCARS;
|
return ThemeId::LCARS;
|
||||||
case 6:
|
case 6:
|
||||||
return ThemeId::Nord;
|
return ThemeId::Leuchtturm;
|
||||||
case 7:
|
case 7:
|
||||||
return ThemeId::OldBook;
|
return ThemeId::Nord;
|
||||||
case 8:
|
case 8:
|
||||||
return ThemeId::Orbital;
|
return ThemeId::OldBook;
|
||||||
case 9:
|
case 9:
|
||||||
return ThemeId::Plan9;
|
return ThemeId::Orbital;
|
||||||
case 10:
|
case 10:
|
||||||
return ThemeId::Solarized;
|
return ThemeId::Plan9;
|
||||||
case 11:
|
case 11:
|
||||||
return ThemeId::WeylandYutani;
|
return ThemeId::Solarized;
|
||||||
case 12:
|
case 12:
|
||||||
|
return ThemeId::Tufte;
|
||||||
|
case 13:
|
||||||
|
return ThemeId::WeylandYutani;
|
||||||
|
case 14:
|
||||||
return ThemeId::Zenburn;
|
return ThemeId::Zenburn;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
// --- Syntax palette (v1): map TokenKind to ink color per current theme/background ---
|
||||||
|
|
||||||
|
// Tufte palette: high-contrast, restrained color. Body text is true black on
|
||||||
|
// cream; only keywords and links get subtle color to avoid a "christmas tree."
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkTufte(const TokenKind k, const bool dark)
|
||||||
|
{
|
||||||
|
const ImVec4 ink = dark ? RGBA(0xEAE6DE) : RGBA(0x111111); // body text
|
||||||
|
const ImVec4 dim = dark ? RGBA(0x8A8680) : RGBA(0x555555); // comments
|
||||||
|
const ImVec4 red = dark ? RGBA(0xD06060) : RGBA(0x8B0000); // keywords/preproc
|
||||||
|
const ImVec4 navy = dark ? RGBA(0x7098C0) : RGBA(0x1A3A5C); // functions/links
|
||||||
|
const ImVec4 grn = dark ? RGBA(0x8AAA6E) : RGBA(0x2E5E2E); // strings
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return red;
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return grn;
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dim;
|
||||||
|
case TokenKind::Function:
|
||||||
|
return navy;
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xC8A85A) : RGBA(0x6B4C00);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0xBBAA90) : RGBA(0x333333);
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xD06060) : RGBA(0xCC0000);
|
||||||
|
default:
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Leuchtturm palette: blue-black fountain pen ink with brass and bronze accents.
|
||||||
|
// Body text is ink-colored; accents drawn from the pen metals.
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkLeuchtturm(const TokenKind k, const bool dark)
|
||||||
|
{
|
||||||
|
const ImVec4 ink = dark ? RGBA(0xE5DDD0) : RGBA(0x040720); // fountain pen ink
|
||||||
|
const ImVec4 dim = dark ? RGBA(0x7A7060) : RGBA(0x6A6558); // comments
|
||||||
|
const ImVec4 brass = dark ? RGBA(0xB8A060) : RGBA(0x504518); // patinated brass
|
||||||
|
const ImVec4 bronze= dark ? RGBA(0xC08050) : RGBA(0x5C3010); // dark bronze
|
||||||
|
const ImVec4 navy = dark ? RGBA(0x8898B0) : RGBA(0x1C2E4A); // deep navy
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return brass;
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return bronze;
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dim;
|
||||||
|
case TokenKind::Function:
|
||||||
|
return navy;
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xA89060) : RGBA(0x483C10);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0xC0B898) : RGBA(0x222238);
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xD06060) : RGBA(0xA02020);
|
||||||
|
default:
|
||||||
|
return ink;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Everforest: warm forest palette on dark green-gray (bg 0x2B3339).
|
||||||
|
// Default comment color (0x616E88) is too dim; boost it and tune others.
|
||||||
|
static ImVec4
|
||||||
|
SyntaxInkEverforest(const TokenKind k)
|
||||||
|
{
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
return RGBA(0xE67E80); // everforest red
|
||||||
|
case TokenKind::Type:
|
||||||
|
return RGBA(0xD699B6); // everforest purple
|
||||||
|
case TokenKind::String:
|
||||||
|
case TokenKind::Char:
|
||||||
|
return RGBA(0xA7C080); // everforest green
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return RGBA(0x859289); // boosted from 0x616E88 for contrast
|
||||||
|
case TokenKind::Number:
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return RGBA(0xD8A657); // everforest yellow/orange
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return RGBA(0xE69875); // everforest orange
|
||||||
|
case TokenKind::Function:
|
||||||
|
return RGBA(0x83C092); // everforest aqua
|
||||||
|
case TokenKind::Operator:
|
||||||
|
case TokenKind::Punctuation:
|
||||||
|
return RGBA(0xD3C6AA); // everforest fg
|
||||||
|
case TokenKind::Error:
|
||||||
|
return RGBA(0xE67E80);
|
||||||
|
default:
|
||||||
|
return RGBA(0xD3C6AA); // everforest fg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[maybe_unused]] static ImVec4
|
[[maybe_unused]] static ImVec4
|
||||||
SyntaxInk(const TokenKind k)
|
SyntaxInk(const TokenKind k)
|
||||||
{
|
{
|
||||||
// Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults
|
|
||||||
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
// Base text
|
|
||||||
|
// Per-theme syntax palettes
|
||||||
|
if (gCurrentTheme == ThemeId::Tufte)
|
||||||
|
return SyntaxInkTufte(k, dark);
|
||||||
|
if (gCurrentTheme == ThemeId::Leuchtturm)
|
||||||
|
return SyntaxInkLeuchtturm(k, dark);
|
||||||
|
if (gCurrentTheme == ThemeId::Everforest)
|
||||||
|
return SyntaxInkEverforest(k);
|
||||||
|
|
||||||
|
// Default palettes tuned for Nord-ish themes
|
||||||
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case TokenKind::Keyword:
|
case TokenKind::Keyword:
|
||||||
|
|||||||
36
HelpText.cc
36
HelpText.cc
@@ -27,6 +27,7 @@ HelpText::Text()
|
|||||||
" C-k SPACE Toggle mark\n"
|
" C-k SPACE Toggle mark\n"
|
||||||
" C-k C-d Kill entire line\n"
|
" C-k C-d Kill entire line\n"
|
||||||
" C-k C-q Quit now (no confirm)\n"
|
" C-k C-q Quit now (no confirm)\n"
|
||||||
|
" C-k C-s Save\n"
|
||||||
" C-k C-x Save and quit\n"
|
" C-k C-x Save and quit\n"
|
||||||
" C-k a Mark start of file, jump to end\n"
|
" C-k a Mark start of file, jump to end\n"
|
||||||
" C-k b Switch buffer\n"
|
" C-k b Switch buffer\n"
|
||||||
@@ -40,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"
|
||||||
@@ -63,6 +65,10 @@ HelpText::Text()
|
|||||||
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
" ESC BACKSPACE Delete previous word (Alt-Backspace)\n"
|
||||||
" ESC q Reflow paragraph\n"
|
" ESC q Reflow paragraph\n"
|
||||||
"\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"
|
"Control keys:\n"
|
||||||
" C-a C-e Line start / end\n"
|
" C-a C-e Line start / end\n"
|
||||||
" C-b C-f Move left / right\n"
|
" C-b C-f Move left / right\n"
|
||||||
@@ -74,12 +80,32 @@ HelpText::Text()
|
|||||||
" C-t Regex search & replace\n"
|
" C-t Regex search & replace\n"
|
||||||
" C-h Search & replace\n"
|
" C-h Search & replace\n"
|
||||||
" C-l / C-g Refresh / Cancel\n"
|
" C-l / C-g Refresh / Cancel\n"
|
||||||
" C-u [digits] Universal argument (repeat count)\n"
|
|
||||||
"\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"
|
||||||
|
"GUI commands (command prompt):\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"
|
||||||
|
"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"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
644
ImGuiFrontend.cc
644
ImGuiFrontend.cc
@@ -29,21 +29,148 @@
|
|||||||
|
|
||||||
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Helpers
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
static void
|
||||||
|
apply_syntax_to_buffer(Buffer *b, const GUIConfig &cfg)
|
||||||
|
{
|
||||||
|
if (!b)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Auto-detect edit mode from file extension
|
||||||
|
if (!b->Filename().empty())
|
||||||
|
b->SetEditMode(DetectEditMode(b->Filename()));
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// SetupImGuiStyle_ — apply theme, fonts, and flags to the current ImGui context
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
GUIFrontend::SetupImGuiStyle_()
|
||||||
|
{
|
||||||
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
|
||||||
|
// Disable imgui.ini for secondary windows (primary sets its own path in Init)
|
||||||
|
io.IniFilename = nullptr;
|
||||||
|
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
|
if (config_.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
kte::ApplyThemeByName(config_.theme);
|
||||||
|
|
||||||
|
// Load fonts into this context's font atlas.
|
||||||
|
// Font registry is global and already populated by Init; just load into this atlas.
|
||||||
|
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||||
|
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
// Destroy a single window's ImGui context + SDL/GL resources
|
||||||
|
// ---------------------------------------------------------------------------
|
||||||
|
void
|
||||||
|
GUIFrontend::DestroyWindowResources_(WindowState &ws)
|
||||||
|
{
|
||||||
|
if (ws.imgui_ctx) {
|
||||||
|
// Must activate this window's GL context before shutting down the
|
||||||
|
// OpenGL3 backend, otherwise it deletes another context's resources.
|
||||||
|
if (ws.window && ws.gl_ctx)
|
||||||
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
|
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||||
|
ImGui_ImplOpenGL3_Shutdown();
|
||||||
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
ImGui::DestroyContext(ws.imgui_ctx);
|
||||||
|
ws.imgui_ctx = nullptr;
|
||||||
|
}
|
||||||
|
if (ws.gl_ctx) {
|
||||||
|
SDL_GL_DeleteContext(ws.gl_ctx);
|
||||||
|
ws.gl_ctx = nullptr;
|
||||||
|
}
|
||||||
|
if (ws.window) {
|
||||||
|
SDL_DestroyWindow(ws.window);
|
||||||
|
ws.window = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
||||||
{
|
{
|
||||||
(void) argc;
|
(void) argc;
|
||||||
(void) argv;
|
(void) argv;
|
||||||
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
|
||||||
input_.Attach(&ed);
|
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
||||||
// editor dimensions will be initialized during the first Step() frame
|
config_ = GUIConfig::Load();
|
||||||
|
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load GUI configuration (fullscreen, columns/rows, font size, theme, background)
|
|
||||||
GUIConfig cfg = GUIConfig::Load();
|
|
||||||
|
|
||||||
// GL attributes for core profile
|
// GL attributes for core profile
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_FLAGS, 0);
|
||||||
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
SDL_GL_SetAttribute(SDL_GL_CONTEXT_PROFILE_MASK, SDL_GL_CONTEXT_PROFILE_CORE);
|
||||||
@@ -56,159 +183,114 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
// Compute desired window size from config
|
// Compute desired window size from config
|
||||||
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
Uint32 win_flags = SDL_WINDOW_OPENGL | SDL_WINDOW_RESIZABLE | SDL_WINDOW_ALLOW_HIGHDPI;
|
||||||
|
|
||||||
if (cfg.fullscreen) {
|
int init_w = 1280, init_h = 800;
|
||||||
// "Fullscreen": fill the usable bounds of the primary display.
|
if (config_.fullscreen) {
|
||||||
// On macOS, do NOT use true fullscreen so the menu/status bar remains visible.
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
width_ = usable.w;
|
init_w = usable.w;
|
||||||
height_ = usable.h;
|
init_h = usable.h;
|
||||||
}
|
}
|
||||||
#if !defined(__APPLE__)
|
#if !defined(__APPLE__)
|
||||||
// Non-macOS: desktop fullscreen uses the current display resolution.
|
|
||||||
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
win_flags |= SDL_WINDOW_FULLSCREEN_DESKTOP;
|
||||||
#endif
|
#endif
|
||||||
} else {
|
} else {
|
||||||
// Windowed: width = columns * font_size, height = (rows * 2) * font_size
|
int w = config_.columns * static_cast<int>(config_.font_size);
|
||||||
int w = cfg.columns * static_cast<int>(cfg.font_size);
|
int h = config_.rows * static_cast<int>(config_.font_size * 1.2);
|
||||||
int h = cfg.rows * static_cast<int>(cfg.font_size * 1.2);
|
|
||||||
|
|
||||||
// As a safety, clamp to display usable bounds if retrievable
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
w = std::min(w, usable.w);
|
w = std::min(w, usable.w);
|
||||||
h = std::min(h, usable.h);
|
h = std::min(h, usable.h);
|
||||||
}
|
}
|
||||||
width_ = std::max(320, w);
|
init_w = std::max(320, w);
|
||||||
height_ = std::max(200, h);
|
init_h = std::max(200, h);
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
SDL_SetHint(SDL_HINT_VIDEO_ALLOW_SCREENSAVER, "1");
|
||||||
window_ = SDL_CreateWindow(
|
SDL_Window *win = SDL_CreateWindow(
|
||||||
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
"kge - kyle's graphical editor " KTE_VERSION_STR,
|
||||||
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED,
|
||||||
width_, height_,
|
init_w, init_h,
|
||||||
win_flags);
|
win_flags);
|
||||||
if (!window_) {
|
if (!win) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
SDL_EnableScreenSaver();
|
SDL_EnableScreenSaver();
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
#if defined(__APPLE__)
|
||||||
// macOS: when "fullscreen" is requested, position the window at the
|
if (config_.fullscreen) {
|
||||||
// top-left of the usable display area to mimic fullscreen while keeping
|
|
||||||
// the system menu bar visible.
|
|
||||||
if (cfg.fullscreen) {
|
|
||||||
SDL_Rect usable{};
|
SDL_Rect usable{};
|
||||||
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
if (SDL_GetDisplayUsableBounds(0, &usable) == 0) {
|
||||||
SDL_SetWindowPosition(window_, usable.x, usable.y);
|
SDL_SetWindowPosition(win, usable.x, usable.y);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
gl_ctx_ = SDL_GL_CreateContext(window_);
|
SDL_GLContext gl_ctx = SDL_GL_CreateContext(win);
|
||||||
if (!gl_ctx_)
|
if (!gl_ctx) {
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
return false;
|
return false;
|
||||||
SDL_GL_MakeCurrent(window_, gl_ctx_);
|
}
|
||||||
|
SDL_GL_MakeCurrent(win, gl_ctx);
|
||||||
SDL_GL_SetSwapInterval(1); // vsync
|
SDL_GL_SetSwapInterval(1); // vsync
|
||||||
|
|
||||||
|
// Create primary ImGui context
|
||||||
IMGUI_CHECKVERSION();
|
IMGUI_CHECKVERSION();
|
||||||
ImGui::CreateContext();
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
ImGuiIO &io = ImGui::GetIO();
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
|
|
||||||
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
// Set custom ini filename path to ~/.config/kte/imgui.ini
|
||||||
if (const char *home = std::getenv("HOME")) {
|
if (const char *home = std::getenv("HOME")) {
|
||||||
namespace fs = std::filesystem;
|
namespace fs = std::filesystem;
|
||||||
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
fs::path config_dir = fs::path(home) / ".config" / "kte";
|
||||||
|
|
||||||
std::error_code ec;
|
std::error_code ec;
|
||||||
if (!fs::exists(config_dir)) {
|
if (!fs::exists(config_dir)) {
|
||||||
fs::create_directories(config_dir, ec);
|
fs::create_directories(config_dir, ec);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fs::exists(config_dir)) {
|
if (fs::exists(config_dir)) {
|
||||||
static std::string ini_path = (config_dir / "imgui.ini").string();
|
static std::string ini_path = (config_dir / "imgui.ini").string();
|
||||||
io.IniFilename = ini_path.c_str();
|
io.IniFilename = ini_path.c_str();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; // Enable Keyboard Controls
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard;
|
||||||
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad; // Enable Gamepad Controls
|
io.ConfigFlags |= ImGuiConfigFlags_NavEnableGamepad;
|
||||||
ImGui::StyleColorsDark();
|
ImGui::StyleColorsDark();
|
||||||
|
|
||||||
// Apply background mode and selected theme (default: Nord). Can be changed at runtime via commands.
|
if (config_.background == "light")
|
||||||
if (cfg.background == "light")
|
|
||||||
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
else
|
else
|
||||||
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
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
|
apply_syntax_to_buffer(ed.CurrentBuffer(), config_);
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ImGui_ImplSDL2_InitForOpenGL(window_, gl_ctx_))
|
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx))
|
||||||
return false;
|
return false;
|
||||||
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion))
|
||||||
return false;
|
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;
|
int w, h;
|
||||||
SDL_GetWindowSize(window_, &w, &h);
|
SDL_GetWindowSize(win, &w, &h);
|
||||||
width_ = w;
|
init_w = w;
|
||||||
height_ = h;
|
init_h = h;
|
||||||
|
|
||||||
#if defined(__APPLE__)
|
#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) {
|
if (w > 1 && h > 1) {
|
||||||
SDL_SetWindowSize(window_, w - 1, h - 1);
|
SDL_SetWindowSize(win, w - 1, h - 1);
|
||||||
SDL_SetWindowSize(window_, w, h);
|
SDL_SetWindowSize(win, w, h);
|
||||||
// Update cached size in case backend reports immediately
|
SDL_GetWindowSize(win, &w, &h);
|
||||||
SDL_GetWindowSize(window_, &w, &h);
|
init_w = w;
|
||||||
width_ = w;
|
init_h = h;
|
||||||
height_ = h;
|
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
// Install embedded fonts into registry and load configured font
|
// Install embedded fonts
|
||||||
kte::Fonts::InstallDefaultFonts();
|
kte::Fonts::InstallDefaultFonts();
|
||||||
// Initialize font atlas using configured font name and size; fallback to embedded default helper
|
if (!kte::Fonts::FontRegistry::Instance().LoadFont(config_.font, (float) config_.font_size)) {
|
||||||
if (!kte::Fonts::FontRegistry::Instance().LoadFont(cfg.font, (float) cfg.font_size)) {
|
LoadGuiFont_(nullptr, (float) config_.font_size);
|
||||||
LoadGuiFont_(nullptr, (float) cfg.font_size);
|
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) config_.font_size);
|
||||||
// Record defaults in registry so subsequent size changes have a base
|
|
||||||
kte::Fonts::FontRegistry::Instance().RequestLoadFont("default", (float) cfg.font_size);
|
|
||||||
std::string n;
|
std::string n;
|
||||||
float s = 0.0f;
|
float s = 0.0f;
|
||||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
|
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(n, s)) {
|
||||||
@@ -216,6 +298,90 @@ 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->imgui_ctx = imgui_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)
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Each window gets its own ImGui context — ImGui requires exactly one
|
||||||
|
// NewFrame/Render cycle per context per frame.
|
||||||
|
ImGuiContext *imgui_ctx = ImGui::CreateContext();
|
||||||
|
ImGui::SetCurrentContext(imgui_ctx);
|
||||||
|
|
||||||
|
SetupImGuiStyle_();
|
||||||
|
|
||||||
|
if (!ImGui_ImplSDL2_InitForOpenGL(win, gl_ctx)) {
|
||||||
|
ImGui::DestroyContext(imgui_ctx);
|
||||||
|
SDL_GL_DeleteContext(gl_ctx);
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!ImGui_ImplOpenGL3_Init(kGlslVersion)) {
|
||||||
|
ImGui_ImplSDL2_Shutdown();
|
||||||
|
ImGui::DestroyContext(imgui_ctx);
|
||||||
|
SDL_GL_DeleteContext(gl_ctx);
|
||||||
|
SDL_DestroyWindow(win);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
auto ws = std::make_unique<WindowState>();
|
||||||
|
ws->window = win;
|
||||||
|
ws->gl_ctx = gl_ctx;
|
||||||
|
ws->imgui_ctx = imgui_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());
|
||||||
|
|
||||||
|
// Open a new untitled buffer and switch to it in the new window.
|
||||||
|
ws->editor.AddBuffer(Buffer());
|
||||||
|
ws->editor.SwitchTo(ws->editor.BufferCount() - 1);
|
||||||
|
|
||||||
|
ws->input.Attach(&ws->editor);
|
||||||
|
|
||||||
|
windows_.push_back(std::move(ws));
|
||||||
|
|
||||||
|
// Restore primary context
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,137 +389,251 @@ GUIFrontend::Init(int &argc, char **argv, Editor &ed)
|
|||||||
void
|
void
|
||||||
GUIFrontend::Step(Editor &ed, bool &running)
|
GUIFrontend::Step(Editor &ed, bool &running)
|
||||||
{
|
{
|
||||||
|
// --- Event processing ---
|
||||||
|
// SDL events carry a window ID. Route each event to the correct window's
|
||||||
|
// ImGui context (for ImGui_ImplSDL2_ProcessEvent) and input handler.
|
||||||
SDL_Event e;
|
SDL_Event e;
|
||||||
while (SDL_PollEvent(&e)) {
|
while (SDL_PollEvent(&e)) {
|
||||||
ImGui_ImplSDL2_ProcessEvent(&e);
|
// Determine which window this event belongs to
|
||||||
|
Uint32 event_win_id = 0;
|
||||||
switch (e.type) {
|
switch (e.type) {
|
||||||
case SDL_QUIT:
|
|
||||||
running = false;
|
|
||||||
break;
|
|
||||||
case SDL_WINDOWEVENT:
|
case SDL_WINDOWEVENT:
|
||||||
if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
event_win_id = e.window.windowID;
|
||||||
width_ = e.window.data1;
|
break;
|
||||||
height_ = e.window.data2;
|
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;
|
||||||
|
case SDL_MOUSEMOTION:
|
||||||
|
event_win_id = e.motion.windowID;
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
// Map input to commands
|
|
||||||
input_.ProcessSDLEvent(e);
|
if (e.type == SDL_QUIT) {
|
||||||
|
running = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find the target window and route the event to its ImGui context
|
||||||
|
WindowState *target = nullptr;
|
||||||
|
std::size_t target_idx = 0;
|
||||||
|
if (event_win_id != 0) {
|
||||||
|
for (std::size_t i = 0; i < windows_.size(); ++i) {
|
||||||
|
if (SDL_GetWindowID(windows_[i]->window) == event_win_id) {
|
||||||
|
target = windows_[i].get();
|
||||||
|
target_idx = i;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (target && target->imgui_ctx) {
|
||||||
|
// Set this window's ImGui context so ImGui_ImplSDL2_ProcessEvent
|
||||||
|
// updates the correct IO state.
|
||||||
|
ImGui::SetCurrentContext(target->imgui_ctx);
|
||||||
|
ImGui_ImplSDL2_ProcessEvent(&e);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (e.type == SDL_WINDOWEVENT) {
|
||||||
|
if (e.window.event == SDL_WINDOWEVENT_CLOSE) {
|
||||||
|
if (target) {
|
||||||
|
if (target_idx == 0) {
|
||||||
|
running = false;
|
||||||
|
} else {
|
||||||
|
target->alive = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if (e.window.event == SDL_WINDOWEVENT_SIZE_CHANGED) {
|
||||||
|
if (target) {
|
||||||
|
target->width = e.window.data1;
|
||||||
|
target->height = e.window.data2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Route input events to the correct window's input handler
|
||||||
|
if (target) {
|
||||||
|
target->input.ProcessSDLEvent(e);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply pending font change before starting a new frame
|
if (!running)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// --- Apply pending font change (to all contexts) ---
|
||||||
{
|
{
|
||||||
std::string fname;
|
std::string fname;
|
||||||
float fsize = 0.0f;
|
float fsize = 0.0f;
|
||||||
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
if (kte::Fonts::FontRegistry::Instance().ConsumePendingFontRequest(fname, fsize)) {
|
||||||
if (!fname.empty() && fsize > 0.0f) {
|
if (!fname.empty() && fsize > 0.0f) {
|
||||||
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
for (auto &ws : windows_) {
|
||||||
// Recreate backend font texture
|
if (!ws->alive || !ws->imgui_ctx)
|
||||||
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
continue;
|
||||||
ImGui_ImplOpenGL3_CreateFontsTexture();
|
ImGui::SetCurrentContext(ws->imgui_ctx);
|
||||||
|
SDL_GL_MakeCurrent(ws->window, ws->gl_ctx);
|
||||||
|
kte::Fonts::FontRegistry::Instance().LoadFont(fname, fsize);
|
||||||
|
ImGui_ImplOpenGL3_DestroyFontsTexture();
|
||||||
|
ImGui_ImplOpenGL3_CreateFontsTexture();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Start a new ImGui frame BEFORE processing commands so dimensions are correct
|
// --- Step each window ---
|
||||||
ImGui_ImplOpenGL3_NewFrame();
|
// We iterate by index because OpenNewWindow_ may append to windows_.
|
||||||
ImGui_ImplSDL2_NewFrame(window_);
|
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||||
ImGui::NewFrame();
|
WindowState &ws = *windows_[wi];
|
||||||
|
if (!ws.alive)
|
||||||
|
continue;
|
||||||
|
|
||||||
// Update editor logical rows/cols using current ImGui metrics and display size
|
Editor &wed = (wi == 0) ? ed : ws.editor;
|
||||||
{
|
|
||||||
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.
|
// Shared buffer list may have been modified by another window.
|
||||||
const float pad_x = 6.0f;
|
wed.ValidateBufferIndex();
|
||||||
const float pad_y = 6.0f;
|
|
||||||
|
|
||||||
// Use the same logic as ImGuiRenderer for available height and status bar reservation.
|
// Activate this window's GL and ImGui contexts
|
||||||
float wanted_bar_h = ImGui::GetFrameHeight();
|
SDL_GL_MakeCurrent(ws.window, ws.gl_ctx);
|
||||||
float total_avail_h = std::max(0.0f, disp_h - 2.0f * pad_y);
|
ImGui::SetCurrentContext(ws.imgui_ctx);
|
||||||
float actual_avail_h = std::floor((total_avail_h - wanted_bar_h) / row_h) * row_h;
|
|
||||||
|
|
||||||
// Visible content rows inside the scroll child
|
// Start a new ImGui frame
|
||||||
auto content_rows = static_cast<std::size_t>(std::max(0.0f, std::floor(actual_avail_h / row_h)));
|
ImGui_ImplOpenGL3_NewFrame();
|
||||||
// Editor::Rows includes the status line; add 1 back for it.
|
ImGui_ImplSDL2_NewFrame(ws.window);
|
||||||
std::size_t rows = content_rows + 1;
|
ImGui::NewFrame();
|
||||||
|
|
||||||
float avail_w = std::max(0.0f, disp_w - 2.0f * pad_x);
|
// Update editor dimensions
|
||||||
std::size_t cols = static_cast<std::size_t>(std::max(1.0f, std::floor(avail_w / ch_w)));
|
{
|
||||||
|
ImGuiIO &io = ImGui::GetIO();
|
||||||
// Only update if changed to avoid churn
|
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(ws.width);
|
||||||
if (rows != ed.Rows() || cols != ed.Cols()) {
|
float disp_h = io.DisplaySize.y > 0 ? io.DisplaySize.y : static_cast<float>(ws.height);
|
||||||
ed.SetDimensions(rows, cols);
|
update_editor_dimensions(wed, disp_w, disp_h);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
// Allow deferred opens (including swap recovery prompts) to run.
|
// Allow deferred opens
|
||||||
ed.ProcessPendingOpens();
|
wed.ProcessPendingOpens();
|
||||||
|
|
||||||
// Execute pending mapped inputs (drain queue) AFTER dimensions are updated
|
// Ensure newly opened buffers get syntax + edit mode detection
|
||||||
for (;;) {
|
apply_syntax_to_buffer(wed.CurrentBuffer(), config_);
|
||||||
MappedInput mi;
|
|
||||||
if (!input_.Poll(mi))
|
// Drain input queue
|
||||||
break;
|
for (;;) {
|
||||||
if (mi.hasCommand) {
|
MappedInput mi;
|
||||||
// Track kill ring before and after to sync GUI clipboard when it changes
|
if (!ws.input.Poll(mi))
|
||||||
const std::string before = ed.KillRingHead();
|
break;
|
||||||
Execute(ed, mi.id, mi.arg, mi.count);
|
if (mi.hasCommand) {
|
||||||
const std::string after = ed.KillRingHead();
|
if (mi.id == CommandId::NewWindow) {
|
||||||
if (after != before && !after.empty()) {
|
// Open a new window; handled after this loop
|
||||||
// Update the system clipboard to mirror the kill ring head in GUI
|
wed.SetNewWindowRequested(true);
|
||||||
SDL_SetClipboardText(after.c_str());
|
} else if (mi.id == CommandId::FontZoomIn ||
|
||||||
|
mi.id == CommandId::FontZoomOut ||
|
||||||
|
mi.id == CommandId::FontZoomReset) {
|
||||||
|
auto &fr = kte::Fonts::FontRegistry::Instance();
|
||||||
|
float cur = fr.CurrentFontSize();
|
||||||
|
if (cur <= 0.0f) cur = config_.font_size;
|
||||||
|
float next = cur;
|
||||||
|
if (mi.id == CommandId::FontZoomIn)
|
||||||
|
next = std::min(cur + 2.0f, 72.0f);
|
||||||
|
else if (mi.id == CommandId::FontZoomOut)
|
||||||
|
next = std::max(cur - 2.0f, 8.0f);
|
||||||
|
else
|
||||||
|
next = config_.font_size; // reset to config default
|
||||||
|
if (next != cur)
|
||||||
|
fr.RequestLoadFont(fr.CurrentFontName(), next);
|
||||||
|
} 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()) {
|
||||||
|
SDL_SetClipboardText(after.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wi == 0 && wed.QuitRequested()) {
|
||||||
|
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
|
||||||
|
ws.renderer.Draw(wed);
|
||||||
|
|
||||||
|
// Render
|
||||||
|
ImGui::Render();
|
||||||
|
int 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(ws.window);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ed.QuitRequested()) {
|
// Handle deferred new-window requests (must happen outside the render loop
|
||||||
running = false;
|
// to avoid corrupting an in-progress ImGui frame).
|
||||||
|
for (std::size_t wi = 0; wi < windows_.size(); ++wi) {
|
||||||
|
Editor &wed = (wi == 0) ? ed : windows_[wi]->editor;
|
||||||
|
if (wed.NewWindowRequested()) {
|
||||||
|
wed.SetNewWindowRequested(false);
|
||||||
|
OpenNewWindow_(ed);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// No runtime font UI; always use embedded font.
|
// Remove dead secondary windows
|
||||||
|
for (auto it = windows_.begin() + 1; it != windows_.end();) {
|
||||||
|
if (!(*it)->alive) {
|
||||||
|
DestroyWindowResources_(**it);
|
||||||
|
it = windows_.erase(it);
|
||||||
|
} else {
|
||||||
|
++it;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Draw editor UI
|
// Restore primary context
|
||||||
renderer_.Draw(ed);
|
if (!windows_.empty()) {
|
||||||
|
ImGui::SetCurrentContext(windows_[0]->imgui_ctx);
|
||||||
// Render
|
SDL_GL_MakeCurrent(windows_[0]->window, windows_[0]->gl_ctx);
|
||||||
ImGui::Render();
|
}
|
||||||
int display_w, display_h;
|
|
||||||
SDL_GL_GetDrawableSize(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_);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
GUIFrontend::Shutdown()
|
GUIFrontend::Shutdown()
|
||||||
{
|
{
|
||||||
ImGui_ImplOpenGL3_Shutdown();
|
// Destroy all windows (secondary first, then primary)
|
||||||
ImGui_ImplSDL2_Shutdown();
|
for (auto it = windows_.rbegin(); it != windows_.rend(); ++it) {
|
||||||
ImGui::DestroyContext();
|
DestroyWindowResources_(**it);
|
||||||
|
|
||||||
if (gl_ctx_) {
|
|
||||||
SDL_GL_DeleteContext(gl_ctx_);
|
|
||||||
gl_ctx_ = nullptr;
|
|
||||||
}
|
|
||||||
if (window_) {
|
|
||||||
SDL_DestroyWindow(window_);
|
|
||||||
window_ = nullptr;
|
|
||||||
}
|
}
|
||||||
|
windows_.clear();
|
||||||
SDL_Quit();
|
SDL_Quit();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -367,7 +647,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
|||||||
ImFontConfig config;
|
ImFontConfig config;
|
||||||
config.MergeMode = false;
|
config.MergeMode = false;
|
||||||
|
|
||||||
// Load Basic Latin + Latin Supplement
|
|
||||||
io.Fonts->AddFontFromMemoryCompressedTTF(
|
io.Fonts->AddFontFromMemoryCompressedTTF(
|
||||||
kte::Fonts::DefaultFontData,
|
kte::Fonts::DefaultFontData,
|
||||||
kte::Fonts::DefaultFontSize,
|
kte::Fonts::DefaultFontSize,
|
||||||
@@ -375,7 +654,6 @@ GUIFrontend::LoadGuiFont_(const char * /*path*/, const float size_px)
|
|||||||
&config,
|
&config,
|
||||||
io.Fonts->GetGlyphRangesDefault());
|
io.Fonts->GetGlyphRangesDefault());
|
||||||
|
|
||||||
// Merge Greek and Mathematical symbols from IosevkaExtended
|
|
||||||
config.MergeMode = true;
|
config.MergeMode = true;
|
||||||
static const ImWchar extended_ranges[] = {
|
static const ImWchar extended_ranges[] = {
|
||||||
0x0370, 0x03FF, // Greek and Coptic
|
0x0370, 0x03FF, // Greek and Coptic
|
||||||
|
|||||||
@@ -2,13 +2,18 @@
|
|||||||
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
|
#include <memory>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
#include "ImGuiInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "ImGuiRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
|
struct ImGuiContext;
|
||||||
typedef void *SDL_GLContext;
|
typedef void *SDL_GLContext;
|
||||||
|
|
||||||
class GUIFrontend final : public Frontend {
|
class GUIFrontend final : public Frontend {
|
||||||
@@ -24,13 +29,31 @@ public:
|
|||||||
void Shutdown() override;
|
void Shutdown() override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
// Per-window state — each window owns its own ImGui context so that
|
||||||
|
// NewFrame/Render cycles are fully independent (ImGui requires exactly
|
||||||
|
// one NewFrame per Render per context).
|
||||||
|
struct WindowState {
|
||||||
|
SDL_Window *window = nullptr;
|
||||||
|
SDL_GLContext gl_ctx = nullptr;
|
||||||
|
ImGuiContext *imgui_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);
|
||||||
|
|
||||||
|
// Initialize fonts and theme for a given ImGui context (must be current).
|
||||||
|
void SetupImGuiStyle_();
|
||||||
|
static void DestroyWindowResources_(WindowState &ws);
|
||||||
static bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
ImGuiInputHandler input_{};
|
// Primary window (index 0 in windows_); created during Init.
|
||||||
ImGuiRenderer renderer_{};
|
std::vector<std::unique_ptr<WindowState> > windows_;
|
||||||
SDL_Window *window_ = nullptr;
|
};
|
||||||
SDL_GLContext gl_ctx_ = nullptr;
|
|
||||||
int width_ = 1280;
|
|
||||||
int height_ = 800;
|
|
||||||
};
|
|
||||||
@@ -125,7 +125,11 @@ map_key(const SDL_Keycode key,
|
|||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
k_ctrl_pending = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::Newline, "", 0};
|
if (mod & KMOD_SHIFT) {
|
||||||
|
out = {true, CommandId::SmartNewline, "", 0};
|
||||||
|
} else {
|
||||||
|
out = {true, CommandId::Newline, "", 0};
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
@@ -333,6 +337,38 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
SDL_Keymod mods = SDL_Keymod(e.key.keysym.mod);
|
||||||
const SDL_Keycode key = e.key.keysym.sym;
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Font zoom: Cmd+=/Cmd+-/Cmd+0 (macOS) or Ctrl+=/Ctrl+-/Ctrl+0
|
||||||
|
if ((mods & (KMOD_CTRL | KMOD_GUI)) && !(mods & KMOD_SHIFT)) {
|
||||||
|
bool is_zoom = true;
|
||||||
|
CommandId zoom_cmd = CommandId::FontZoomIn;
|
||||||
|
if (key == SDLK_EQUALS || key == SDLK_PLUS)
|
||||||
|
zoom_cmd = CommandId::FontZoomIn;
|
||||||
|
else if (key == SDLK_MINUS)
|
||||||
|
zoom_cmd = CommandId::FontZoomOut;
|
||||||
|
else if (key == SDLK_0)
|
||||||
|
zoom_cmd = CommandId::FontZoomReset;
|
||||||
|
else
|
||||||
|
is_zoom = false;
|
||||||
|
if (is_zoom) {
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, zoom_cmd, std::string(), 0});
|
||||||
|
suppress_text_input_once_ = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS)
|
// 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.
|
// 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)) {
|
if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) {
|
||||||
@@ -439,12 +475,14 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// If editor universal argument is active, consume digit TEXTINPUT
|
// If editor universal argument is active, consume digit TEXTINPUT
|
||||||
if (ed_ && ed_
|
if (ed_ &&ed_
|
||||||
|
|
||||||
|
|
||||||
->
|
|
||||||
UArg() != 0
|
->
|
||||||
) {
|
UArg() != 0
|
||||||
|
)
|
||||||
|
{
|
||||||
const char *txt = e.text.text;
|
const char *txt = e.text.text;
|
||||||
if (txt && *txt) {
|
if (txt && *txt) {
|
||||||
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
unsigned char c0 = static_cast<unsigned char>(txt[0]);
|
||||||
@@ -606,4 +644,4 @@ ImGuiInputHandler::Poll(MappedInput &out)
|
|||||||
out = q_.front();
|
out = q_.front();
|
||||||
q_.pop();
|
q_.pop();
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
258
ImGuiRenderer.cc
258
ImGuiRenderer.cc
@@ -76,19 +76,16 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
// Two-way sync between Buffer::Rowoffs and ImGui scroll position:
|
||||||
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
// - If command layer changed Buffer::Rowoffs since last frame, drive ImGui scroll from it.
|
||||||
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
// - Otherwise, propagate ImGui scroll to Buffer::Rowoffs so command layer has an up-to-date view.
|
||||||
static long prev_buf_rowoffs = -1; // previous frame's Buffer::Rowoffs
|
|
||||||
static long prev_buf_coloffs = -1; // previous frame's Buffer::Coloffs
|
|
||||||
|
|
||||||
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
const long buf_rowoffs = static_cast<long>(buf->Rowoffs());
|
||||||
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
const long buf_coloffs = static_cast<long>(buf->Coloffs());
|
||||||
|
|
||||||
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
// Detect programmatic change (e.g., page_down command changed rowoffs)
|
||||||
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
// Use SetNextWindowScroll BEFORE BeginChild to set initial scroll position
|
||||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||||
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
ImGui::SetNextWindowScroll(ImVec2(-1.0f, target_y));
|
||||||
}
|
}
|
||||||
if (prev_buf_coloffs >= 0 && buf_coloffs != prev_buf_coloffs) {
|
if (prev_buf_coloffs_ >= 0 && buf_coloffs != prev_buf_coloffs_) {
|
||||||
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
float target_x = static_cast<float>(buf_coloffs) * space_w;
|
||||||
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
float target_y = static_cast<float>(buf_rowoffs) * row_h;
|
||||||
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
ImGui::SetNextWindowScroll(ImVec2(target_x, target_y));
|
||||||
@@ -116,25 +113,22 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
// Synchronize buffer offsets from ImGui scroll if user scrolled manually
|
||||||
bool forced_scroll = false;
|
bool forced_scroll = false;
|
||||||
{
|
{
|
||||||
static float prev_scroll_y = -1.0f; // previous frame's ImGui scroll Y in pixels
|
|
||||||
static float prev_scroll_x = -1.0f; // previous frame's ImGui scroll X in pixels
|
|
||||||
|
|
||||||
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
const long scroll_top = static_cast<long>(scroll_y / row_h);
|
||||||
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
const long scroll_left = static_cast<long>(scroll_x / space_w);
|
||||||
|
|
||||||
// Check if rowoffs was programmatically changed this frame
|
// Check if rowoffs was programmatically changed this frame
|
||||||
if (prev_buf_rowoffs >= 0 && buf_rowoffs != prev_buf_rowoffs) {
|
if (prev_buf_rowoffs_ >= 0 && buf_rowoffs != prev_buf_rowoffs_) {
|
||||||
forced_scroll = true;
|
forced_scroll = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If user scrolled (not programmatic), update buffer offsets accordingly
|
// If user scrolled (not programmatic), update buffer offsets accordingly
|
||||||
if (prev_scroll_y >= 0.0f && scroll_y != prev_scroll_y && !forced_scroll) {
|
if (prev_scroll_y_ >= 0.0f && scroll_y != prev_scroll_y_ && !forced_scroll) {
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
mbuf->SetOffsets(static_cast<std::size_t>(std::max(0L, scroll_top)),
|
||||||
mbuf->Coloffs());
|
mbuf->Coloffs());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (prev_scroll_x >= 0.0f && scroll_x != prev_scroll_x && !forced_scroll) {
|
if (prev_scroll_x_ >= 0.0f && scroll_x != prev_scroll_x_ && !forced_scroll) {
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
||||||
mbuf->SetOffsets(mbuf->Rowoffs(),
|
mbuf->SetOffsets(mbuf->Rowoffs(),
|
||||||
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
static_cast<std::size_t>(std::max(0L, scroll_left)));
|
||||||
@@ -142,11 +136,11 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Update trackers for next frame
|
// Update trackers for next frame
|
||||||
prev_scroll_y = scroll_y;
|
prev_scroll_y_ = scroll_y;
|
||||||
prev_scroll_x = scroll_x;
|
prev_scroll_x_ = scroll_x;
|
||||||
}
|
}
|
||||||
prev_buf_rowoffs = buf_rowoffs;
|
prev_buf_rowoffs_ = buf_rowoffs;
|
||||||
prev_buf_coloffs = buf_coloffs;
|
prev_buf_coloffs_ = buf_coloffs;
|
||||||
// Cache current horizontal offset in rendered columns for click handling
|
// Cache current horizontal offset in rendered columns for click handling
|
||||||
const std::size_t coloffs_now = buf->Coloffs();
|
const std::size_t coloffs_now = buf->Coloffs();
|
||||||
|
|
||||||
@@ -169,7 +163,7 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
const std::size_t vsel_sy = vsel_active ? buf->VisualLineStartY() : 0;
|
||||||
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
const std::size_t vsel_ey = vsel_active ? buf->VisualLineEndY() : 0;
|
||||||
|
|
||||||
static bool mouse_selecting = false;
|
// (mouse_selecting__ is a member variable)
|
||||||
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
auto mouse_pos_to_buf = [&]() -> std::pair<std::size_t, std::size_t> {
|
||||||
ImVec2 mp = ImGui::GetIO().MousePos;
|
ImVec2 mp = ImGui::GetIO().MousePos;
|
||||||
// Convert mouse pos to buffer row
|
// Convert mouse pos to buffer row
|
||||||
@@ -181,64 +175,131 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
if (by >= lines.size())
|
if (by >= lines.size())
|
||||||
by = lines.empty() ? 0 : (lines.size() - 1);
|
by = lines.empty() ? 0 : (lines.size() - 1);
|
||||||
|
|
||||||
// Convert mouse pos to rendered x
|
if (lines.empty())
|
||||||
|
return {0, 0};
|
||||||
|
|
||||||
|
// Expand tabs for the clicked line
|
||||||
|
std::string line_clicked = static_cast<std::string>(lines[by]);
|
||||||
|
const std::size_t tabw = 8;
|
||||||
|
std::string click_expanded;
|
||||||
|
click_expanded.reserve(line_clicked.size() + 16);
|
||||||
|
std::size_t click_rx = 0;
|
||||||
|
// Map: source column -> expanded column
|
||||||
|
std::vector<std::size_t> src_to_exp;
|
||||||
|
src_to_exp.reserve(line_clicked.size() + 1);
|
||||||
|
for (std::size_t ci = 0; ci < line_clicked.size(); ++ci) {
|
||||||
|
src_to_exp.push_back(click_rx);
|
||||||
|
if (line_clicked[ci] == '\t') {
|
||||||
|
std::size_t adv = (tabw - (click_rx % tabw));
|
||||||
|
click_expanded.append(adv, ' ');
|
||||||
|
click_rx += adv;
|
||||||
|
} else {
|
||||||
|
click_expanded.push_back(line_clicked[ci]);
|
||||||
|
click_rx += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
src_to_exp.push_back(click_rx); // past-end position
|
||||||
|
|
||||||
|
// Pixel x relative to the line start (accounting for scroll)
|
||||||
float visual_x = mp.x - child_window_pos.x;
|
float visual_x = mp.x - child_window_pos.x;
|
||||||
if (visual_x < 0.0f)
|
if (visual_x < 0.0f)
|
||||||
visual_x = 0.0f;
|
visual_x = 0.0f;
|
||||||
std::size_t clicked_rx = static_cast<std::size_t>(visual_x / space_w) + coloffs_now;
|
// Add scroll offset in pixels
|
||||||
|
visual_x += scroll_x;
|
||||||
|
|
||||||
// Convert rendered column to source column
|
// Find the source column whose expanded position is closest
|
||||||
if (lines.empty())
|
// to the click pixel, using actual text measurement.
|
||||||
return {0, 0};
|
std::size_t best_col = 0;
|
||||||
std::string line_clicked = static_cast<std::string>(lines[by]);
|
float best_dist = std::numeric_limits<float>::infinity();
|
||||||
const std::size_t tabw = 8;
|
for (std::size_t ci = 0; ci <= line_clicked.size(); ++ci) {
|
||||||
std::size_t rx = 0;
|
std::size_t exp_col = src_to_exp[ci];
|
||||||
std::size_t best_col = 0;
|
float px = 0.0f;
|
||||||
float best_dist = std::numeric_limits<float>::infinity();
|
if (exp_col > 0 && !click_expanded.empty()) {
|
||||||
float clicked_rx_f = static_cast<float>(clicked_rx);
|
std::size_t end = std::min(click_expanded.size(), exp_col);
|
||||||
for (std::size_t i = 0; i <= line_clicked.size(); ++i) {
|
px = ImGui::CalcTextSize(click_expanded.c_str(),
|
||||||
float dist = std::fabs(clicked_rx_f - static_cast<float>(rx));
|
click_expanded.c_str() + end).x;
|
||||||
|
}
|
||||||
|
float dist = std::fabs(visual_x - px);
|
||||||
if (dist < best_dist) {
|
if (dist < best_dist) {
|
||||||
best_dist = dist;
|
best_dist = dist;
|
||||||
best_col = i;
|
best_col = ci;
|
||||||
}
|
|
||||||
if (i < line_clicked.size()) {
|
|
||||||
rx += (line_clicked[i] == '\t') ? (tabw - (rx % tabw)) : 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {by, best_col};
|
return {by, best_col};
|
||||||
};
|
};
|
||||||
|
|
||||||
// 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)) {
|
if (ImGui::IsWindowHovered() && ImGui::IsMouseClicked(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = true;
|
mouse_selecting_ = true;
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
auto [by, bx] = mouse_pos_to_buf();
|
||||||
char tmp[64];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
if (Buffer *mbuf = const_cast<Buffer *>(buf)) {
|
|
||||||
mbuf->SetMark(bx, by);
|
// 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)) {
|
if (mouse_selecting_ && ImGui::IsWindowHovered() && ImGui::IsMouseDown(ImGuiMouseButton_Left)) {
|
||||||
auto [by, bx] = mouse_pos_to_buf();
|
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];
|
char tmp[64];
|
||||||
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
std::snprintf(tmp, sizeof(tmp), "%zu:%zu", by, bx);
|
||||||
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
Execute(ed, CommandId::MoveCursorTo, std::string(tmp));
|
||||||
}
|
}
|
||||||
if (mouse_selecting && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
if (mouse_selecting_ && ImGui::IsMouseReleased(ImGuiMouseButton_Left)) {
|
||||||
mouse_selecting = false;
|
mouse_selecting_ = false;
|
||||||
}
|
}
|
||||||
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
for (std::size_t i = rowoffs; i < lines.size(); ++i) {
|
||||||
// Capture the screen position before drawing the line
|
// Capture the screen position before drawing the line
|
||||||
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;
|
||||||
@@ -293,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;
|
||||||
@@ -334,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)
|
line_pos.y);
|
||||||
: 0;
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
std::size_t vx1 = rx_end - coloffs_now;
|
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w,
|
|
||||||
line_pos.y);
|
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
ImU32 col = ImGui::GetColorU32(ImGuiCol_TextSelectedBg);
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
@@ -359,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)
|
line_pos.y);
|
||||||
: 0;
|
ImVec2 p1 = ImVec2(line_pos.x + rx_to_px(rx_end),
|
||||||
std::size_t vx1 = rx_end - coloffs_now;
|
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(vx0) * space_w,
|
|
||||||
line_pos.y);
|
|
||||||
ImVec2 p1 = ImVec2(line_pos.x + static_cast<float>(vx1) * space_w,
|
|
||||||
line_pos.y + line_h);
|
line_pos.y + line_h);
|
||||||
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(
|
||||||
@@ -435,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);
|
||||||
@@ -462,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
|
||||||
@@ -529,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 {
|
||||||
cursor_rx += 1;
|
cur_expanded.push_back(cur_line[ci]);
|
||||||
|
cur_rx += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Compute rendered column of cursor
|
||||||
|
std::size_t cursor_rx = 0;
|
||||||
|
for (std::size_t ci = 0; ci < cx && ci < cur_line.size(); ++ci) {
|
||||||
|
if (cur_line[ci] == '\t')
|
||||||
|
cursor_rx += tabw - (cursor_rx % tabw);
|
||||||
|
else
|
||||||
|
cursor_rx += 1;
|
||||||
|
}
|
||||||
|
std::size_t exp_end = std::min(cur_expanded.size(), cursor_rx);
|
||||||
|
if (exp_end > 0)
|
||||||
|
cursor_px_abs = ImGui::CalcTextSize(cur_expanded.c_str(),
|
||||||
|
cur_expanded.c_str() + exp_end).x;
|
||||||
}
|
}
|
||||||
long cxr = static_cast<long>(cursor_rx);
|
if (cursor_px_abs < scroll_x_now || cursor_px_abs > scroll_x_now + child_w_actual) {
|
||||||
if (cxr < first_col || cxr > last_col) {
|
float target_x = cursor_px_abs - (child_w_actual / 2.0f);
|
||||||
float target_x = static_cast<float>(cxr) * space_w;
|
|
||||||
target_x -= (child_w_actual / 2.0f);
|
|
||||||
if (target_x < 0.f)
|
if (target_x < 0.f)
|
||||||
target_x = 0.f;
|
target_x = 0.f;
|
||||||
float max_x = ImGui::GetScrollMaxX();
|
float max_x = ImGui::GetScrollMaxX();
|
||||||
@@ -927,4 +955,4 @@ ImGuiRenderer::Draw(Editor &ed)
|
|||||||
ed.SetFilePickerVisible(false);
|
ed.SetFilePickerVisible(false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -11,4 +11,13 @@ public:
|
|||||||
~ImGuiRenderer() override = default;
|
~ImGuiRenderer() override = default;
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
void Draw(Editor &ed) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
// Per-window scroll tracking for two-way sync between Buffer offsets and ImGui scroll.
|
||||||
|
// These must be per-instance (not static) so each window maintains independent state.
|
||||||
|
long prev_buf_rowoffs_ = -1;
|
||||||
|
long prev_buf_coloffs_ = -1;
|
||||||
|
float prev_scroll_y_ = -1.0f;
|
||||||
|
float prev_scroll_x_ = -1.0f;
|
||||||
|
bool mouse_selecting_ = false;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -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;
|
||||||
@@ -226,8 +229,12 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
case 'q':
|
case 'q':
|
||||||
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
out = CommandId::ReflowParagraph; // Esc q (reflow paragraph)
|
||||||
return true;
|
return true;
|
||||||
|
case '\n':
|
||||||
|
case '\r':
|
||||||
|
out = CommandId::SmartNewline; // Shift+Enter (some terminals send this as Alt+Enter sequences)
|
||||||
|
return true;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
435
Swap.cc
435
Swap.cc
@@ -1,5 +1,8 @@
|
|||||||
#include "Swap.h"
|
#include "Swap.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
|
#include "ErrorHandler.h"
|
||||||
|
#include "SyscallWrappers.h"
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
@@ -530,8 +533,7 @@ SwapManager::ComputeSidecarPath(const Buffer &buf)
|
|||||||
// Unnamed buffers: unique within the process.
|
// Unnamed buffers: unique within the process.
|
||||||
static std::atomic<std::uint64_t> ctr{0};
|
static std::atomic<std::uint64_t> ctr{0};
|
||||||
const std::uint64_t n = ++ctr;
|
const std::uint64_t n = ++ctr;
|
||||||
const int pid = (int) ::getpid();
|
const std::string name = "unnamed-" + std::to_string(n) + ".swp";
|
||||||
const std::string name = "unnamed-" + std::to_string(pid) + "-" + std::to_string(n) + ".swp";
|
|
||||||
return (root / name).string();
|
return (root / name).string();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,38 +601,68 @@ SwapManager::write_header(int fd)
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SwapManager::open_ctx(JournalCtx &ctx, const std::string &path)
|
SwapManager::open_ctx(JournalCtx &ctx, const std::string &path, std::string &err)
|
||||||
{
|
{
|
||||||
|
err.clear();
|
||||||
if (ctx.fd >= 0)
|
if (ctx.fd >= 0)
|
||||||
return true;
|
return true;
|
||||||
if (!ensure_parent_dir(path))
|
if (!ensure_parent_dir(path)) {
|
||||||
|
err = "Failed to create parent directory for swap file: " + path;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
int flags = O_CREAT | O_WRONLY | O_APPEND;
|
int flags = O_CREAT | O_WRONLY | O_APPEND;
|
||||||
#ifdef O_CLOEXEC
|
#ifdef O_CLOEXEC
|
||||||
flags |= O_CLOEXEC;
|
flags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
int fd = ::open(path.c_str(), flags, 0600);
|
|
||||||
if (fd < 0)
|
// Retry on transient errors (ENOSPC, EDQUOT, EBUSY, etc.)
|
||||||
|
int fd = -1;
|
||||||
|
auto open_fn = [&]() -> bool {
|
||||||
|
fd = kte::syscall::Open(path.c_str(), flags, 0600);
|
||||||
|
return fd >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!RetryOnTransientError(open_fn, RetryPolicy::Aggressive(), err)) {
|
||||||
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to open swap file '" + path + "': " + std::strerror(saved_errno) + err;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
// Ensure permissions even if file already existed.
|
// Ensure permissions even if file already existed.
|
||||||
(void) ::fchmod(fd, 0600);
|
(void) kte::syscall::Fchmod(fd, 0600);
|
||||||
struct stat st{};
|
struct stat st{};
|
||||||
if (fstat(fd, &st) != 0) {
|
if (kte::syscall::Fstat(fd, &st) != 0) {
|
||||||
::close(fd);
|
int saved_errno = errno;
|
||||||
|
kte::syscall::Close(fd);
|
||||||
|
err = "Failed to fstat swap file '" + path + "': " + std::strerror(saved_errno);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
// If an existing file is too small to contain the fixed header, truncate
|
// If an existing file is too small to contain the fixed header, truncate
|
||||||
// and restart.
|
// and restart.
|
||||||
if (st.st_size > 0 && st.st_size < 64) {
|
if (st.st_size > 0 && st.st_size < 64) {
|
||||||
::close(fd);
|
kte::syscall::Close(fd);
|
||||||
int tflags = O_CREAT | O_WRONLY | O_TRUNC | O_APPEND;
|
int tflags = O_CREAT | O_WRONLY | O_TRUNC | O_APPEND;
|
||||||
#ifdef O_CLOEXEC
|
#ifdef O_CLOEXEC
|
||||||
tflags |= O_CLOEXEC;
|
tflags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
fd = ::open(path.c_str(), tflags, 0600);
|
|
||||||
if (fd < 0)
|
// Retry on transient errors for truncation open
|
||||||
|
fd = -1;
|
||||||
|
auto reopen_fn = [&]() -> bool {
|
||||||
|
fd = kte::syscall::Open(path.c_str(), tflags, 0600);
|
||||||
|
return fd >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!RetryOnTransientError(reopen_fn, RetryPolicy::Aggressive(), err)) {
|
||||||
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to reopen swap file for truncation '" + path + "': " + std::strerror(
|
||||||
|
saved_errno) + err;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
(void) ::fchmod(fd, 0600);
|
}
|
||||||
|
(void) kte::syscall::Fchmod(fd, 0600);
|
||||||
st.st_size = 0;
|
st.st_size = 0;
|
||||||
}
|
}
|
||||||
ctx.fd = fd;
|
ctx.fd = fd;
|
||||||
@@ -638,6 +670,9 @@ SwapManager::open_ctx(JournalCtx &ctx, const std::string &path)
|
|||||||
if (st.st_size == 0) {
|
if (st.st_size == 0) {
|
||||||
ctx.header_ok = write_header(fd);
|
ctx.header_ok = write_header(fd);
|
||||||
ctx.approx_size_bytes = ctx.header_ok ? 64 : 0;
|
ctx.approx_size_bytes = ctx.header_ok ? 64 : 0;
|
||||||
|
if (!ctx.header_ok) {
|
||||||
|
err = "Failed to write swap file header: " + path;
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.header_ok = true; // stage 1: trust existing header
|
ctx.header_ok = true; // stage 1: trust existing header
|
||||||
ctx.approx_size_bytes = static_cast<std::uint64_t>(st.st_size);
|
ctx.approx_size_bytes = static_cast<std::uint64_t>(st.st_size);
|
||||||
@@ -650,8 +685,8 @@ void
|
|||||||
SwapManager::close_ctx(JournalCtx &ctx)
|
SwapManager::close_ctx(JournalCtx &ctx)
|
||||||
{
|
{
|
||||||
if (ctx.fd >= 0) {
|
if (ctx.fd >= 0) {
|
||||||
(void) ::fsync(ctx.fd);
|
(void) kte::syscall::Fsync(ctx.fd);
|
||||||
::close(ctx.fd);
|
kte::syscall::Close(ctx.fd);
|
||||||
ctx.fd = -1;
|
ctx.fd = -1;
|
||||||
}
|
}
|
||||||
ctx.header_ok = false;
|
ctx.header_ok = false;
|
||||||
@@ -659,47 +694,77 @@ SwapManager::close_ctx(JournalCtx &ctx)
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record)
|
SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record, std::string &err)
|
||||||
{
|
{
|
||||||
if (ctx.path.empty())
|
err.clear();
|
||||||
|
if (ctx.path.empty()) {
|
||||||
|
err = "Compact failed: empty path";
|
||||||
return false;
|
return false;
|
||||||
if (chkpt_record.empty())
|
}
|
||||||
|
if (chkpt_record.empty()) {
|
||||||
|
err = "Compact failed: empty checkpoint record";
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
// Close existing file before rename.
|
// Close existing file before rename.
|
||||||
if (ctx.fd >= 0) {
|
if (ctx.fd >= 0) {
|
||||||
(void) ::fsync(ctx.fd);
|
(void) kte::syscall::Fsync(ctx.fd);
|
||||||
::close(ctx.fd);
|
kte::syscall::Close(ctx.fd);
|
||||||
ctx.fd = -1;
|
ctx.fd = -1;
|
||||||
}
|
}
|
||||||
ctx.header_ok = false;
|
ctx.header_ok = false;
|
||||||
|
|
||||||
const std::string tmp_path = ctx.path + ".tmp";
|
const std::string tmp_path = ctx.path + ".tmp";
|
||||||
// Create the compacted file: header + checkpoint record.
|
// Create the compacted file: header + checkpoint record.
|
||||||
if (!ensure_parent_dir(tmp_path))
|
if (!ensure_parent_dir(tmp_path)) {
|
||||||
|
err = "Failed to create parent directory for temp swap file: " + tmp_path;
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
int flags = O_CREAT | O_WRONLY | O_TRUNC;
|
int flags = O_CREAT | O_WRONLY | O_TRUNC;
|
||||||
#ifdef O_CLOEXEC
|
#ifdef O_CLOEXEC
|
||||||
flags |= O_CLOEXEC;
|
flags |= O_CLOEXEC;
|
||||||
#endif
|
#endif
|
||||||
int tfd = ::open(tmp_path.c_str(), flags, 0600);
|
|
||||||
if (tfd < 0)
|
// Retry on transient errors for temp file creation
|
||||||
|
int tfd = -1;
|
||||||
|
auto open_tmp_fn = [&]() -> bool {
|
||||||
|
tfd = kte::syscall::Open(tmp_path.c_str(), flags, 0600);
|
||||||
|
return tfd >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!RetryOnTransientError(open_tmp_fn, RetryPolicy::Aggressive(), err)) {
|
||||||
|
if (tfd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to open temp swap file '" + tmp_path + "': " + std::strerror(saved_errno) + err;
|
||||||
|
}
|
||||||
return false;
|
return false;
|
||||||
(void) ::fchmod(tfd, 0600);
|
}
|
||||||
|
(void) kte::syscall::Fchmod(tfd, 0600);
|
||||||
bool ok = write_header(tfd);
|
bool ok = write_header(tfd);
|
||||||
if (ok)
|
if (ok)
|
||||||
ok = write_full(tfd, chkpt_record.data(), chkpt_record.size());
|
ok = write_full(tfd, chkpt_record.data(), chkpt_record.size());
|
||||||
if (ok)
|
if (ok) {
|
||||||
ok = (::fsync(tfd) == 0);
|
if (kte::syscall::Fsync(tfd) != 0) {
|
||||||
::close(tfd);
|
int saved_errno = errno;
|
||||||
|
err = "Failed to fsync temp swap file '" + tmp_path + "': " + std::strerror(saved_errno);
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
kte::syscall::Close(tfd);
|
||||||
if (!ok) {
|
if (!ok) {
|
||||||
|
if (err.empty()) {
|
||||||
|
err = "Failed to write temp swap file: " + tmp_path;
|
||||||
|
}
|
||||||
std::remove(tmp_path.c_str());
|
std::remove(tmp_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Atomic replace.
|
// Atomic replace.
|
||||||
if (::rename(tmp_path.c_str(), ctx.path.c_str()) != 0) {
|
if (::rename(tmp_path.c_str(), ctx.path.c_str()) != 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to rename temp swap file '" + tmp_path + "' to '" + ctx.path + "': " + std::strerror(
|
||||||
|
saved_errno);
|
||||||
std::remove(tmp_path.c_str());
|
std::remove(tmp_path.c_str());
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -713,10 +778,10 @@ SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8
|
|||||||
#ifdef O_DIRECTORY
|
#ifdef O_DIRECTORY
|
||||||
dflags |= O_DIRECTORY;
|
dflags |= O_DIRECTORY;
|
||||||
#endif
|
#endif
|
||||||
int dfd = ::open(dir.string().c_str(), dflags);
|
int dfd = kte::syscall::Open(dir.string().c_str(), dflags);
|
||||||
if (dfd >= 0) {
|
if (dfd >= 0) {
|
||||||
(void) ::fsync(dfd);
|
(void) kte::syscall::Fsync(dfd);
|
||||||
::close(dfd);
|
kte::syscall::Close(dfd);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
@@ -724,8 +789,10 @@ SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Re-open for further appends.
|
// Re-open for further appends.
|
||||||
if (!open_ctx(ctx, ctx.path))
|
if (!open_ctx(ctx, ctx.path, err)) {
|
||||||
|
// err already set by open_ctx
|
||||||
return false;
|
return false;
|
||||||
|
}
|
||||||
ctx.approx_size_bytes = 64 + static_cast<std::uint64_t>(chkpt_record.size());
|
ctx.approx_size_bytes = 64 + static_cast<std::uint64_t>(chkpt_record.size());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -970,7 +1037,13 @@ SwapManager::writer_loop()
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
for (const Pending &p: batch) {
|
for (const Pending &p: batch) {
|
||||||
process_one(p);
|
try {
|
||||||
|
process_one(p);
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in process_one", p.buf);
|
||||||
|
}
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
if (p.seq > last_processed_)
|
if (p.seq > last_processed_)
|
||||||
@@ -982,23 +1055,29 @@ SwapManager::writer_loop()
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Throttled fsync: best-effort (grouped)
|
// Throttled fsync: best-effort (grouped)
|
||||||
std::vector<int> to_sync;
|
try {
|
||||||
std::uint64_t now = now_ns();
|
std::vector<int> to_sync;
|
||||||
{
|
std::uint64_t now = now_ns();
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
{
|
||||||
for (auto &kv: journals_) {
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
JournalCtx &ctx = kv.second;
|
for (auto &kv: journals_) {
|
||||||
if (ctx.fd >= 0) {
|
JournalCtx &ctx = kv.second;
|
||||||
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >=
|
if (ctx.fd >= 0) {
|
||||||
cfg_.fsync_interval_ms) {
|
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >=
|
||||||
ctx.last_fsync_ns = now;
|
cfg_.fsync_interval_ms) {
|
||||||
to_sync.push_back(ctx.fd);
|
ctx.last_fsync_ns = now;
|
||||||
|
to_sync.push_back(ctx.fd);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
for (int fd: to_sync) {
|
||||||
for (int fd: to_sync) {
|
(void) kte::syscall::Fsync(fd);
|
||||||
(void) ::fsync(fd);
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in fsync operations: ") + e.what());
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in fsync operations");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Wake any waiters.
|
// Wake any waiters.
|
||||||
@@ -1011,70 +1090,145 @@ SwapManager::process_one(const Pending &p)
|
|||||||
{
|
{
|
||||||
if (!p.buf)
|
if (!p.buf)
|
||||||
return;
|
return;
|
||||||
Buffer &buf = *p.buf;
|
|
||||||
|
|
||||||
JournalCtx *ctxp = nullptr;
|
// Check circuit breaker before processing
|
||||||
std::string path;
|
bool circuit_open = false;
|
||||||
std::size_t compact_bytes = 0;
|
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lg(mtx_);
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
auto it = journals_.find(p.buf);
|
if (!circuit_breaker_.AllowRequest()) {
|
||||||
if (it == journals_.end())
|
circuit_open = true;
|
||||||
return;
|
}
|
||||||
if (it->second.path.empty())
|
|
||||||
it->second.path = ComputeSidecarPath(buf);
|
|
||||||
path = it->second.path;
|
|
||||||
ctxp = &it->second;
|
|
||||||
compact_bytes = cfg_.compact_bytes;
|
|
||||||
}
|
}
|
||||||
if (!ctxp)
|
|
||||||
return;
|
if (circuit_open) {
|
||||||
if (!open_ctx(*ctxp, path))
|
// Circuit is open - graceful degradation: skip swap write
|
||||||
return;
|
// This prevents repeated failures from overwhelming the system
|
||||||
if (p.payload.size() > 0xFFFFFFu)
|
// Swap recording will resume when circuit closes
|
||||||
|
static std::atomic<std::uint64_t> last_warning_ns{0};
|
||||||
|
const std::uint64_t now = now_ns();
|
||||||
|
const std::uint64_t last = last_warning_ns.load();
|
||||||
|
// Log warning at most once per 60 seconds to avoid spam
|
||||||
|
if (now - last > 60000000000ULL) {
|
||||||
|
last_warning_ns.store(now);
|
||||||
|
ErrorHandler::Instance().Warning("SwapManager",
|
||||||
|
"Swap operations temporarily disabled due to repeated failures (circuit breaker open)",
|
||||||
|
p.buf && !p.buf->Filename().empty()
|
||||||
|
? p.buf->Filename()
|
||||||
|
: "<unnamed>");
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Build record: [type u8][len u24][payload][crc32 u32]
|
try {
|
||||||
std::uint8_t len3[3];
|
Buffer &buf = *p.buf;
|
||||||
put_u24_le(len3, static_cast<std::uint32_t>(p.payload.size()));
|
|
||||||
|
|
||||||
std::uint8_t head[4];
|
JournalCtx *ctxp = nullptr;
|
||||||
head[0] = static_cast<std::uint8_t>(p.type);
|
std::string path;
|
||||||
head[1] = len3[0];
|
std::size_t compact_bytes = 0;
|
||||||
head[2] = len3[1];
|
{
|
||||||
head[3] = len3[2];
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
auto it = journals_.find(p.buf);
|
||||||
|
if (it == journals_.end())
|
||||||
|
return;
|
||||||
|
if (it->second.path.empty())
|
||||||
|
it->second.path = ComputeSidecarPath(buf);
|
||||||
|
path = it->second.path;
|
||||||
|
ctxp = &it->second;
|
||||||
|
compact_bytes = cfg_.compact_bytes;
|
||||||
|
}
|
||||||
|
if (!ctxp)
|
||||||
|
return;
|
||||||
|
std::string open_err;
|
||||||
|
if (!open_ctx(*ctxp, path, open_err)) {
|
||||||
|
report_error(open_err, p.buf);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordFailure();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (p.payload.size() > 0xFFFFFFu) {
|
||||||
|
report_error("Payload too large: " + std::to_string(p.payload.size()) + " bytes", p.buf);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordFailure();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
std::uint32_t c = 0;
|
// Build record: [type u8][len u24][payload][crc32 u32]
|
||||||
c = crc32(head, sizeof(head), c);
|
std::uint8_t len3[3];
|
||||||
if (!p.payload.empty())
|
put_u24_le(len3, static_cast<std::uint32_t>(p.payload.size()));
|
||||||
c = crc32(p.payload.data(), p.payload.size(), c);
|
|
||||||
std::uint8_t crcbytes[4];
|
|
||||||
crcbytes[0] = static_cast<std::uint8_t>(c & 0xFFu);
|
|
||||||
crcbytes[1] = static_cast<std::uint8_t>((c >> 8) & 0xFFu);
|
|
||||||
crcbytes[2] = static_cast<std::uint8_t>((c >> 16) & 0xFFu);
|
|
||||||
crcbytes[3] = static_cast<std::uint8_t>((c >> 24) & 0xFFu);
|
|
||||||
|
|
||||||
std::vector<std::uint8_t> rec;
|
std::uint8_t head[4];
|
||||||
rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes));
|
head[0] = static_cast<std::uint8_t>(p.type);
|
||||||
rec.insert(rec.end(), head, head + sizeof(head));
|
head[1] = len3[0];
|
||||||
if (!p.payload.empty())
|
head[2] = len3[1];
|
||||||
rec.insert(rec.end(), p.payload.begin(), p.payload.end());
|
head[3] = len3[2];
|
||||||
rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes));
|
|
||||||
|
|
||||||
// Write (handle partial writes and check results)
|
std::uint32_t c = 0;
|
||||||
bool ok = write_full(ctxp->fd, rec.data(), rec.size());
|
c = crc32(head, sizeof(head), c);
|
||||||
if (ok) {
|
if (!p.payload.empty())
|
||||||
|
c = crc32(p.payload.data(), p.payload.size(), c);
|
||||||
|
std::uint8_t crcbytes[4];
|
||||||
|
crcbytes[0] = static_cast<std::uint8_t>(c & 0xFFu);
|
||||||
|
crcbytes[1] = static_cast<std::uint8_t>((c >> 8) & 0xFFu);
|
||||||
|
crcbytes[2] = static_cast<std::uint8_t>((c >> 16) & 0xFFu);
|
||||||
|
crcbytes[3] = static_cast<std::uint8_t>((c >> 24) & 0xFFu);
|
||||||
|
|
||||||
|
std::vector<std::uint8_t> rec;
|
||||||
|
rec.reserve(sizeof(head) + p.payload.size() + sizeof(crcbytes));
|
||||||
|
rec.insert(rec.end(), head, head + sizeof(head));
|
||||||
|
if (!p.payload.empty())
|
||||||
|
rec.insert(rec.end(), p.payload.begin(), p.payload.end());
|
||||||
|
rec.insert(rec.end(), crcbytes, crcbytes + sizeof(crcbytes));
|
||||||
|
|
||||||
|
// Write (handle partial writes and check results)
|
||||||
|
bool ok = write_full(ctxp->fd, rec.data(), rec.size());
|
||||||
|
if (!ok) {
|
||||||
|
int err = errno;
|
||||||
|
report_error("Failed to write swap record to '" + path + "': " + std::strerror(err), p.buf);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordFailure();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
ctxp->approx_size_bytes += static_cast<std::uint64_t>(rec.size());
|
ctxp->approx_size_bytes += static_cast<std::uint64_t>(rec.size());
|
||||||
if (p.urgent_flush) {
|
if (p.urgent_flush) {
|
||||||
(void) ::fsync(ctxp->fd);
|
if (kte::syscall::Fsync(ctxp->fd) != 0) {
|
||||||
|
int err = errno;
|
||||||
|
report_error("Failed to fsync swap file '" + path + "': " + std::strerror(err), p.buf);
|
||||||
|
}
|
||||||
ctxp->last_fsync_ns = now_ns();
|
ctxp->last_fsync_ns = now_ns();
|
||||||
}
|
}
|
||||||
if (p.type == SwapRecType::CHKPT && compact_bytes > 0 &&
|
if (p.type == SwapRecType::CHKPT && compact_bytes > 0 &&
|
||||||
ctxp->approx_size_bytes >= static_cast<std::uint64_t>(compact_bytes)) {
|
ctxp->approx_size_bytes >= static_cast<std::uint64_t>(compact_bytes)) {
|
||||||
(void) compact_to_checkpoint(*ctxp, rec);
|
std::string compact_err;
|
||||||
|
if (!compact_to_checkpoint(*ctxp, rec, compact_err)) {
|
||||||
|
report_error(compact_err, p.buf);
|
||||||
|
// Note: compaction failure is not fatal, don't record circuit breaker failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record success for circuit breaker
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordSuccess();
|
||||||
|
}
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
report_error(std::string("Exception in process_one: ") + e.what(), p.buf);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordFailure();
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
report_error("Unknown exception in process_one", p.buf);
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
circuit_breaker_.RecordFailure();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
(void) ok; // best-effort; future work could mark ctx error state
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1185,8 +1339,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
switch (type) {
|
switch (type) {
|
||||||
case SwapRecType::INS: {
|
case SwapRecType::INS: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// INS payload: encver(1) + row(4) + col(4) + nbytes(4) + data(nbytes)
|
||||||
err = "Swap record missing INS payload";
|
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
|
||||||
|
if (payload.size() < 13) {
|
||||||
|
err = "INS payload too short (need at least 13 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1197,7 +1353,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
std::uint32_t row = 0, col = 0, nbytes = 0;
|
std::uint32_t row = 0, col = 0, nbytes = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
||||||
payload, off, nbytes)) {
|
payload, off, nbytes)) {
|
||||||
err = "Malformed INS payload";
|
err = "Malformed INS payload (failed to parse row/col/nbytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (off + nbytes > payload.size()) {
|
if (off + nbytes > payload.size()) {
|
||||||
@@ -1210,8 +1366,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::DEL: {
|
case SwapRecType::DEL: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// DEL payload: encver(1) + row(4) + col(4) + dlen(4)
|
||||||
err = "Swap record missing DEL payload";
|
// Minimum: 1 + 4 + 4 + 4 = 13 bytes
|
||||||
|
if (payload.size() < 13) {
|
||||||
|
err = "DEL payload too short (need at least 13 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1222,7 +1380,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
std::uint32_t row = 0, col = 0, dlen = 0;
|
std::uint32_t row = 0, col = 0, dlen = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col) || !parse_u32_le(
|
||||||
payload, off, dlen)) {
|
payload, off, dlen)) {
|
||||||
err = "Malformed DEL payload";
|
err = "Malformed DEL payload (failed to parse row/col/dlen)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.delete_text((int) row, (int) col, (std::size_t) dlen);
|
buf.delete_text((int) row, (int) col, (std::size_t) dlen);
|
||||||
@@ -1230,8 +1388,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::SPLIT: {
|
case SwapRecType::SPLIT: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// SPLIT payload: encver(1) + row(4) + col(4)
|
||||||
err = "Swap record missing SPLIT payload";
|
// Minimum: 1 + 4 + 4 = 9 bytes
|
||||||
|
if (payload.size() < 9) {
|
||||||
|
err = "SPLIT payload too short (need at least 9 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1241,7 +1401,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t row = 0, col = 0;
|
std::uint32_t row = 0, col = 0;
|
||||||
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) {
|
if (!parse_u32_le(payload, off, row) || !parse_u32_le(payload, off, col)) {
|
||||||
err = "Malformed SPLIT payload";
|
err = "Malformed SPLIT payload (failed to parse row/col)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.split_line((int) row, (int) col);
|
buf.split_line((int) row, (int) col);
|
||||||
@@ -1249,8 +1409,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::JOIN: {
|
case SwapRecType::JOIN: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
if (payload.empty()) {
|
// JOIN payload: encver(1) + row(4)
|
||||||
err = "Swap record missing JOIN payload";
|
// Minimum: 1 + 4 = 5 bytes
|
||||||
|
if (payload.size() < 5) {
|
||||||
|
err = "JOIN payload too short (need at least 5 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1260,7 +1422,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t row = 0;
|
std::uint32_t row = 0;
|
||||||
if (!parse_u32_le(payload, off, row)) {
|
if (!parse_u32_le(payload, off, row)) {
|
||||||
err = "Malformed JOIN payload";
|
err = "Malformed JOIN payload (failed to parse row)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
buf.join_lines((int) row);
|
buf.join_lines((int) row);
|
||||||
@@ -1268,8 +1430,10 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
case SwapRecType::CHKPT: {
|
case SwapRecType::CHKPT: {
|
||||||
std::size_t off = 0;
|
std::size_t off = 0;
|
||||||
|
// CHKPT payload: encver(1) + nbytes(4) + data(nbytes)
|
||||||
|
// Minimum: 1 + 4 = 5 bytes
|
||||||
if (payload.size() < 5) {
|
if (payload.size() < 5) {
|
||||||
err = "Malformed CHKPT payload";
|
err = "CHKPT payload too short (need at least 5 bytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const std::uint8_t encver = payload[off++];
|
const std::uint8_t encver = payload[off++];
|
||||||
@@ -1279,7 +1443,7 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
std::uint32_t nbytes = 0;
|
std::uint32_t nbytes = 0;
|
||||||
if (!parse_u32_le(payload, off, nbytes)) {
|
if (!parse_u32_le(payload, off, nbytes)) {
|
||||||
err = "Malformed CHKPT payload";
|
err = "Malformed CHKPT payload (failed to parse nbytes)";
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (off + nbytes > payload.size()) {
|
if (off + nbytes > payload.size()) {
|
||||||
@@ -1296,4 +1460,61 @@ SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace kte
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::report_error(const std::string &message, Buffer *buf)
|
||||||
|
{
|
||||||
|
std::string context;
|
||||||
|
if (buf && !buf->Filename().empty()) {
|
||||||
|
context = buf->Filename();
|
||||||
|
} else if (buf) {
|
||||||
|
context = "<unnamed>";
|
||||||
|
} else {
|
||||||
|
context = "<unknown>";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Report to centralized error handler
|
||||||
|
ErrorHandler::Instance().Error("SwapManager", message, context);
|
||||||
|
|
||||||
|
// Maintain local error tracking for backward compatibility
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
SwapError err;
|
||||||
|
err.timestamp_ns = now_ns();
|
||||||
|
err.message = message;
|
||||||
|
err.buffer_name = context;
|
||||||
|
errors_.push_back(err);
|
||||||
|
// Bound the error queue to 100 entries
|
||||||
|
while (errors_.size() > 100) {
|
||||||
|
errors_.pop_front();
|
||||||
|
}
|
||||||
|
++total_error_count_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SwapManager::HasErrors() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return !errors_.empty();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
SwapManager::GetLastError() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (errors_.empty())
|
||||||
|
return "";
|
||||||
|
const SwapError &e = errors_.back();
|
||||||
|
return "[" + e.buffer_name + "] " + e.message;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::size_t
|
||||||
|
SwapManager::GetErrorCount() const
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
return total_error_count_;
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
35
Swap.h
35
Swap.h
@@ -10,10 +10,12 @@
|
|||||||
#include <memory>
|
#include <memory>
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
#include <condition_variable>
|
#include <condition_variable>
|
||||||
|
#include <deque>
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <atomic>
|
#include <atomic>
|
||||||
|
|
||||||
#include "SwapRecorder.h"
|
#include "SwapRecorder.h"
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
|
|
||||||
class Buffer;
|
class Buffer;
|
||||||
|
|
||||||
@@ -131,6 +133,20 @@ public:
|
|||||||
// Per-buffer toggle
|
// Per-buffer toggle
|
||||||
void SetSuspended(Buffer &buf, bool on);
|
void SetSuspended(Buffer &buf, bool on);
|
||||||
|
|
||||||
|
// Error reporting for background thread
|
||||||
|
struct SwapError {
|
||||||
|
std::uint64_t timestamp_ns{0};
|
||||||
|
std::string message;
|
||||||
|
std::string buffer_name; // filename or "<unnamed>"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Query error state (thread-safe)
|
||||||
|
bool HasErrors() const;
|
||||||
|
|
||||||
|
std::string GetLastError() const;
|
||||||
|
|
||||||
|
std::size_t GetErrorCount() const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
class BufferRecorder final : public SwapRecorder {
|
class BufferRecorder final : public SwapRecorder {
|
||||||
public:
|
public:
|
||||||
@@ -190,11 +206,12 @@ private:
|
|||||||
|
|
||||||
static bool write_header(int fd);
|
static bool write_header(int fd);
|
||||||
|
|
||||||
static bool open_ctx(JournalCtx &ctx, const std::string &path);
|
static bool open_ctx(JournalCtx &ctx, const std::string &path, std::string &err);
|
||||||
|
|
||||||
static void close_ctx(JournalCtx &ctx);
|
static void close_ctx(JournalCtx &ctx);
|
||||||
|
|
||||||
static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record);
|
static bool compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record,
|
||||||
|
std::string &err);
|
||||||
|
|
||||||
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
||||||
|
|
||||||
@@ -210,11 +227,14 @@ private:
|
|||||||
|
|
||||||
void process_one(const Pending &p);
|
void process_one(const Pending &p);
|
||||||
|
|
||||||
|
// Error reporting helper (called from writer thread)
|
||||||
|
void report_error(const std::string &message, Buffer *buf = nullptr);
|
||||||
|
|
||||||
// State
|
// State
|
||||||
SwapConfig cfg_{};
|
SwapConfig cfg_{};
|
||||||
std::unordered_map<Buffer *, JournalCtx> journals_;
|
std::unordered_map<Buffer *, JournalCtx> journals_;
|
||||||
std::unordered_map<Buffer *, std::unique_ptr<BufferRecorder> > recorders_;
|
std::unordered_map<Buffer *, std::unique_ptr<BufferRecorder> > recorders_;
|
||||||
std::mutex mtx_;
|
mutable std::mutex mtx_;
|
||||||
std::condition_variable cv_;
|
std::condition_variable cv_;
|
||||||
std::vector<Pending> queue_;
|
std::vector<Pending> queue_;
|
||||||
std::uint64_t next_seq_{0};
|
std::uint64_t next_seq_{0};
|
||||||
@@ -222,5 +242,12 @@ private:
|
|||||||
std::uint64_t inflight_{0};
|
std::uint64_t inflight_{0};
|
||||||
std::atomic<bool> running_{false};
|
std::atomic<bool> running_{false};
|
||||||
std::thread worker_;
|
std::thread worker_;
|
||||||
|
|
||||||
|
// Error tracking (protected by mtx_)
|
||||||
|
std::deque<SwapError> errors_; // bounded to max 100 entries
|
||||||
|
std::size_t total_error_count_{0};
|
||||||
|
|
||||||
|
// Circuit breaker for swap operations (protected by mtx_)
|
||||||
|
CircuitBreaker circuit_breaker_;
|
||||||
};
|
};
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
76
SyscallWrappers.cc
Normal file
76
SyscallWrappers.cc
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "SyscallWrappers.h"
|
||||||
|
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <cerrno>
|
||||||
|
#include <cstdlib>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
namespace syscall {
|
||||||
|
int
|
||||||
|
Open(const char *path, int flags, mode_t mode)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
do {
|
||||||
|
fd = ::open(path, flags, mode);
|
||||||
|
} while (fd == -1 && errno == EINTR);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Close(int fd)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
do {
|
||||||
|
ret = ::close(fd);
|
||||||
|
} while (ret == -1 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Fsync(int fd)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
do {
|
||||||
|
ret = ::fsync(fd);
|
||||||
|
} while (ret == -1 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Fstat(int fd, struct stat *buf)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
do {
|
||||||
|
ret = ::fstat(fd, buf);
|
||||||
|
} while (ret == -1 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Fchmod(int fd, mode_t mode)
|
||||||
|
{
|
||||||
|
int ret;
|
||||||
|
do {
|
||||||
|
ret = ::fchmod(fd, mode);
|
||||||
|
} while (ret == -1 && errno == EINTR);
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Mkstemp(char *template_str)
|
||||||
|
{
|
||||||
|
int fd;
|
||||||
|
do {
|
||||||
|
fd = ::mkstemp(template_str);
|
||||||
|
} while (fd == -1 && errno == EINTR);
|
||||||
|
return fd;
|
||||||
|
}
|
||||||
|
} // namespace syscall
|
||||||
|
} // namespace kte
|
||||||
47
SyscallWrappers.h
Normal file
47
SyscallWrappers.h
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// SyscallWrappers.h - EINTR-safe syscall wrappers for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
namespace syscall {
|
||||||
|
// EINTR-safe wrapper for open(2).
|
||||||
|
// Returns file descriptor on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
int Open(const char *path, int flags, mode_t mode = 0);
|
||||||
|
|
||||||
|
// EINTR-safe wrapper for close(2).
|
||||||
|
// Returns 0 on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
// Note: Some systems may not restart close() on EINTR, but we retry anyway
|
||||||
|
// as recommended by POSIX.1-2008.
|
||||||
|
int Close(int fd);
|
||||||
|
|
||||||
|
// EINTR-safe wrapper for fsync(2).
|
||||||
|
// Returns 0 on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
int Fsync(int fd);
|
||||||
|
|
||||||
|
// EINTR-safe wrapper for fstat(2).
|
||||||
|
// Returns 0 on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
int Fstat(int fd, struct stat *buf);
|
||||||
|
|
||||||
|
// EINTR-safe wrapper for fchmod(2).
|
||||||
|
// Returns 0 on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
int Fchmod(int fd, mode_t mode);
|
||||||
|
|
||||||
|
// EINTR-safe wrapper for mkstemp(3).
|
||||||
|
// Returns file descriptor on success, -1 on failure (errno set).
|
||||||
|
// Automatically retries on EINTR.
|
||||||
|
// Note: template_str must be a mutable buffer ending in "XXXXXX".
|
||||||
|
int Mkstemp(char *template_str);
|
||||||
|
|
||||||
|
// Note: rename(2) and unlink(2) are not wrapped because they operate on
|
||||||
|
// filesystem metadata and typically complete atomically without EINTR.
|
||||||
|
// If interrupted, they either succeed or fail without partial state.
|
||||||
|
} // namespace syscall
|
||||||
|
} // namespace kte
|
||||||
@@ -67,13 +67,20 @@ map_key_to_command(const int ch,
|
|||||||
if (pressed) {
|
if (pressed) {
|
||||||
mouse_selecting = true;
|
mouse_selecting = true;
|
||||||
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
if (Buffer *b = ed->CurrentBuffer()) {
|
// We don't set the mark on simple click anymore in ncurses either,
|
||||||
b->SetMark(b->Curx(), b->Cury());
|
// 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;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (mouse_selecting && moved) {
|
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));
|
Execute(*ed, CommandId::MoveCursorTo, std::string(buf));
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
@@ -329,4 +336,4 @@ TerminalInputHandler::Poll(MappedInput &out)
|
|||||||
{
|
{
|
||||||
out = {};
|
out = {};
|
||||||
return decode_(out) && out.hasCommand;
|
return decode_(out) && out.hasCommand;
|
||||||
}
|
}
|
||||||
@@ -9,6 +9,7 @@ enum class UndoType : std::uint8_t {
|
|||||||
Paste,
|
Paste,
|
||||||
Newline,
|
Newline,
|
||||||
DeleteRow,
|
DeleteRow,
|
||||||
|
InsertRow,
|
||||||
};
|
};
|
||||||
|
|
||||||
struct UndoNode {
|
struct UndoNode {
|
||||||
@@ -20,4 +21,4 @@ struct UndoNode {
|
|||||||
UndoNode *parent = nullptr; // previous state; null means pre-first-edit
|
UndoNode *parent = nullptr; // previous state; null means pre-first-edit
|
||||||
UndoNode *child = nullptr; // next in current timeline
|
UndoNode *child = nullptr; // next in current timeline
|
||||||
UndoNode *next = nullptr; // redo branch
|
UndoNode *next = nullptr; // redo branch
|
||||||
};
|
};
|
||||||
@@ -36,7 +36,8 @@ UndoSystem::Begin(UndoType type)
|
|||||||
const int col = static_cast<int>(buf_->Curx());
|
const int col = static_cast<int>(buf_->Curx());
|
||||||
|
|
||||||
// Some operations should always be standalone undo steps.
|
// 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) {
|
if (always_standalone) {
|
||||||
commit();
|
commit();
|
||||||
}
|
}
|
||||||
@@ -75,6 +76,7 @@ UndoSystem::Begin(UndoType type)
|
|||||||
}
|
}
|
||||||
case UndoType::Newline:
|
case UndoType::Newline:
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
|
case UndoType::InsertRow:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -314,6 +316,15 @@ UndoSystem::apply(const UndoNode *node, int direction)
|
|||||||
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
buf_->SetCursor(0, static_cast<std::size_t>(node->row));
|
||||||
}
|
}
|
||||||
break;
|
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";
|
return "Newline";
|
||||||
case UndoType::DeleteRow:
|
case UndoType::DeleteRow:
|
||||||
return "DeleteRow";
|
return "DeleteRow";
|
||||||
|
case UndoType::InsertRow:
|
||||||
|
return "InsertRow";
|
||||||
}
|
}
|
||||||
return "?";
|
return "?";
|
||||||
}
|
}
|
||||||
@@ -452,4 +465,4 @@ UndoSystem::debug_log(const char *op) const
|
|||||||
#else
|
#else
|
||||||
(void) op;
|
(void) op;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -48,6 +48,7 @@ stdenv.mkDerivation {
|
|||||||
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
||||||
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
|
"-DKTE_STATIC_LINK=OFF"
|
||||||
];
|
];
|
||||||
|
|
||||||
installPhase = ''
|
installPhase = ''
|
||||||
|
|||||||
@@ -11,7 +11,8 @@ codebase, make changes, and contribute effectively.
|
|||||||
4. [Building and Testing](#building-and-testing)
|
4. [Building and Testing](#building-and-testing)
|
||||||
5. [Making Changes](#making-changes)
|
5. [Making Changes](#making-changes)
|
||||||
6. [Code Style](#code-style)
|
6. [Code Style](#code-style)
|
||||||
7. [Common Tasks](#common-tasks)
|
7. [Error Handling Conventions](#error-handling-conventions)
|
||||||
|
8. [Common Tasks](#common-tasks)
|
||||||
|
|
||||||
## Architecture Overview
|
## Architecture Overview
|
||||||
|
|
||||||
@@ -537,6 +538,491 @@ void maybeConsolidate() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Error Handling Conventions
|
||||||
|
|
||||||
|
kte uses standardized error handling patterns to ensure consistency and
|
||||||
|
reliability across the codebase. This section documents when to use each
|
||||||
|
pattern and how to integrate with the centralized error handling system.
|
||||||
|
|
||||||
|
### Error Propagation Patterns
|
||||||
|
|
||||||
|
kte uses three standard patterns for error handling:
|
||||||
|
|
||||||
|
#### 1. `bool` + `std::string &err` (I/O and Fallible Operations)
|
||||||
|
|
||||||
|
**When to use**: Operations that can fail and need detailed error
|
||||||
|
messages
|
||||||
|
(file I/O, network operations, parsing, resource allocation).
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool OperationName(args..., std::string &err) {
|
||||||
|
err.clear();
|
||||||
|
|
||||||
|
// Attempt operation
|
||||||
|
if (/* operation failed */) {
|
||||||
|
err = "Detailed error message with context";
|
||||||
|
ErrorHandler::Instance().Error("ComponentName", err, "optional_context");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Buffer::OpenFromFile(const std::string &path, std::string &err)`
|
||||||
|
- `Buffer::Save(std::string &err)`
|
||||||
|
-
|
||||||
|
|
||||||
|
`SwapManager::ReplayFile(Buffer &buf, const std::string &path, std::string &err)`
|
||||||
|
|
||||||
|
**Guidelines**:
|
||||||
|
|
||||||
|
- Always clear `err` at the start of the function
|
||||||
|
- Provide actionable error messages with context (file paths, operation
|
||||||
|
details)
|
||||||
|
- Call `ErrorHandler::Instance().Error()` for centralized logging
|
||||||
|
- Return `false` on failure, `true` on success
|
||||||
|
- Capture `errno` immediately after syscall failures: `int saved_errno =
|
||||||
|
errno;`
|
||||||
|
- Use `std::strerror(saved_errno)` for syscall error messages
|
||||||
|
|
||||||
|
#### 2. `void` (Infallible State Changes)
|
||||||
|
|
||||||
|
**When to use**: Operations that modify internal state and cannot fail
|
||||||
|
(setters, cursor movement, flag toggles).
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void SetProperty(Type value) {
|
||||||
|
property_ = value;
|
||||||
|
// Update related state if needed
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Buffer::SetCursor(std::size_t x, std::size_t y)`
|
||||||
|
- `Buffer::SetDirty(bool d)`
|
||||||
|
- `Editor::SetStatus(const std::string &msg)`
|
||||||
|
|
||||||
|
**Guidelines**:
|
||||||
|
|
||||||
|
- Use for simple state changes that cannot fail
|
||||||
|
- No error reporting needed
|
||||||
|
- Keep operations atomic and side-effect free when possible
|
||||||
|
|
||||||
|
#### 3. `bool` without error parameter (Control Flow)
|
||||||
|
|
||||||
|
**When to use**: Operations where success/failure is sufficient
|
||||||
|
information
|
||||||
|
and detailed error messages aren't needed (validation checks, control
|
||||||
|
flow
|
||||||
|
decisions).
|
||||||
|
|
||||||
|
**Pattern**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool CheckCondition() const {
|
||||||
|
return condition_is_met;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Editor::SwitchTo(std::size_t index)` - returns false if index invalid
|
||||||
|
- `Editor::CloseBuffer(std::size_t index)` - returns false if can't
|
||||||
|
close
|
||||||
|
|
||||||
|
**Guidelines**:
|
||||||
|
|
||||||
|
- Use when the caller only needs to know success/failure
|
||||||
|
- Typically for validation or control flow decisions
|
||||||
|
- Don't use for operations that need error diagnostics
|
||||||
|
|
||||||
|
### ErrorHandler Integration
|
||||||
|
|
||||||
|
All error-prone operations should report errors to the centralized
|
||||||
|
`ErrorHandler` for logging and UI integration.
|
||||||
|
|
||||||
|
**Severity Levels**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
ErrorHandler::Instance().Info("Component", "message", "context"); // Informational
|
||||||
|
ErrorHandler::Instance().Warning("Component", "message", "context"); // Warning
|
||||||
|
ErrorHandler::Instance().Error("Component", "message", "context"); // Error
|
||||||
|
ErrorHandler::Instance().Critical("Component", "message", "context"); // Critical
|
||||||
|
```
|
||||||
|
|
||||||
|
**When to use each severity**:
|
||||||
|
|
||||||
|
- **Info**: Non-error events (file saved, operation completed)
|
||||||
|
- **Warning**: Recoverable issues (external file modification detected)
|
||||||
|
- **Error**: Operation failures (file I/O errors, allocation failures)
|
||||||
|
- **Critical**: Fatal errors (unhandled exceptions, data corruption)
|
||||||
|
|
||||||
|
**Component names**: Use the class name ("Buffer", "SwapManager",
|
||||||
|
"Editor", "main")
|
||||||
|
|
||||||
|
**Context**: Optional string providing additional context (filename,
|
||||||
|
buffer
|
||||||
|
name, operation details)
|
||||||
|
|
||||||
|
### Error Handling in Different Contexts
|
||||||
|
|
||||||
|
#### File I/O Operations
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
bool Buffer::Save(std::string &err) const {
|
||||||
|
if (!is_file_backed_ || filename_.empty()) {
|
||||||
|
err = "Buffer is not file-backed; use SaveAs()";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::size_t sz = content_.Size();
|
||||||
|
const char *data = sz ? content_.Data() : nullptr;
|
||||||
|
|
||||||
|
if (!atomic_write_file(filename_, data ? data : "", sz, err)) {
|
||||||
|
ErrorHandler::Instance().Error("Buffer", err, filename_);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Syscall Error Handling with EINTR-Safe Wrappers
|
||||||
|
|
||||||
|
kte provides EINTR-safe syscall wrappers in `SyscallWrappers.h` that
|
||||||
|
automatically retry on `EINTR`. **Always use these wrappers instead of
|
||||||
|
direct syscalls.**
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "SyscallWrappers.h"
|
||||||
|
|
||||||
|
bool open_file(const std::string &path, std::string &err) {
|
||||||
|
int fd = kte::syscall::Open(path.c_str(), O_RDONLY);
|
||||||
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno; // Capture immediately!
|
||||||
|
err = "Failed to open file '" + path + "': " + std::strerror(saved_errno);
|
||||||
|
ErrorHandler::Instance().Error("Component", err, path);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
// ... use fd
|
||||||
|
kte::syscall::Close(fd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Available EINTR-safe wrappers**:
|
||||||
|
|
||||||
|
- `kte::syscall::Open(path, flags, mode)` - wraps `open(2)`
|
||||||
|
- `kte::syscall::Close(fd)` - wraps `close(2)`
|
||||||
|
- `kte::syscall::Fsync(fd)` - wraps `fsync(2)`
|
||||||
|
- `kte::syscall::Fstat(fd, buf)` - wraps `fstat(2)`
|
||||||
|
- `kte::syscall::Fchmod(fd, mode)` - wraps `fchmod(2)`
|
||||||
|
- `kte::syscall::Mkstemp(template)` - wraps `mkstemp(3)`
|
||||||
|
|
||||||
|
**Note**: `rename(2)` and `unlink(2)` are NOT wrapped because they
|
||||||
|
operate on filesystem metadata atomically and don't need EINTR retry.
|
||||||
|
|
||||||
|
#### Background Thread Errors
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
void background_worker() {
|
||||||
|
try {
|
||||||
|
// ... work
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
std::string msg = std::string("Exception in worker: ") + e.what();
|
||||||
|
ErrorHandler::Instance().Error("WorkerThread", msg);
|
||||||
|
} catch (...) {
|
||||||
|
ErrorHandler::Instance().Error("WorkerThread", "Unknown exception");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Top-Level Exception Handling
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
int main(int argc, char *argv[]) {
|
||||||
|
try {
|
||||||
|
// ... main logic
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
std::string msg = std::string("Unhandled exception: ") + e.what();
|
||||||
|
ErrorHandler::Instance().Critical("main", msg);
|
||||||
|
std::cerr << "FATAL ERROR: " << e.what() << "\n";
|
||||||
|
return 1;
|
||||||
|
} catch (...) {
|
||||||
|
ErrorHandler::Instance().Critical("main", "Unknown exception");
|
||||||
|
std::cerr << "FATAL ERROR: Unknown exception\n";
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Handling Anti-Patterns
|
||||||
|
|
||||||
|
**❌ Don't**: Silently ignore errors
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// BAD
|
||||||
|
void process() {
|
||||||
|
std::string err;
|
||||||
|
if (!operation(err)) {
|
||||||
|
// Error ignored!
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Do**: Always handle or propagate errors
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// GOOD
|
||||||
|
bool process(std::string &err) {
|
||||||
|
if (!operation(err)) {
|
||||||
|
// err already set by operation()
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Don't**: Use generic error messages
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// BAD
|
||||||
|
err = "Operation failed";
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Do**: Provide specific, actionable error messages
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// GOOD
|
||||||
|
err = "Failed to open file '" + path + "': " + std::strerror(errno);
|
||||||
|
```
|
||||||
|
|
||||||
|
**❌ Don't**: Forget to capture errno
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// BAD
|
||||||
|
if (::write(fd, data, len) < 0) {
|
||||||
|
// errno might be overwritten by other calls!
|
||||||
|
err = std::strerror(errno);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**✅ Do**: Capture errno immediately
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// GOOD
|
||||||
|
if (::write(fd, data, len) < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = std::strerror(saved_errno);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Error Log Location
|
||||||
|
|
||||||
|
All errors are automatically logged to:
|
||||||
|
|
||||||
|
```
|
||||||
|
~/.local/state/kte/error.log
|
||||||
|
```
|
||||||
|
|
||||||
|
Log format:
|
||||||
|
|
||||||
|
```
|
||||||
|
[2026-02-17 20:12:34.567] [ERROR] SwapManager (buffer.txt): Failed to write swap record
|
||||||
|
[2026-02-17 20:12:35.123] [CRITICAL] main: Unhandled exception: out of memory
|
||||||
|
```
|
||||||
|
|
||||||
|
### Migration Guide
|
||||||
|
|
||||||
|
When updating existing code to follow these conventions:
|
||||||
|
|
||||||
|
1. **Identify error-prone operations** - File I/O, syscalls, allocations
|
||||||
|
2. **Add `std::string &err` parameter** if not present
|
||||||
|
3. **Add ErrorHandler calls** at all error sites
|
||||||
|
4. **Capture errno** for syscall failures
|
||||||
|
5. **Update callers** to handle the error parameter
|
||||||
|
6. **Write tests** that verify error handling
|
||||||
|
|
||||||
|
### Error Recovery Mechanisms
|
||||||
|
|
||||||
|
kte implements automatic error recovery for transient failures using
|
||||||
|
retry logic and circuit breaker patterns.
|
||||||
|
|
||||||
|
#### Transient Error Classification
|
||||||
|
|
||||||
|
Transient errors are temporary failures that may succeed on retry:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
|
|
||||||
|
bool IsTransientError(int err); // Returns true for EAGAIN, EWOULDBLOCK, EBUSY, EIO, ETIMEDOUT, ENOSPC, EDQUOT
|
||||||
|
```
|
||||||
|
|
||||||
|
**Transient errors**:
|
||||||
|
|
||||||
|
- `EAGAIN` / `EWOULDBLOCK` - Resource temporarily unavailable
|
||||||
|
- `EBUSY` - Device or resource busy
|
||||||
|
- `EIO` - I/O error (may be transient on network filesystems)
|
||||||
|
- `ETIMEDOUT` - Operation timed out
|
||||||
|
- `ENOSPC` - No space left on device (may become available)
|
||||||
|
- `EDQUOT` - Disk quota exceeded (may become available)
|
||||||
|
|
||||||
|
**Permanent errors** (don't retry):
|
||||||
|
|
||||||
|
- `ENOENT` - File not found
|
||||||
|
- `EACCES` - Permission denied
|
||||||
|
- `EINVAL` - Invalid argument
|
||||||
|
- `ENOTDIR` - Not a directory
|
||||||
|
|
||||||
|
#### Retry Policies
|
||||||
|
|
||||||
|
Three predefined retry policies are available:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Default: 3 attempts, 100ms initial delay, 2x backoff, 5s max delay
|
||||||
|
RetryPolicy::Default()
|
||||||
|
|
||||||
|
// Aggressive: 5 attempts, 50ms initial delay, 1.5x backoff, 2s max delay
|
||||||
|
// Use for critical operations (swap files, file saves)
|
||||||
|
RetryPolicy::Aggressive()
|
||||||
|
|
||||||
|
// Conservative: 2 attempts, 200ms initial delay, 2.5x backoff, 10s max delay
|
||||||
|
// Use for non-critical operations
|
||||||
|
RetryPolicy::Conservative()
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Using RetryOnTransientError
|
||||||
|
|
||||||
|
Wrap syscalls with automatic retry on transient errors:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
#include "ErrorRecovery.h"
|
||||||
|
#include "SyscallWrappers.h"
|
||||||
|
|
||||||
|
bool save_file(const std::string &path, std::string &err) {
|
||||||
|
int fd = -1;
|
||||||
|
auto open_fn = [&]() -> bool {
|
||||||
|
fd = kte::syscall::Open(path.c_str(), O_CREAT | O_WRONLY, 0644);
|
||||||
|
return fd >= 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!kte::RetryOnTransientError(open_fn, kte::RetryPolicy::Aggressive(), err)) {
|
||||||
|
if (fd < 0) {
|
||||||
|
int saved_errno = errno;
|
||||||
|
err = "Failed to open file '" + path + "': " + std::strerror(saved_errno) + err;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ... use fd
|
||||||
|
kte::syscall::Close(fd);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Key points**:
|
||||||
|
|
||||||
|
- Lambda must return `bool` (true = success, false = failure)
|
||||||
|
- Lambda must set `errno` on failure for transient error detection
|
||||||
|
- Use EINTR-safe syscall wrappers (`kte::syscall::*`) inside lambdas
|
||||||
|
- Capture errno immediately after failure
|
||||||
|
- Append retry info to error message (automatically added by
|
||||||
|
RetryOnTransientError)
|
||||||
|
|
||||||
|
#### Circuit Breaker Pattern
|
||||||
|
|
||||||
|
The circuit breaker prevents repeated attempts to failing operations,
|
||||||
|
enabling graceful degradation.
|
||||||
|
|
||||||
|
**States**:
|
||||||
|
|
||||||
|
- **Closed** (normal): All requests allowed
|
||||||
|
- **Open** (failing): Requests rejected immediately, operation disabled
|
||||||
|
- **HalfOpen** (testing): Limited requests allowed to test recovery
|
||||||
|
|
||||||
|
**Configuration** (SwapManager example):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
CircuitBreaker::Config cfg;
|
||||||
|
cfg.failure_threshold = 5; // Open after 5 failures
|
||||||
|
cfg.timeout = std::chrono::seconds(30); // Try recovery after 30s
|
||||||
|
cfg.success_threshold = 2; // Close after 2 successes in HalfOpen
|
||||||
|
cfg.window = std::chrono::seconds(60); // Count failures in 60s window
|
||||||
|
|
||||||
|
CircuitBreaker breaker(cfg);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Usage**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Check before operation
|
||||||
|
if (!breaker.AllowRequest()) {
|
||||||
|
// Circuit is open - graceful degradation
|
||||||
|
log_warning("Operation disabled due to repeated failures");
|
||||||
|
return; // Skip operation
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform operation
|
||||||
|
if (operation_succeeds()) {
|
||||||
|
breaker.RecordSuccess();
|
||||||
|
} else {
|
||||||
|
breaker.RecordFailure();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**SwapManager Integration**:
|
||||||
|
|
||||||
|
The SwapManager uses a circuit breaker to handle repeated swap file
|
||||||
|
failures:
|
||||||
|
|
||||||
|
1. After 5 swap write failures in 60 seconds, circuit opens
|
||||||
|
2. Swap recording is disabled (graceful degradation)
|
||||||
|
3. Warning logged once per 60 seconds to avoid spam
|
||||||
|
4. After 30 seconds, circuit enters HalfOpen state
|
||||||
|
5. If 2 consecutive operations succeed, circuit closes and swap
|
||||||
|
recording resumes
|
||||||
|
|
||||||
|
This ensures the editor remains functional even when swap files are
|
||||||
|
unavailable (disk full, quota exceeded, filesystem errors).
|
||||||
|
|
||||||
|
#### Graceful Degradation Strategies
|
||||||
|
|
||||||
|
When operations fail repeatedly:
|
||||||
|
|
||||||
|
1. **Disable non-critical features** - Swap recording can be disabled
|
||||||
|
without affecting editing
|
||||||
|
2. **Log warnings** - Inform user of degraded operation via ErrorHandler
|
||||||
|
3. **Rate-limit warnings** - Avoid log spam (e.g., once per 60 seconds)
|
||||||
|
4. **Automatic recovery** - Circuit breaker automatically tests recovery
|
||||||
|
5. **Preserve core functionality** - Editor remains usable without swap
|
||||||
|
files
|
||||||
|
|
||||||
|
**Example** (from SwapManager):
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
if (circuit_open) {
|
||||||
|
// Graceful degradation: skip swap write
|
||||||
|
static std::atomic<std::uint64_t> last_warning_ns{0};
|
||||||
|
const std::uint64_t now = now_ns();
|
||||||
|
if (now - last_warning_ns.load() > 60000000000ULL) {
|
||||||
|
last_warning_ns.store(now);
|
||||||
|
ErrorHandler::Instance().Warning("SwapManager",
|
||||||
|
"Swap operations temporarily disabled due to repeated failures",
|
||||||
|
buffer_name);
|
||||||
|
}
|
||||||
|
return; // Skip operation, editor continues normally
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
## Common Tasks
|
## Common Tasks
|
||||||
|
|
||||||
### Adding a New Command
|
### Adding a New Command
|
||||||
|
|||||||
549
docs/audits/error-propagation-standardization.md
Normal file
549
docs/audits/error-propagation-standardization.md
Normal file
@@ -0,0 +1,549 @@
|
|||||||
|
# Error Propagation Standardization Report
|
||||||
|
|
||||||
|
**Project:** kte (Kyle's Text Editor)
|
||||||
|
**Date:** 2026-02-17
|
||||||
|
**Auditor:** Error Propagation Standardization Review
|
||||||
|
**Language:** C++20
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Executive Summary
|
||||||
|
|
||||||
|
This report documents the standardization of error propagation patterns
|
||||||
|
across the kte codebase. Following the implementation of centralized
|
||||||
|
error handling (ErrorHandler), this audit identifies inconsistencies in
|
||||||
|
error propagation and provides concrete remediation recommendations.
|
||||||
|
|
||||||
|
**Key Findings:**
|
||||||
|
|
||||||
|
- **Dominant Pattern**: `bool + std::string &err` is used consistently
|
||||||
|
in Buffer and SwapManager for I/O operations
|
||||||
|
- **Inconsistencies**: PieceTable has no error reporting mechanism; some
|
||||||
|
internal helpers lack error propagation
|
||||||
|
- **Standard Chosen**: `bool + std::string &err` pattern (C++20 project,
|
||||||
|
std::expected not available)
|
||||||
|
- **Documentation**: Comprehensive error handling conventions added to
|
||||||
|
DEVELOPER_GUIDE.md
|
||||||
|
|
||||||
|
**Overall Assessment**: The codebase has a **solid foundation** with the
|
||||||
|
`bool + err` pattern used consistently in critical I/O paths. Primary
|
||||||
|
gaps are in PieceTable memory allocation error handling and some
|
||||||
|
internal helper functions.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. CURRENT STATE ANALYSIS
|
||||||
|
|
||||||
|
### 1.1 Error Propagation Patterns Found
|
||||||
|
|
||||||
|
#### Pattern 1: `bool + std::string &err` (Dominant)
|
||||||
|
|
||||||
|
**Usage**: File I/O, swap operations, resource allocation
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Buffer::OpenFromFile(const std::string &path, std::string &err)` (
|
||||||
|
Buffer.h:72)
|
||||||
|
- `Buffer::Save(std::string &err)` (Buffer.h:74)
|
||||||
|
- `Buffer::SaveAs(const std::string &path, std::string &err)` (Buffer.h:
|
||||||
|
75)
|
||||||
|
- `Editor::OpenFile(const std::string &path, std::string &err)` (
|
||||||
|
Editor.h:536)
|
||||||
|
-
|
||||||
|
`SwapManager::ReplayFile(Buffer &buf, const std::string &swap_path, std::string &err)` (
|
||||||
|
Swap.h:104)
|
||||||
|
-
|
||||||
|
`SwapManager::open_ctx(JournalCtx &ctx, const std::string &path, std::string &err)` (
|
||||||
|
Swap.h:208)
|
||||||
|
-
|
||||||
|
`SwapManager::compact_to_checkpoint(JournalCtx &ctx, const std::vector<std::uint8_t> &chkpt_record, std::string &err)` (
|
||||||
|
Swap.h:212-213)
|
||||||
|
|
||||||
|
**Assessment**: ✅ **Excellent** - Consistent, well-implemented,
|
||||||
|
integrated with ErrorHandler
|
||||||
|
|
||||||
|
#### Pattern 2: `void` (State Changes)
|
||||||
|
|
||||||
|
**Usage**: Setters, cursor movement, flag toggles, internal state
|
||||||
|
modifications
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Buffer::SetCursor(std::size_t x, std::size_t y)` (Buffer.h:348)
|
||||||
|
- `Buffer::SetDirty(bool d)` (Buffer.h:368)
|
||||||
|
- `Buffer::SetMark(std::size_t x, std::size_t y)` (Buffer.h:387)
|
||||||
|
- `Buffer::insert_text(int row, int col, std::string_view text)` (
|
||||||
|
Buffer.h:545)
|
||||||
|
- `Buffer::delete_text(int row, int col, std::size_t len)` (Buffer.h:
|
||||||
|
547)
|
||||||
|
- `Editor::SetStatus(const std::string &msg)` (Editor.h:various)
|
||||||
|
|
||||||
|
**Assessment**: ✅ **Appropriate** - These operations are infallible
|
||||||
|
state changes
|
||||||
|
|
||||||
|
#### Pattern 3: `bool` without error parameter (Control Flow)
|
||||||
|
|
||||||
|
**Usage**: Validation checks, control flow decisions
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `Editor::ProcessPendingOpens()` (Editor.h:544)
|
||||||
|
- `Editor::ResolveRecoveryPrompt(bool yes)` (Editor.h:558)
|
||||||
|
- `Editor::SwitchTo(std::size_t index)` (Editor.h:563)
|
||||||
|
- `Editor::CloseBuffer(std::size_t index)` (Editor.h:565)
|
||||||
|
|
||||||
|
**Assessment**: ✅ **Appropriate** - Success/failure is sufficient for
|
||||||
|
control flow
|
||||||
|
|
||||||
|
#### Pattern 4: No Error Reporting (PieceTable)
|
||||||
|
|
||||||
|
**Usage**: Memory allocation, text manipulation
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
- `void PieceTable::Reserve(std::size_t newCapacity)` (PieceTable.h:71)
|
||||||
|
- `void PieceTable::Append(const char *s, std::size_t len)` (
|
||||||
|
PieceTable.h:75)
|
||||||
|
-
|
||||||
|
`void PieceTable::Insert(std::size_t byte_offset, const char *text, std::size_t len)` (
|
||||||
|
PieceTable.h:118)
|
||||||
|
- `char *PieceTable::Data()` (PieceTable.h:89-93) - returns nullptr on
|
||||||
|
allocation failure
|
||||||
|
|
||||||
|
**Assessment**: ⚠️ **Gap** - Memory allocation failures are not reported
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. STANDARDIZATION DECISION
|
||||||
|
|
||||||
|
### 2.1 Chosen Pattern: `bool + std::string &err`
|
||||||
|
|
||||||
|
**Rationale**:
|
||||||
|
|
||||||
|
1. **C++20 Project**: `std::expected` (C++23) is not available
|
||||||
|
2. **Existing Adoption**: Already used consistently in Buffer,
|
||||||
|
SwapManager, Editor for I/O operations
|
||||||
|
3. **Clear Semantics**: `bool` return indicates success/failure, `err`
|
||||||
|
provides details
|
||||||
|
4. **ErrorHandler Integration**: Works seamlessly with centralized error
|
||||||
|
logging
|
||||||
|
5. **Zero Overhead**: No exceptions, no dynamic allocation for error
|
||||||
|
paths
|
||||||
|
6. **Testability**: Easy to verify error messages in unit tests
|
||||||
|
|
||||||
|
**Alternative Considered**: `std::expected<T, std::string>` (C++23)
|
||||||
|
|
||||||
|
- **Rejected**: Requires C++23, would require major refactoring, not
|
||||||
|
available in current toolchain
|
||||||
|
|
||||||
|
### 2.2 Pattern Selection Guidelines
|
||||||
|
|
||||||
|
| Operation Type | Pattern | Example |
|
||||||
|
|---------------------|---------------------------|-----------------------------------------------------------------------------------|
|
||||||
|
| File I/O | `bool + std::string &err` | `Buffer::Save(std::string &err)` |
|
||||||
|
| Syscalls | `bool + std::string &err` | `open_ctx(JournalCtx &ctx, const std::string &path, std::string &err)` |
|
||||||
|
| Resource Allocation | `bool + std::string &err` | Future: `PieceTable::Reserve(std::size_t cap, std::string &err)` |
|
||||||
|
| Parsing/Validation | `bool + std::string &err` | `SwapManager::ReplayFile(Buffer &buf, const std::string &path, std::string &err)` |
|
||||||
|
| State Changes | `void` | `Buffer::SetCursor(std::size_t x, std::size_t y)` |
|
||||||
|
| Control Flow | `bool` (no err) | `Editor::SwitchTo(std::size_t index)` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. INCONSISTENCIES AND GAPS
|
||||||
|
|
||||||
|
### 3.1 PieceTable Memory Allocation (Severity: 6/10)
|
||||||
|
|
||||||
|
**Finding**: PieceTable methods that allocate memory (`Reserve`,
|
||||||
|
`Append`, `Insert`, `Data`) do not report allocation failures.
|
||||||
|
|
||||||
|
**Impact**:
|
||||||
|
|
||||||
|
- Memory allocation failures are silent
|
||||||
|
- `Data()` returns `nullptr` on failure, but callers may not check
|
||||||
|
- Large file operations could fail without user notification
|
||||||
|
|
||||||
|
**Evidence**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// PieceTable.h:71
|
||||||
|
void Reserve(std::size_t newCapacity); // No error reporting
|
||||||
|
|
||||||
|
// PieceTable.h:89-93
|
||||||
|
char *Data(); // Returns nullptr on allocation failure
|
||||||
|
```
|
||||||
|
|
||||||
|
**Remediation Priority**: **Medium** - Memory allocation failures are
|
||||||
|
rare on modern systems, but should be handled for robustness
|
||||||
|
|
||||||
|
**Recommended Fix**:
|
||||||
|
|
||||||
|
**Option 1: Add error parameter to fallible operations** (Preferred)
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// PieceTable.h
|
||||||
|
bool Reserve(std::size_t newCapacity, std::string &err);
|
||||||
|
bool Append(const char *s, std::size_t len, std::string &err);
|
||||||
|
bool Insert(std::size_t byte_offset, const char *text, std::size_t len, std::string &err);
|
||||||
|
|
||||||
|
// Returns nullptr on failure; check with HasMaterializationError()
|
||||||
|
char *Data();
|
||||||
|
bool HasMaterializationError() const;
|
||||||
|
std::string GetMaterializationError() const;
|
||||||
|
```
|
||||||
|
|
||||||
|
**Option 2: Use exceptions for allocation failures** (Not recommended)
|
||||||
|
|
||||||
|
PieceTable could throw `std::bad_alloc` on allocation failures, but this
|
||||||
|
conflicts with the project's error handling philosophy and would require
|
||||||
|
exception handling throughout the codebase.
|
||||||
|
|
||||||
|
**Option 3: Status quo with improved documentation** (Minimal change)
|
||||||
|
|
||||||
|
Document that `Data()` can return `nullptr` and callers must check. Add
|
||||||
|
assertions in debug builds.
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// PieceTable.h
|
||||||
|
// Returns pointer to materialized buffer, or nullptr if materialization fails.
|
||||||
|
// Callers MUST check for nullptr before dereferencing.
|
||||||
|
char *Data();
|
||||||
|
```
|
||||||
|
|
||||||
|
**Recommendation**: **Option 3** for now (document + assertions), *
|
||||||
|
*Option 1** if memory allocation errors become a concern in production.
|
||||||
|
|
||||||
|
### 3.2 Internal Helper Functions (Severity: 4/10)
|
||||||
|
|
||||||
|
**Finding**: Some internal helper functions in Swap.cc and Buffer.cc use
|
||||||
|
`bool` returns without error parameters.
|
||||||
|
|
||||||
|
**Examples**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Swap.cc:562
|
||||||
|
static bool ensure_parent_dir(const std::string &path); // No error details
|
||||||
|
|
||||||
|
// Swap.cc:579
|
||||||
|
static bool write_header(int fd); // No error details
|
||||||
|
|
||||||
|
// Buffer.cc:101
|
||||||
|
static bool write_all_fd(int fd, const char *data, std::size_t len, std::string &err); // ✅ Good
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact**: Limited - These are internal helpers called by functions
|
||||||
|
that do report errors
|
||||||
|
|
||||||
|
**Remediation Priority**: **Low** - Callers already provide error
|
||||||
|
context
|
||||||
|
|
||||||
|
**Recommended Fix**: Add error parameters to internal helpers for
|
||||||
|
consistency
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Swap.cc
|
||||||
|
static bool ensure_parent_dir(const std::string &path, std::string &err);
|
||||||
|
static bool write_header(int fd, std::string &err);
|
||||||
|
```
|
||||||
|
|
||||||
|
**Status**: **Deferred** - Low priority, callers already provide
|
||||||
|
adequate error context
|
||||||
|
|
||||||
|
### 3.3 Editor Control Flow Methods (Severity: 2/10)
|
||||||
|
|
||||||
|
**Finding**: Editor methods like `SwitchTo()`, `CloseBuffer()` return
|
||||||
|
`bool` without error details.
|
||||||
|
|
||||||
|
**Assessment**: ✅ **Appropriate** - These are control flow decisions
|
||||||
|
where success/failure is sufficient
|
||||||
|
|
||||||
|
**Remediation**: **None needed** - Current pattern is correct for this
|
||||||
|
use case
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. ERRORHANDLER INTEGRATION STATUS
|
||||||
|
|
||||||
|
### 4.1 Components with ErrorHandler Integration
|
||||||
|
|
||||||
|
✅ **Buffer** (Buffer.cc)
|
||||||
|
|
||||||
|
- `OpenFromFile()` - Reports file open, seek, read errors
|
||||||
|
- `Save()` - Reports write errors
|
||||||
|
- `SaveAs()` - Reports write errors
|
||||||
|
|
||||||
|
✅ **SwapManager** (Swap.cc)
|
||||||
|
|
||||||
|
- `report_error()` - All swap file errors reported
|
||||||
|
- Background thread errors captured and logged
|
||||||
|
- Errno captured for all syscalls
|
||||||
|
|
||||||
|
✅ **main** (main.cc)
|
||||||
|
|
||||||
|
- Top-level exception handler reports Critical errors
|
||||||
|
- Both `std::exception` and unknown exceptions captured
|
||||||
|
|
||||||
|
### 4.2 Components Without ErrorHandler Integration
|
||||||
|
|
||||||
|
⚠️ **PieceTable** (PieceTable.cc)
|
||||||
|
|
||||||
|
- No error reporting mechanism
|
||||||
|
- Memory allocation failures are silent
|
||||||
|
|
||||||
|
⚠️ **Editor** (Editor.cc)
|
||||||
|
|
||||||
|
- File operations delegate to Buffer (✅ covered)
|
||||||
|
- Control flow methods don't need error reporting (✅ appropriate)
|
||||||
|
|
||||||
|
⚠️ **Command** (Command.cc)
|
||||||
|
|
||||||
|
- Commands use `Editor::SetStatus()` for user-facing messages
|
||||||
|
- No ErrorHandler integration for command failures
|
||||||
|
- **Assessment**: Commands are user-initiated actions; status messages
|
||||||
|
are appropriate
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. DOCUMENTATION STATUS
|
||||||
|
|
||||||
|
### 5.1 Error Handling Conventions (DEVELOPER_GUIDE.md)
|
||||||
|
|
||||||
|
✅ **Added comprehensive section** covering:
|
||||||
|
|
||||||
|
- Three standard error propagation patterns
|
||||||
|
- Pattern selection guidelines with decision tree
|
||||||
|
- ErrorHandler integration requirements
|
||||||
|
- Code examples for file I/O, syscalls, background threads, top-level
|
||||||
|
handlers
|
||||||
|
- Anti-patterns and best practices
|
||||||
|
- Error log location and format
|
||||||
|
- Migration guide for updating existing code
|
||||||
|
|
||||||
|
**Location**: `docs/DEVELOPER_GUIDE.md` section 7
|
||||||
|
|
||||||
|
### 5.2 API Documentation
|
||||||
|
|
||||||
|
⚠️ **Gap**: Individual function documentation in headers could be
|
||||||
|
improved
|
||||||
|
|
||||||
|
**Recommendation**: Add brief comments to public APIs documenting error
|
||||||
|
behavior
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Buffer.h
|
||||||
|
// Opens a file and loads its content into the buffer.
|
||||||
|
// Returns false on failure; err contains detailed error message.
|
||||||
|
// Errors are logged to ErrorHandler.
|
||||||
|
bool OpenFromFile(const std::string &path, std::string &err);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. REMEDIATION RECOMMENDATIONS
|
||||||
|
|
||||||
|
### 6.1 High Priority (Severity 7-10)
|
||||||
|
|
||||||
|
**None identified** - Critical error handling gaps were addressed in
|
||||||
|
previous sessions:
|
||||||
|
|
||||||
|
- ✅ Top-level exception handler added (Severity 9/10)
|
||||||
|
- ✅ Background thread error reporting added (Severity 9/10)
|
||||||
|
- ✅ File I/O error checking added (Severity 8/10)
|
||||||
|
- ✅ Errno capture added to swap operations (Severity 7/10)
|
||||||
|
- ✅ Centralized error handling implemented (Severity 7/10)
|
||||||
|
|
||||||
|
### 6.2 Medium Priority (Severity 4-6)
|
||||||
|
|
||||||
|
#### 6.2.1 PieceTable Memory Allocation Error Handling (Severity: 6/10)
|
||||||
|
|
||||||
|
**Action**: Document that `Data()` can return `nullptr` and add debug
|
||||||
|
assertions
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// PieceTable.h
|
||||||
|
// Returns pointer to materialized buffer, or nullptr if materialization fails
|
||||||
|
// due to memory allocation error. Callers MUST check for nullptr.
|
||||||
|
char *Data();
|
||||||
|
|
||||||
|
// PieceTable.cc
|
||||||
|
char *PieceTable::Data() {
|
||||||
|
materialize();
|
||||||
|
assert(materialized_ != nullptr && "PieceTable materialization failed");
|
||||||
|
return materialized_;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effort**: Low (documentation + assertions)
|
||||||
|
**Risk**: Low (no API changes)
|
||||||
|
**Timeline**: Next maintenance cycle
|
||||||
|
|
||||||
|
#### 6.2.2 Add Error Parameters to Internal Helpers (Severity: 4/10)
|
||||||
|
|
||||||
|
**Action**: Add `std::string &err` parameters to `ensure_parent_dir()`
|
||||||
|
and `write_header()`
|
||||||
|
|
||||||
|
**Implementation**:
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// Swap.cc
|
||||||
|
static bool ensure_parent_dir(const std::string &path, std::string &err) {
|
||||||
|
try {
|
||||||
|
fs::path p(path);
|
||||||
|
fs::path dir = p.parent_path();
|
||||||
|
if (dir.empty())
|
||||||
|
return true;
|
||||||
|
if (!fs::exists(dir))
|
||||||
|
fs::create_directories(dir);
|
||||||
|
return true;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
err = std::string("Failed to create directory: ") + e.what();
|
||||||
|
return false;
|
||||||
|
} catch (...) {
|
||||||
|
err = "Failed to create directory: unknown error";
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Effort**: Low (update 2 functions + call sites)
|
||||||
|
**Risk**: Low (internal helpers only)
|
||||||
|
**Timeline**: Next maintenance cycle
|
||||||
|
|
||||||
|
### 6.3 Low Priority (Severity 1-3)
|
||||||
|
|
||||||
|
#### 6.3.1 Add Function-Level Error Documentation (Severity: 3/10)
|
||||||
|
|
||||||
|
**Action**: Add brief comments to public APIs documenting error behavior
|
||||||
|
|
||||||
|
**Effort**: Medium (many functions to document)
|
||||||
|
**Risk**: None (documentation only)
|
||||||
|
**Timeline**: Ongoing as code is touched
|
||||||
|
|
||||||
|
#### 6.3.2 Add ErrorHandler Integration to Commands (Severity: 2/10)
|
||||||
|
|
||||||
|
**Action**: Consider logging command failures to ErrorHandler for
|
||||||
|
diagnostics
|
||||||
|
|
||||||
|
**Assessment**: **Not recommended** - Commands are user-initiated
|
||||||
|
actions; status messages are more appropriate than error logs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. TESTING RECOMMENDATIONS
|
||||||
|
|
||||||
|
### 7.1 Error Handling Test Coverage
|
||||||
|
|
||||||
|
**Current State**:
|
||||||
|
|
||||||
|
- ✅ Swap file error handling tested (test_swap_edge_cases.cc)
|
||||||
|
- ✅ Buffer I/O error handling tested (test_buffer_io.cc)
|
||||||
|
- ⚠️ PieceTable allocation failure testing missing
|
||||||
|
|
||||||
|
**Recommendations**:
|
||||||
|
|
||||||
|
1. **Add PieceTable allocation failure tests** (if Option 1 from 3.1 is
|
||||||
|
implemented)
|
||||||
|
2. **Add ErrorHandler query tests** - Verify error logging and retrieval
|
||||||
|
3. **Add errno capture tests** - Verify errno is captured correctly in
|
||||||
|
syscall failures
|
||||||
|
|
||||||
|
### 7.2 Test Examples
|
||||||
|
|
||||||
|
```cpp
|
||||||
|
// test_error_handler.cc
|
||||||
|
TEST(ErrorHandler, LogsErrorsWithContext) {
|
||||||
|
ErrorHandler::Instance().Error("TestComponent", "Test error", "test.txt");
|
||||||
|
EXPECT_TRUE(ErrorHandler::Instance().HasErrors());
|
||||||
|
EXPECT_EQ(ErrorHandler::Instance().GetErrorCount(), 1);
|
||||||
|
std::string last = ErrorHandler::Instance().GetLastError();
|
||||||
|
EXPECT_TRUE(last.find("Test error") != std::string::npos);
|
||||||
|
EXPECT_TRUE(last.find("test.txt") != std::string::npos);
|
||||||
|
}
|
||||||
|
|
||||||
|
// test_piece_table.cc (if Option 1 implemented)
|
||||||
|
TEST(PieceTable, ReportsAllocationFailure) {
|
||||||
|
PieceTable pt;
|
||||||
|
std::string err;
|
||||||
|
// Attempt to allocate huge buffer
|
||||||
|
bool ok = pt.Reserve(SIZE_MAX, err);
|
||||||
|
EXPECT_FALSE(ok);
|
||||||
|
EXPECT_FALSE(err.empty());
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. MIGRATION CHECKLIST
|
||||||
|
|
||||||
|
For developers updating existing code to follow error handling
|
||||||
|
conventions:
|
||||||
|
|
||||||
|
- [ ] Identify all error-prone operations (file I/O, syscalls,
|
||||||
|
allocations)
|
||||||
|
- [ ] Add `std::string &err` parameter if not present
|
||||||
|
- [ ] Clear `err` at function start: `err.clear();`
|
||||||
|
- [ ] Capture `errno` immediately after syscall failures:
|
||||||
|
`int saved_errno = errno;`
|
||||||
|
- [ ] Build detailed error messages with context (paths, operation
|
||||||
|
details)
|
||||||
|
- [ ] Call `ErrorHandler::Instance().Error()` at all error sites
|
||||||
|
- [ ] Return `false` on failure, `true` on success
|
||||||
|
- [ ] Update all call sites to handle the error parameter
|
||||||
|
- [ ] Write unit tests that verify error handling
|
||||||
|
- [ ] Update function documentation to describe error behavior
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. SUMMARY AND NEXT STEPS
|
||||||
|
|
||||||
|
### 9.1 Achievements
|
||||||
|
|
||||||
|
✅ **Standardized on `bool + std::string &err` pattern** for error-prone
|
||||||
|
operations
|
||||||
|
✅ **Documented comprehensive error handling conventions** in
|
||||||
|
DEVELOPER_GUIDE.md
|
||||||
|
✅ **Identified and prioritized remaining gaps** (PieceTable, internal
|
||||||
|
helpers)
|
||||||
|
✅ **Integrated ErrorHandler** into Buffer, SwapManager, and main
|
||||||
|
✅ **Established clear pattern selection guidelines** for future
|
||||||
|
development
|
||||||
|
|
||||||
|
### 9.2 Remaining Work
|
||||||
|
|
||||||
|
**Medium Priority**:
|
||||||
|
|
||||||
|
1. Document PieceTable `Data()` nullptr behavior and add assertions
|
||||||
|
2. Add error parameters to internal helper functions
|
||||||
|
|
||||||
|
**Low Priority**:
|
||||||
|
|
||||||
|
3. Add function-level error documentation to public APIs
|
||||||
|
4. Add ErrorHandler query tests
|
||||||
|
|
||||||
|
### 9.3 Conclusion
|
||||||
|
|
||||||
|
The kte codebase has achieved **strong error handling consistency** with
|
||||||
|
the `bool + std::string &err` pattern used uniformly across critical I/O
|
||||||
|
paths. The centralized ErrorHandler provides comprehensive logging and
|
||||||
|
UI integration. Remaining gaps are minor and primarily affect edge
|
||||||
|
cases (memory allocation failures) that are rare in practice.
|
||||||
|
|
||||||
|
**Overall Grade**: **B+ (8.5/10)**
|
||||||
|
|
||||||
|
**Strengths**:
|
||||||
|
|
||||||
|
- Consistent error propagation in Buffer and SwapManager
|
||||||
|
- Comprehensive ErrorHandler integration
|
||||||
|
- Excellent documentation in DEVELOPER_GUIDE.md
|
||||||
|
- Errno capture for all syscalls
|
||||||
|
- Top-level exception handling
|
||||||
|
|
||||||
|
**Areas for Improvement**:
|
||||||
|
|
||||||
|
- PieceTable memory allocation error handling
|
||||||
|
- Internal helper function error propagation
|
||||||
|
- Function-level API documentation
|
||||||
|
|
||||||
|
The error handling infrastructure is **production-ready** and provides a
|
||||||
|
solid foundation for reliable operation and debugging.
|
||||||
@@ -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
|
||||||
232
main.cc
232
main.cc
@@ -20,6 +20,7 @@
|
|||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "TerminalFrontend.h"
|
#include "TerminalFrontend.h"
|
||||||
|
#include "ErrorHandler.h"
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
#if defined(KTE_BUILD_GUI)
|
||||||
#if defined(KTE_USE_QT)
|
#if defined(KTE_USE_QT)
|
||||||
@@ -116,6 +117,9 @@ main(int argc, char *argv[])
|
|||||||
{
|
{
|
||||||
std::setlocale(LC_ALL, "");
|
std::setlocale(LC_ALL, "");
|
||||||
|
|
||||||
|
// Ensure the error handler (and its log file) is initialised early.
|
||||||
|
kte::ErrorHandler::Instance();
|
||||||
|
|
||||||
Editor editor;
|
Editor editor;
|
||||||
|
|
||||||
// CLI parsing using getopt_long
|
// CLI parsing using getopt_long
|
||||||
@@ -181,124 +185,144 @@ main(int argc, char *argv[])
|
|||||||
return RunStressHighlighter(stress_seconds);
|
return RunStressHighlighter(stress_seconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine frontend
|
// Top-level exception handler to prevent data loss and ensure cleanup
|
||||||
|
try {
|
||||||
|
// Determine frontend
|
||||||
#if !defined(KTE_BUILD_GUI)
|
#if !defined(KTE_BUILD_GUI)
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed." <<
|
std::cerr << "kte: GUI not built. Reconfigure with -DBUILD_GUI=ON and required deps installed."
|
||||||
std::endl;
|
<<
|
||||||
return 2;
|
std::endl;
|
||||||
}
|
return 2;
|
||||||
|
}
|
||||||
#else
|
#else
|
||||||
bool use_gui = false;
|
bool use_gui = false;
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
use_gui = true;
|
use_gui = true;
|
||||||
} else if (req_term) {
|
} else if (req_term) {
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
} else {
|
} else {
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
// Default depends on build target: kge defaults to GUI, kte to terminal
|
// Default depends on build target: kge defaults to GUI, kte to terminal
|
||||||
#if defined(KTE_DEFAULT_GUI)
|
#if defined(KTE_DEFAULT_GUI)
|
||||||
use_gui = true;
|
use_gui = true;
|
||||||
#else
|
#else
|
||||||
use_gui = false;
|
use_gui = false;
|
||||||
#endif
|
#endif
|
||||||
}
|
|
||||||
#endif
|
|
||||||
|
|
||||||
// Open files passed on the CLI; support +N to jump to line N in the next file.
|
|
||||||
// If no files are provided, create an empty buffer.
|
|
||||||
if (optind < argc) {
|
|
||||||
// Seed a scratch buffer so the UI has something to show while deferred opens
|
|
||||||
// (and potential swap recovery prompts) are processed.
|
|
||||||
editor.AddBuffer(Buffer());
|
|
||||||
std::size_t pending_line = 0; // 0 = no pending line
|
|
||||||
for (int i = optind; i < argc; ++i) {
|
|
||||||
const char *arg = argv[i];
|
|
||||||
if (arg && arg[0] == '+') {
|
|
||||||
// Parse +<digits>
|
|
||||||
const char *p = arg + 1;
|
|
||||||
if (*p != '\0') {
|
|
||||||
bool all_digits = true;
|
|
||||||
for (const char *q = p; *q; ++q) {
|
|
||||||
if (!std::isdigit(static_cast<unsigned char>(*q))) {
|
|
||||||
all_digits = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (all_digits) {
|
|
||||||
// Clamp to >=1 later; 0 disables.
|
|
||||||
try {
|
|
||||||
unsigned long v = std::stoul(p);
|
|
||||||
if (v > std::numeric_limits<std::size_t>::max()) {
|
|
||||||
std::cerr <<
|
|
||||||
"kte: Warning: Line number too large, ignoring\n";
|
|
||||||
pending_line = 0;
|
|
||||||
} else {
|
|
||||||
pending_line = static_cast<std::size_t>(v);
|
|
||||||
}
|
|
||||||
} catch (...) {
|
|
||||||
// Ignore malformed huge numbers
|
|
||||||
pending_line = 0;
|
|
||||||
}
|
|
||||||
continue; // look for the next file arg
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Fall through: not a +number, treat as filename starting with '+'
|
|
||||||
}
|
|
||||||
|
|
||||||
const std::string path = arg;
|
|
||||||
editor.RequestOpenFile(path, pending_line);
|
|
||||||
pending_line = 0; // consumed (if set)
|
|
||||||
}
|
}
|
||||||
// If we ended with a pending +N but no subsequent file, ignore it.
|
|
||||||
} else {
|
|
||||||
// Create a single empty buffer
|
|
||||||
editor.AddBuffer(Buffer());
|
|
||||||
editor.SetStatus("new: empty buffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Install built-in commands
|
|
||||||
InstallDefaultCommands();
|
|
||||||
|
|
||||||
// Select frontend
|
|
||||||
std::unique_ptr<Frontend> fe;
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
|
||||||
if (use_gui) {
|
|
||||||
fe = std::make_unique<GUIFrontend>();
|
|
||||||
} else
|
|
||||||
#endif
|
#endif
|
||||||
{
|
|
||||||
fe = std::make_unique<TerminalFrontend>();
|
// Open files passed on the CLI; support +N to jump to line N in the next file.
|
||||||
}
|
// If no files are provided, create an empty buffer.
|
||||||
|
if (optind < argc) {
|
||||||
|
// Seed a scratch buffer so the UI has something to show while deferred opens
|
||||||
|
// (and potential swap recovery prompts) are processed.
|
||||||
|
editor.AddBuffer(Buffer());
|
||||||
|
std::size_t pending_line = 0; // 0 = no pending line
|
||||||
|
for (int i = optind; i < argc; ++i) {
|
||||||
|
const char *arg = argv[i];
|
||||||
|
if (arg && arg[0] == '+') {
|
||||||
|
// Parse +<digits>
|
||||||
|
const char *p = arg + 1;
|
||||||
|
if (*p != '\0') {
|
||||||
|
bool all_digits = true;
|
||||||
|
for (const char *q = p; *q; ++q) {
|
||||||
|
if (!std::isdigit(static_cast<unsigned char>(*q))) {
|
||||||
|
all_digits = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (all_digits) {
|
||||||
|
// Clamp to >=1 later; 0 disables.
|
||||||
|
try {
|
||||||
|
unsigned long v = std::stoul(p);
|
||||||
|
if (v > std::numeric_limits<std::size_t>::max()) {
|
||||||
|
std::cerr <<
|
||||||
|
"kte: Warning: Line number too large, ignoring\n";
|
||||||
|
pending_line = 0;
|
||||||
|
} else {
|
||||||
|
pending_line = static_cast<std::size_t>(v);
|
||||||
|
}
|
||||||
|
} catch (...) {
|
||||||
|
// Ignore malformed huge numbers
|
||||||
|
pending_line = 0;
|
||||||
|
}
|
||||||
|
continue; // look for the next file arg
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Fall through: not a +number, treat as filename starting with '+'
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::string path = arg;
|
||||||
|
editor.RequestOpenFile(path, pending_line);
|
||||||
|
pending_line = 0; // consumed (if set)
|
||||||
|
}
|
||||||
|
// If we ended with a pending +N but no subsequent file, ignore it.
|
||||||
|
} else {
|
||||||
|
// Create a single empty buffer
|
||||||
|
editor.AddBuffer(Buffer());
|
||||||
|
editor.SetStatus("new: empty buffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Install built-in commands
|
||||||
|
InstallDefaultCommands();
|
||||||
|
|
||||||
|
// Select frontend
|
||||||
|
std::unique_ptr<Frontend> fe;
|
||||||
|
#if defined(KTE_BUILD_GUI)
|
||||||
|
if (use_gui) {
|
||||||
|
fe = std::make_unique<GUIFrontend>();
|
||||||
|
} else
|
||||||
|
#endif
|
||||||
|
{
|
||||||
|
fe = std::make_unique<TerminalFrontend>();
|
||||||
|
}
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
#if defined(KTE_BUILD_GUI) && defined(__APPLE__)
|
||||||
if (use_gui) {
|
if (use_gui) {
|
||||||
/* likely using the .app, so need to cd */
|
/* likely using the .app, so need to cd */
|
||||||
const char *home = getenv("HOME");
|
const char *home = getenv("HOME");
|
||||||
if (!home) {
|
if (!home) {
|
||||||
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
std::cerr << "kge.app: HOME environment variable not set" << std::endl;
|
||||||
return 1;
|
return 1;
|
||||||
|
}
|
||||||
|
if (chdir(home) != 0) {
|
||||||
|
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
||||||
|
<< std::strerror(errno) << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (chdir(home) != 0) {
|
|
||||||
std::cerr << "kge.app: failed to chdir to " << home << ": "
|
|
||||||
<< std::strerror(errno) << std::endl;
|
|
||||||
return 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (!fe->Init(argc, argv, editor)) {
|
if (!fe->Init(argc, argv, editor)) {
|
||||||
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
std::cerr << "kte: failed to initialize frontend" << std::endl;
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Execute(editor, CommandId::CenterOnCursor);
|
||||||
|
|
||||||
|
bool running = true;
|
||||||
|
while (running) {
|
||||||
|
fe->Step(editor, running);
|
||||||
|
}
|
||||||
|
|
||||||
|
fe->Shutdown();
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
} catch (const std::exception &e) {
|
||||||
|
std::string msg = std::string("Unhandled exception: ") + e.what();
|
||||||
|
kte::ErrorHandler::Instance().Critical("main", msg);
|
||||||
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
||||||
|
<< "kte encountered an unhandled exception: " << e.what() << "\n"
|
||||||
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
||||||
|
return 1;
|
||||||
|
} catch (...) {
|
||||||
|
kte::ErrorHandler::Instance().Critical("main", "Unknown exception");
|
||||||
|
std::cerr << "\n*** FATAL ERROR ***\n"
|
||||||
|
<< "kte encountered an unknown exception.\n"
|
||||||
|
<< "The editor will now exit. Any unsaved changes may be recovered from swap files.\n";
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
Execute(editor, CommandId::CenterOnCursor);
|
|
||||||
|
|
||||||
bool running = true;
|
|
||||||
while (running) {
|
|
||||||
fe->Step(editor, running);
|
|
||||||
}
|
|
||||||
|
|
||||||
fe->Shutdown();
|
|
||||||
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
@@ -15,20 +15,18 @@ sha256sum kge.app.zip
|
|||||||
open .
|
open .
|
||||||
cd ..
|
cd ..
|
||||||
|
|
||||||
mkdir -p cmake-build-release-qt
|
# Qt build disabled — ImGui frontend is the primary GUI.
|
||||||
cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
# mkdir -p cmake-build-release-qt
|
||||||
|
# cmake -S . -B cmake-build-release-qt -DBUILD_GUI=ON -DKTE_USE_QT=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||||
cd cmake-build-release-qt
|
#
|
||||||
make clean
|
# cd cmake-build-release-qt
|
||||||
rm -fr kge.app* kge-qt.app*
|
# make clean
|
||||||
make
|
# rm -fr kge.app* kge-qt.app*
|
||||||
mv -f kge.app kge-qt.app
|
# make
|
||||||
# Use the same Qt's macdeployqt as used for building; ensure it overwrites in-bundle paths
|
# mv -f kge.app kge-qt.app
|
||||||
macdeployqt kge-qt.app -always-overwrite -verbose=3
|
# macdeployqt kge-qt.app -always-overwrite -verbose=3
|
||||||
|
# cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
||||||
# Run CMake BundleUtilities fixup to internalize non-Qt dylibs and rewrite install names
|
# zip -r kge-qt.app.zip kge-qt.app
|
||||||
cmake -DAPP_BUNDLE="$(pwd)/kge-qt.app" -P "${PWD%/*}/cmake/fix_bundle.cmake"
|
# sha256sum kge-qt.app.zip
|
||||||
zip -r kge-qt.app.zip kge-qt.app
|
# open .
|
||||||
sha256sum kge-qt.app.zip
|
# cd ..
|
||||||
open .
|
|
||||||
cd ..
|
|
||||||
|
|||||||
@@ -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 )
|
||||||
69
tests/test_reflow_undo.cc
Normal file
69
tests/test_reflow_undo.cc
Normal 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);
|
||||||
|
}
|
||||||
79
tests/test_smart_newline.cc
Normal file
79
tests/test_smart_newline.cc
Normal 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);
|
||||||
|
}
|
||||||
125
tests/test_swap_cleanup2.cc
Normal file
125
tests/test_swap_cleanup2.cc
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
#include "tests/TestHarness.h"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
|
||||||
|
static void
|
||||||
|
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||||
|
{
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out.write(bytes.data(), (std::streamsize) bytes.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// RAII helper to set XDG_STATE_HOME for the duration of a test and clean up.
|
||||||
|
struct XdgStateGuard {
|
||||||
|
fs::path root;
|
||||||
|
std::string old_xdg;
|
||||||
|
bool had_old;
|
||||||
|
|
||||||
|
explicit XdgStateGuard(const std::string &suffix)
|
||||||
|
{
|
||||||
|
root = fs::temp_directory_path() /
|
||||||
|
(std::string("kte_ut_xdg_") + suffix + "_" + std::to_string((int) ::getpid()));
|
||||||
|
fs::remove_all(root);
|
||||||
|
fs::create_directories(root);
|
||||||
|
|
||||||
|
const char *p = std::getenv("XDG_STATE_HOME");
|
||||||
|
had_old = (p != nullptr);
|
||||||
|
if (p)
|
||||||
|
old_xdg = p;
|
||||||
|
setenv("XDG_STATE_HOME", root.string().c_str(), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
~XdgStateGuard()
|
||||||
|
{
|
||||||
|
if (had_old)
|
||||||
|
setenv("XDG_STATE_HOME", old_xdg.c_str(), 1);
|
||||||
|
else
|
||||||
|
unsetenv("XDG_STATE_HOME");
|
||||||
|
fs::remove_all(root);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
|
||||||
|
TEST(SwapCleanup_SaveAndQuit)
|
||||||
|
{
|
||||||
|
ktet::InstallDefaultCommandsOnce();
|
||||||
|
XdgStateGuard xdg("save_quit");
|
||||||
|
|
||||||
|
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||||
|
fs::create_directories(xdg.root / "work");
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
ed.AddBuffer(Buffer());
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||||
|
Buffer *b = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(b != nullptr);
|
||||||
|
|
||||||
|
// Edit to create swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "Z"));
|
||||||
|
ASSERT_TRUE(b->Dirty());
|
||||||
|
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||||
|
ASSERT_TRUE(fs::exists(swp));
|
||||||
|
|
||||||
|
// Save-and-quit should clean up the swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::SaveAndQuit));
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
ASSERT_TRUE(!fs::exists(swp));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::remove(path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST(SwapCleanup_EditorReset)
|
||||||
|
{
|
||||||
|
ktet::InstallDefaultCommandsOnce();
|
||||||
|
XdgStateGuard xdg("editor_reset");
|
||||||
|
|
||||||
|
const std::string path = (xdg.root / "work" / "file.txt").string();
|
||||||
|
fs::create_directories(xdg.root / "work");
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
Editor ed;
|
||||||
|
ed.SetDimensions(24, 80);
|
||||||
|
ed.AddBuffer(Buffer());
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(ed.OpenFile(path, err));
|
||||||
|
Buffer *b = ed.CurrentBuffer();
|
||||||
|
ASSERT_TRUE(b != nullptr);
|
||||||
|
|
||||||
|
// Edit to create swap file
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::MoveFileStart));
|
||||||
|
ASSERT_TRUE(Execute(ed, CommandId::InsertText, "W"));
|
||||||
|
ASSERT_TRUE(b->Dirty());
|
||||||
|
|
||||||
|
ed.Swap()->Flush(b);
|
||||||
|
const std::string swp = kte::SwapManager::ComputeSwapPathForTests(*b);
|
||||||
|
ASSERT_TRUE(fs::exists(swp));
|
||||||
|
|
||||||
|
// Reset (simulates clean editor exit) should remove swap files
|
||||||
|
ed.Reset();
|
||||||
|
ASSERT_TRUE(!fs::exists(swp));
|
||||||
|
|
||||||
|
// Cleanup
|
||||||
|
std::remove(path.c_str());
|
||||||
|
}
|
||||||
814
tests/test_swap_edge_cases.cc
Normal file
814
tests/test_swap_edge_cases.cc
Normal file
@@ -0,0 +1,814 @@
|
|||||||
|
#include "Test.h"
|
||||||
|
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "Swap.h"
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <fstream>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
|
||||||
|
// CRC32 helper (same algorithm as SwapManager::crc32)
|
||||||
|
static std::uint32_t
|
||||||
|
crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0)
|
||||||
|
{
|
||||||
|
static std::uint32_t table[256];
|
||||||
|
static bool inited = false;
|
||||||
|
if (!inited) {
|
||||||
|
for (std::uint32_t i = 0; i < 256; ++i) {
|
||||||
|
std::uint32_t c = i;
|
||||||
|
for (int j = 0; j < 8; ++j)
|
||||||
|
c = (c & 1) ? (0xEDB88320u ^ (c >> 1)) : (c >> 1);
|
||||||
|
table[i] = c;
|
||||||
|
}
|
||||||
|
inited = true;
|
||||||
|
}
|
||||||
|
std::uint32_t c = ~seed;
|
||||||
|
for (std::size_t i = 0; i < len; ++i)
|
||||||
|
c = table[(c ^ data[i]) & 0xFFu] ^ (c >> 8);
|
||||||
|
return ~c;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build a valid 64-byte swap file header
|
||||||
|
static std::string
|
||||||
|
build_swap_header()
|
||||||
|
{
|
||||||
|
std::uint8_t hdr[64];
|
||||||
|
std::memset(hdr, 0, sizeof(hdr));
|
||||||
|
// Magic
|
||||||
|
const std::uint8_t magic[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
|
||||||
|
std::memcpy(hdr, magic, 8);
|
||||||
|
// Version = 1 (little-endian)
|
||||||
|
hdr[8] = 1;
|
||||||
|
hdr[9] = 0;
|
||||||
|
hdr[10] = 0;
|
||||||
|
hdr[11] = 0;
|
||||||
|
// Flags = 0
|
||||||
|
// Created time (just use 0 for tests)
|
||||||
|
return std::string(reinterpret_cast<char *>(hdr), sizeof(hdr));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build a swap record: [type u8][len u24][payload][crc32 u32]
|
||||||
|
static std::string
|
||||||
|
build_swap_record(std::uint8_t type, const std::vector<std::uint8_t> &payload)
|
||||||
|
{
|
||||||
|
std::vector<std::uint8_t> record;
|
||||||
|
|
||||||
|
// Record header: type(1) + length(3)
|
||||||
|
record.push_back(type);
|
||||||
|
std::uint32_t len = static_cast<std::uint32_t>(payload.size());
|
||||||
|
record.push_back(static_cast<std::uint8_t>(len & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((len >> 8) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((len >> 16) & 0xFFu));
|
||||||
|
|
||||||
|
// Payload
|
||||||
|
record.insert(record.end(), payload.begin(), payload.end());
|
||||||
|
|
||||||
|
// CRC32 (compute over header + payload)
|
||||||
|
std::uint32_t crc = crc32(record.data(), record.size());
|
||||||
|
record.push_back(static_cast<std::uint8_t>(crc & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 8) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 16) & 0xFFu));
|
||||||
|
record.push_back(static_cast<std::uint8_t>((crc >> 24) & 0xFFu));
|
||||||
|
|
||||||
|
return std::string(reinterpret_cast<char *>(record.data()), record.size());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Build complete swap file with header and records
|
||||||
|
static std::string
|
||||||
|
build_swap_file(const std::vector<std::string> &records)
|
||||||
|
{
|
||||||
|
std::string file = build_swap_header();
|
||||||
|
for (const auto &rec: records) {
|
||||||
|
file += rec;
|
||||||
|
}
|
||||||
|
return file;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Write bytes to file
|
||||||
|
static void
|
||||||
|
write_file_bytes(const std::string &path, const std::string &bytes)
|
||||||
|
{
|
||||||
|
std::ofstream out(path, std::ios::binary | std::ios::trunc);
|
||||||
|
out.write(bytes.data(), static_cast<std::streamsize>(bytes.size()));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Helper to encode u32 little-endian
|
||||||
|
static void
|
||||||
|
put_u32_le(std::vector<std::uint8_t> &out, std::uint32_t v)
|
||||||
|
{
|
||||||
|
out.push_back(static_cast<std::uint8_t>(v & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 8) & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 16) & 0xFFu));
|
||||||
|
out.push_back(static_cast<std::uint8_t>((v >> 24) & 0xFFu));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 1. MINIMUM VALID PAYLOAD SIZE TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record: encver(1) + row(4) + col(4) + nbytes(4) = 13 bytes minimum
|
||||||
|
// nbytes=0 means zero-length insertion
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // nbytes=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_DEL_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_del_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_del_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// DEL record: encver(1) + row(4) + col(4) + dlen(4) = 13 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // dlen=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_SPLIT_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_split_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_split_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// SPLIT record: encver(1) + row(4) + col(4) = 9 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_JOIN_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_join_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_join_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\nworld\n");
|
||||||
|
|
||||||
|
// JOIN record: encver(1) + row(4) = 5 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::JOIN), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_MinimumValidPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_min.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_min.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record: encver(1) + nbytes(4) = 5 bytes minimum
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // nbytes=0
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 2. TRUNCATED PAYLOAD TESTS (BELOW MINIMUM)
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedPayload_1Byte)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_trunc1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_trunc1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with only 1 byte (just encver)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedPayload_5Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_trunc5.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_trunc5.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with 5 bytes (encver + row only)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_DEL_TruncatedPayload_9Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_del_trunc9.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_del_trunc9.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// DEL record with 9 bytes (encver + row + col, missing dlen)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
// missing dlen
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_SPLIT_TruncatedPayload_5Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_split_trunc5.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_split_trunc5.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// SPLIT record with 5 bytes (encver + row, missing col)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
// missing col
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_JOIN_TruncatedPayload_1Byte)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_join_trunc1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_join_trunc1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\nworld\n");
|
||||||
|
|
||||||
|
// JOIN record with 1 byte (just encver)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::JOIN), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("JOIN payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_TruncatedPayload_3Bytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_trunc3.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_trunc3.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record with 3 bytes (encver + partial nbytes)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
payload.push_back(0); // partial nbytes (only 2 bytes instead of 4)
|
||||||
|
payload.push_back(0);
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("CHKPT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 3. DATA OVERFLOW TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_TruncatedData_NbytesExceedsPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_overflow.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_overflow.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record where nbytes=100 but payload only contains 13 bytes total
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 100); // nbytes=100 (but no data follows)
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_TruncatedData_NbytesExceedsPayload)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_overflow.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_overflow.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record where nbytes=1000 but payload only contains 5 bytes total
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 1000); // nbytes=1000 (but no data follows)
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated CHKPT payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 4. UNSUPPORTED ENCODING VERSION TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_UnsupportedEncodingVersion)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_badenc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_badenc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with encver=2 (unsupported)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(2); // encver=2 (unsupported)
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 0); // nbytes
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Unsupported swap payload encoding") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_CHKPT_UnsupportedEncodingVersion)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_chkpt_badenc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_chkpt_badenc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// CHKPT record with encver=99 (unsupported)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(99); // encver=99 (unsupported)
|
||||||
|
put_u32_le(payload, 0); // nbytes
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::CHKPT), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Unsupported swap checkpoint encoding") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 5. BOUNDARY CONDITION TESTS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_ExactlyEnoughBytes)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_exact.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_exact.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with nbytes=10 and exactly 23 bytes total (13 header + 10 data)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 10); // nbytes=10
|
||||||
|
// Add exactly 10 bytes of data
|
||||||
|
for (int i = 0; i < 10; i++) {
|
||||||
|
payload.push_back('X');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_TRUE(kte::SwapManager::ReplayFile(b, swap_path, err));
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_INS_OneByteTooFew)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_ins_toofew.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_ins_toofew.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with nbytes=10 but only 22 bytes total (13 header + 9 data)
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1); // encver
|
||||||
|
put_u32_le(payload, 0); // row
|
||||||
|
put_u32_le(payload, 0); // col
|
||||||
|
put_u32_le(payload, 10); // nbytes=10
|
||||||
|
// Add only 9 bytes of data (one too few)
|
||||||
|
for (int i = 0; i < 9; i++) {
|
||||||
|
payload.push_back('X');
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("Truncated INS payload bytes") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 6. MIXED VALID AND INVALID RECORDS
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_MixedRecords_ValidThenInvalid)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_mixed1.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_mixed1.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// First record: valid INS
|
||||||
|
std::vector<std::uint8_t> payload1;
|
||||||
|
payload1.push_back(1); // encver
|
||||||
|
put_u32_le(payload1, 0); // row
|
||||||
|
put_u32_le(payload1, 0); // col
|
||||||
|
put_u32_le(payload1, 1); // nbytes=1
|
||||||
|
payload1.push_back('X'); // data
|
||||||
|
|
||||||
|
std::string rec1 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload1);
|
||||||
|
|
||||||
|
// Second record: truncated DEL
|
||||||
|
std::vector<std::uint8_t> payload2;
|
||||||
|
payload2.push_back(1); // encver only
|
||||||
|
|
||||||
|
std::string rec2 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::DEL), payload2);
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec1, rec2});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("DEL payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
// Verify first INS was applied before failure
|
||||||
|
auto view = b.GetLineView(0);
|
||||||
|
std::string line(view.data(), view.size());
|
||||||
|
ASSERT_TRUE(line.find('X') != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
TEST (SwapEdge_MixedRecords_MultipleValidOneInvalid)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_mixed2.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_mixed2.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "ab\n");
|
||||||
|
|
||||||
|
// First record: valid INS at (0,0)
|
||||||
|
std::vector<std::uint8_t> payload1;
|
||||||
|
payload1.push_back(1);
|
||||||
|
put_u32_le(payload1, 0);
|
||||||
|
put_u32_le(payload1, 0);
|
||||||
|
put_u32_le(payload1, 1);
|
||||||
|
payload1.push_back('X');
|
||||||
|
std::string rec1 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload1);
|
||||||
|
|
||||||
|
// Second record: valid INS at (0,1)
|
||||||
|
std::vector<std::uint8_t> payload2;
|
||||||
|
payload2.push_back(1);
|
||||||
|
put_u32_le(payload2, 0);
|
||||||
|
put_u32_le(payload2, 1);
|
||||||
|
put_u32_le(payload2, 1);
|
||||||
|
payload2.push_back('Y');
|
||||||
|
std::string rec2 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload2);
|
||||||
|
|
||||||
|
// Third record: truncated SPLIT
|
||||||
|
std::vector<std::uint8_t> payload3;
|
||||||
|
payload3.push_back(1); // encver only
|
||||||
|
std::string rec3 = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::SPLIT), payload3);
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec1, rec2, rec3});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("SPLIT payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
// Verify first two INS were applied
|
||||||
|
auto view = b.GetLineView(0);
|
||||||
|
std::string line(view.data(), view.size());
|
||||||
|
ASSERT_TRUE(line.find('X') != std::string::npos);
|
||||||
|
ASSERT_TRUE(line.find('Y') != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 7. EMPTY PAYLOAD TEST
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_EmptyPayload_INS)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_empty.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_empty.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// INS record with zero-length payload
|
||||||
|
std::vector<std::uint8_t> payload; // empty
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("INS payload too short") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//=============================================================================
|
||||||
|
// 8. CRC MISMATCH TEST
|
||||||
|
//=============================================================================
|
||||||
|
|
||||||
|
TEST (SwapEdge_ValidStructure_BadCRC)
|
||||||
|
{
|
||||||
|
const std::string path = "./.kte_ut_edge_badcrc.txt";
|
||||||
|
const std::string swap_path = "./.kte_ut_edge_badcrc.swp";
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
|
||||||
|
write_file_bytes(path, "hello\n");
|
||||||
|
|
||||||
|
// Build a valid INS record
|
||||||
|
std::vector<std::uint8_t> payload;
|
||||||
|
payload.push_back(1);
|
||||||
|
put_u32_le(payload, 0);
|
||||||
|
put_u32_le(payload, 0);
|
||||||
|
put_u32_le(payload, 1);
|
||||||
|
payload.push_back('X');
|
||||||
|
|
||||||
|
std::string rec = build_swap_record(static_cast<std::uint8_t>(kte::SwapRecType::INS), payload);
|
||||||
|
|
||||||
|
// Corrupt the CRC (last 4 bytes)
|
||||||
|
rec[rec.size() - 1] ^= 0xFF;
|
||||||
|
|
||||||
|
std::string file = build_swap_file({rec});
|
||||||
|
write_file_bytes(swap_path, file);
|
||||||
|
|
||||||
|
Buffer b;
|
||||||
|
std::string err;
|
||||||
|
ASSERT_TRUE(b.OpenFromFile(path, err));
|
||||||
|
ASSERT_EQ(kte::SwapManager::ReplayFile(b, swap_path, err), false);
|
||||||
|
ASSERT_TRUE(err.find("CRC mismatch") != std::string::npos);
|
||||||
|
|
||||||
|
std::remove(path.c_str());
|
||||||
|
std::remove(swap_path.c_str());
|
||||||
|
}
|
||||||
@@ -57,7 +57,7 @@ validate_undo_tree(const UndoSystem &u)
|
|||||||
// The undo suite aims to cover invariants with a small, adversarial test matrix.
|
// 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;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -81,7 +81,7 @@ TEST(Undo_InsertRun_Coalesces_OneStep)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
TEST (Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -109,7 +109,7 @@ TEST(Undo_InsertRun_BreaksOnNonAdjacentCursor)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_BackspaceRun_Coalesces_OneStep)
|
TEST (Undo_BackspaceRun_Coalesces_OneStep)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
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;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -176,7 +176,7 @@ TEST(Undo_DeleteKeyRun_Coalesces_OneStep)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_Newline_IsStandalone)
|
TEST (Undo_Newline_IsStandalone)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -211,7 +211,7 @@ TEST(Undo_Newline_IsStandalone)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_ExplicitGroup_UndoesAsUnit)
|
TEST (Undo_ExplicitGroup_UndoesAsUnit)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -239,7 +239,7 @@ TEST(Undo_ExplicitGroup_UndoesAsUnit)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_Branching_RedoBranchSelectionDeterministic)
|
TEST (Undo_Branching_RedoBranchSelectionDeterministic)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -283,7 +283,7 @@ TEST(Undo_Branching_RedoBranchSelectionDeterministic)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_DirtyFlag_CrossesMarkSaved)
|
TEST (Undo_DirtyFlag_CrossesMarkSaved)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -312,7 +312,7 @@ TEST(Undo_DirtyFlag_CrossesMarkSaved)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST(Undo_RoundTrip_Lossless_RandomEdits)
|
TEST (Undo_RoundTrip_Lossless_RandomEdits)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
UndoSystem *u = b.Undo();
|
UndoSystem *u = b.Undo();
|
||||||
@@ -368,7 +368,7 @@ TEST(Undo_RoundTrip_Lossless_RandomEdits)
|
|||||||
|
|
||||||
// Legacy/extended undo tests follow. Keep them available for debugging,
|
// Legacy/extended undo tests follow. Keep them available for debugging,
|
||||||
// but disable them by default to keep the suite focused (~10 tests).
|
// but disable them by default to keep the suite focused (~10 tests).
|
||||||
#if 0
|
#if 1
|
||||||
|
|
||||||
|
|
||||||
TEST (Undo_Branching_RedoPreservedAfterNewEdit)
|
TEST (Undo_Branching_RedoPreservedAfterNewEdit)
|
||||||
@@ -713,6 +713,7 @@ TEST (Undo_StructuralInvariants_BranchingAndRoots)
|
|||||||
validate_undo_tree(*u);
|
validate_undo_tree(*u);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
||||||
{
|
{
|
||||||
Buffer b;
|
Buffer b;
|
||||||
@@ -796,7 +797,7 @@ TEST (Undo_BranchSelection_ThreeSiblingsAndHeadPersists)
|
|||||||
|
|
||||||
|
|
||||||
// Additional legacy tests below are useful, but kept disabled by default.
|
// Additional legacy tests below are useful, but kept disabled by default.
|
||||||
#if 0
|
#if 1
|
||||||
|
|
||||||
TEST (Undo_Branching_SwitchBetweenTwoRedoBranches_TextAndCursor)
|
TEST (Undo_Branching_SwitchBetweenTwoRedoBranches_TextAndCursor)
|
||||||
{
|
{
|
||||||
@@ -1196,4 +1197,167 @@ TEST (Undo_Command_RedoCountSelectsBranch)
|
|||||||
validate_undo_tree(*u);
|
validate_undo_tree(*u);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endif // legacy tests
|
|
||||||
|
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
|
||||||
204
themes/Leuchtturm.h
Normal file
204
themes/Leuchtturm.h
Normal file
@@ -0,0 +1,204 @@
|
|||||||
|
// themes/Leuchtturm.h — Fountain pen on cream paper, brass and leather (header-only)
|
||||||
|
// Inspired by Kaweco Brass/Bronze Sport pens on Leuchtturm1917 notebook paper.
|
||||||
|
// Light: warm cream paper with blue-black fountain pen ink.
|
||||||
|
// Dark: leather case and patinated metal.
|
||||||
|
#pragma once
|
||||||
|
#include "ThemeHelpers.h"
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
ApplyLeuchtturmLightTheme()
|
||||||
|
{
|
||||||
|
// Notebook paper and fountain pen ink
|
||||||
|
const ImVec4 paper = RGBA(0xF2ECDF); // Leuchtturm cream paper
|
||||||
|
const ImVec4 bg1 = RGBA(0xE8E2D5); // slightly darker cream
|
||||||
|
const ImVec4 bg2 = RGBA(0xDDD7CA); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0xD1CBBD); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0x040720); // blue-black fountain pen ink
|
||||||
|
const ImVec4 dim = RGBA(0x7A756A); // faded text (like printed headers)
|
||||||
|
const ImVec4 border = RGBA(0xCCC6B4); // faint ruled lines
|
||||||
|
|
||||||
|
// Metal accents from the pens
|
||||||
|
const ImVec4 brass = RGBA(0x6B5E2A); // dark patinated brass
|
||||||
|
const ImVec4 brown = RGBA(0x5C3D28); // leather/bronze
|
||||||
|
|
||||||
|
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] = 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] = brass;
|
||||||
|
|
||||||
|
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] = brass;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||||
|
|
||||||
|
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(brass.x, brass.y, brass.z, 0.18f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = brass;
|
||||||
|
colors[ImGuiCol_NavHighlight] = brass;
|
||||||
|
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] = brown;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = brass;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = brown;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = brass;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Dark variant — leather pen case with warm metal and cream accents
|
||||||
|
static inline void
|
||||||
|
ApplyLeuchtturmDarkTheme()
|
||||||
|
{
|
||||||
|
const ImVec4 bg0 = RGBA(0x1C1610); // dark leather
|
||||||
|
const ImVec4 bg1 = RGBA(0x251E16); // slightly lighter
|
||||||
|
const ImVec4 bg2 = RGBA(0x30281E); // UI elements
|
||||||
|
const ImVec4 bg3 = RGBA(0x3E3428); // hover/active
|
||||||
|
const ImVec4 ink = RGBA(0xE5DDD0); // warm cream text
|
||||||
|
const ImVec4 dim = RGBA(0x978E7C); // secondary text
|
||||||
|
const ImVec4 border = RGBA(0x4A3E30); // subtle borders
|
||||||
|
|
||||||
|
const ImVec4 brass = RGBA(0xB8A060); // polished brass
|
||||||
|
const ImVec4 brown = RGBA(0x8B6848); // bronze pen
|
||||||
|
|
||||||
|
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] = brass;
|
||||||
|
colors[ImGuiCol_SliderGrab] = brass;
|
||||||
|
colors[ImGuiCol_SliderGrabActive] = brown;
|
||||||
|
|
||||||
|
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] = brass;
|
||||||
|
|
||||||
|
colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f);
|
||||||
|
colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f);
|
||||||
|
colors[ImGuiCol_ResizeGripActive] = brass;
|
||||||
|
|
||||||
|
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(brass.x, brass.y, brass.z, 0.22f);
|
||||||
|
colors[ImGuiCol_DragDropTarget] = brass;
|
||||||
|
colors[ImGuiCol_NavHighlight] = brass;
|
||||||
|
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] = brass;
|
||||||
|
colors[ImGuiCol_PlotLinesHovered] = brown;
|
||||||
|
colors[ImGuiCol_PlotHistogram] = brass;
|
||||||
|
colors[ImGuiCol_PlotHistogramHovered] = brown;
|
||||||
|
}
|
||||||
203
themes/Tufte.h
Normal file
203
themes/Tufte.h
Normal 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;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user