Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 952e1ed3f2 | |||
| 7069943df5 | |||
| ee2c9939d7 | |||
| f5a4625652 | |||
| 37472c71ec | |||
| 5ff4b2ed3e | |||
| ab2f9918f3 | |||
| d2b53601e2 | |||
| 78b9345799 | |||
| 495183ebd2 | |||
| 998b1b9817 | |||
| dc2cf4c0a6 | |||
| f6c4a5ab34 | |||
| 35ef74910d | |||
| b17672d440 |
14
Buffer.h
14
Buffer.h
@@ -16,6 +16,11 @@
|
|||||||
#include "syntax/HighlighterEngine.h"
|
#include "syntax/HighlighterEngine.h"
|
||||||
#include "Highlight.h"
|
#include "Highlight.h"
|
||||||
|
|
||||||
|
// Forward declaration for swap journal integration
|
||||||
|
namespace kte {
|
||||||
|
class SwapRecorder;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class Buffer {
|
class Buffer {
|
||||||
public:
|
public:
|
||||||
@@ -423,6 +428,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Swap journal integration (set by Editor)
|
||||||
|
void SetSwapRecorder(kte::SwapRecorder *rec)
|
||||||
|
{
|
||||||
|
swap_rec_ = rec;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Raw, low-level editing APIs used by UndoSystem apply().
|
// Raw, low-level editing APIs used by UndoSystem apply().
|
||||||
// These must NOT trigger undo recording. They also do not move the cursor.
|
// These must NOT trigger undo recording. They also do not move the cursor.
|
||||||
void insert_text(int row, int col, std::string_view text);
|
void insert_text(int row, int col, std::string_view text);
|
||||||
@@ -465,4 +477,6 @@ private:
|
|||||||
bool syntax_enabled_ = true;
|
bool syntax_enabled_ = true;
|
||||||
std::string filetype_;
|
std::string filetype_;
|
||||||
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
std::unique_ptr<kte::HighlighterEngine> highlighter_;
|
||||||
|
// Non-owning pointer to swap recorder managed by Editor/SwapManager
|
||||||
|
kte::SwapRecorder *swap_rec_ = nullptr;
|
||||||
};
|
};
|
||||||
@@ -3,12 +3,13 @@ project(kte)
|
|||||||
|
|
||||||
include(GNUInstallDirs)
|
include(GNUInstallDirs)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 17)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(KTE_VERSION "1.3.2")
|
set(KTE_VERSION "1.4.1")
|
||||||
|
|
||||||
# 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.
|
||||||
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
set(BUILD_GUI ON CACHE BOOL "Enable building the graphical version.")
|
||||||
|
set(KTE_USE_QT OFF CACHE BOOL "Build the QT frontend instead of ImGui.")
|
||||||
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
set(BUILD_TESTS OFF CACHE BOOL "Enable building test programs.")
|
||||||
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
option(KTE_USE_PIECE_TABLE "Use PieceTable instead of GapBuffer implementation" ON)
|
||||||
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")
|
||||||
@@ -16,7 +17,7 @@ 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)
|
||||||
|
|
||||||
# Optionally enable AddressSanitizer (ASan)
|
# Optionally enable AddressSanitizer (ASan)
|
||||||
option(ENABLE_ASAN "Enable AddressSanitizer for builds" ON)
|
option(ENABLE_ASAN "Enable AddressSanitizer for builds" OFF)
|
||||||
|
|
||||||
if (ENABLE_ASAN)
|
if (ENABLE_ASAN)
|
||||||
message(STATUS "ASan enabled")
|
message(STATUS "ASan enabled")
|
||||||
@@ -32,25 +33,23 @@ else ()
|
|||||||
endif ()
|
endif ()
|
||||||
|
|
||||||
add_compile_options(
|
add_compile_options(
|
||||||
"-static"
|
|
||||||
"-Wall"
|
|
||||||
"-Wextra"
|
|
||||||
"-Werror"
|
|
||||||
"-Wno-unused-function"
|
|
||||||
"-Wno-unused-parameter"
|
|
||||||
"-g"
|
|
||||||
"$<$<CONFIG:RELEASE>:-O2>"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if (MSVC)
|
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"
|
||||||
|
"-pedantic"
|
||||||
|
"-Wno-unused-function"
|
||||||
|
"-Wno-unused-parameter"
|
||||||
|
"$<$<CONFIG:RELEASE>:-O2>"
|
||||||
"$<$<CONFIG:DEBUG>:-g>"
|
"$<$<CONFIG:DEBUG>:-g>"
|
||||||
"$<$<CONFIG:RELEASE>:-O2>")
|
)
|
||||||
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
if ("${CMAKE_CXX_COMPILER_ID}" STREQUAL "Clang")
|
||||||
add_compile_options("-stdlib=libc++")
|
add_compile_options("-stdlib=libc++")
|
||||||
else ()
|
else ()
|
||||||
@@ -103,14 +102,30 @@ set(FONT_SOURCES
|
|||||||
fonts/FontRegistry.cc
|
fonts/FontRegistry.cc
|
||||||
)
|
)
|
||||||
|
|
||||||
set(GUI_SOURCES
|
if (BUILD_GUI)
|
||||||
${FONT_SOURCES}
|
set(GUI_SOURCES
|
||||||
GUIConfig.cc
|
GUIConfig.cc
|
||||||
GUIRenderer.cc
|
)
|
||||||
GUIInputHandler.cc
|
if (KTE_USE_QT)
|
||||||
GUIFrontend.cc
|
find_package(Qt6 COMPONENTS Widgets REQUIRED)
|
||||||
)
|
set(GUI_SOURCES
|
||||||
|
${GUI_SOURCES}
|
||||||
|
QtFrontend.cc
|
||||||
|
QtInputHandler.cc
|
||||||
|
QtRenderer.cc
|
||||||
|
)
|
||||||
|
# Expose preprocessor switch so sources can exclude ImGui-specific code
|
||||||
|
add_compile_definitions(KTE_USE_QT)
|
||||||
|
else ()
|
||||||
|
set(GUI_SOURCES
|
||||||
|
${GUI_SOURCES}
|
||||||
|
${FONT_SOURCES}
|
||||||
|
ImGuiFrontend.cc
|
||||||
|
ImGuiInputHandler.cc
|
||||||
|
ImGuiRenderer.cc
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
set(COMMON_SOURCES
|
set(COMMON_SOURCES
|
||||||
GapBuffer.cc
|
GapBuffer.cc
|
||||||
@@ -120,6 +135,7 @@ set(COMMON_SOURCES
|
|||||||
Command.cc
|
Command.cc
|
||||||
HelpText.cc
|
HelpText.cc
|
||||||
KKeymap.cc
|
KKeymap.cc
|
||||||
|
Swap.cc
|
||||||
TerminalInputHandler.cc
|
TerminalInputHandler.cc
|
||||||
TerminalRenderer.cc
|
TerminalRenderer.cc
|
||||||
TerminalFrontend.cc
|
TerminalFrontend.cc
|
||||||
@@ -205,6 +221,7 @@ set(COMMON_HEADERS
|
|||||||
Command.h
|
Command.h
|
||||||
HelpText.h
|
HelpText.h
|
||||||
KKeymap.h
|
KKeymap.h
|
||||||
|
Swap.h
|
||||||
InputHandler.h
|
InputHandler.h
|
||||||
TerminalInputHandler.h
|
TerminalInputHandler.h
|
||||||
Renderer.h
|
Renderer.h
|
||||||
@@ -222,14 +239,29 @@ set(COMMON_HEADERS
|
|||||||
${SYNTAX_HEADERS}
|
${SYNTAX_HEADERS}
|
||||||
)
|
)
|
||||||
|
|
||||||
set(GUI_HEADERS
|
if (BUILD_GUI)
|
||||||
${THEME_HEADERS}
|
set(GUI_HEADERS
|
||||||
${FONT_HEADERS}
|
GUIConfig.h
|
||||||
GUIConfig.h
|
)
|
||||||
GUIRenderer.h
|
|
||||||
GUIInputHandler.h
|
if (KTE_USE_QT)
|
||||||
GUIFrontend.h
|
set(GUI_HEADERS
|
||||||
)
|
${GUI_HEADERS}
|
||||||
|
QtFrontend.h
|
||||||
|
QtInputHandler.h
|
||||||
|
QtRenderer.h
|
||||||
|
)
|
||||||
|
else ()
|
||||||
|
set(GUI_HEADERS
|
||||||
|
${GUI_HEADERS}
|
||||||
|
${THEME_HEADERS}
|
||||||
|
${FONT_HEADERS}
|
||||||
|
ImGuiFrontend.h
|
||||||
|
ImGuiInputHandler.h
|
||||||
|
ImGuiRenderer.h
|
||||||
|
)
|
||||||
|
endif ()
|
||||||
|
endif ()
|
||||||
|
|
||||||
# kte (terminal-first) executable
|
# kte (terminal-first) executable
|
||||||
add_executable(kte
|
add_executable(kte
|
||||||
@@ -319,10 +351,17 @@ if (${BUILD_GUI})
|
|||||||
|
|
||||||
)
|
)
|
||||||
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
target_compile_definitions(kge PRIVATE KTE_BUILD_GUI=1 KTE_DEFAULT_GUI=1 KTE_FONT_SIZE=${KTE_FONT_SIZE})
|
||||||
|
if (KTE_USE_QT)
|
||||||
|
target_compile_definitions(kge PRIVATE KTE_USE_QT=1)
|
||||||
|
endif ()
|
||||||
if (KTE_UNDO_DEBUG)
|
if (KTE_UNDO_DEBUG)
|
||||||
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
target_compile_definitions(kge PRIVATE KTE_UNDO_DEBUG=1)
|
||||||
endif ()
|
endif ()
|
||||||
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
if (KTE_USE_QT)
|
||||||
|
target_link_libraries(kge ${CURSES_LIBRARIES} Qt6::Widgets)
|
||||||
|
else ()
|
||||||
|
target_link_libraries(kge ${CURSES_LIBRARIES} imgui)
|
||||||
|
endif ()
|
||||||
|
|
||||||
# On macOS, build kge as a proper .app bundle
|
# On macOS, build kge as a proper .app bundle
|
||||||
if (APPLE)
|
if (APPLE)
|
||||||
|
|||||||
714
Command.cc
714
Command.cc
@@ -18,11 +18,31 @@
|
|||||||
#include "syntax/HighlighterEngine.h"
|
#include "syntax/HighlighterEngine.h"
|
||||||
#include "syntax/CppHighlighter.h"
|
#include "syntax/CppHighlighter.h"
|
||||||
#ifdef KTE_BUILD_GUI
|
#ifdef KTE_BUILD_GUI
|
||||||
#include "GUITheme.h"
|
# include "GUITheme.h"
|
||||||
#include "fonts/FontRegistry.h"
|
# if !defined(KTE_USE_QT)
|
||||||
#include "imgui.h"
|
# include "fonts/FontRegistry.h"
|
||||||
|
# include "imgui.h"
|
||||||
|
# endif
|
||||||
|
# if defined(KTE_USE_QT)
|
||||||
|
# include <QFontDatabase>
|
||||||
|
# include <QStringList>
|
||||||
|
# endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
// Define cross-frontend theme change flags declared in GUITheme.h
|
||||||
|
namespace kte {
|
||||||
|
bool gThemeChangePending = false;
|
||||||
|
std::string gThemeChangeRequest;
|
||||||
|
// Qt font change globals
|
||||||
|
bool gFontChangePending = false;
|
||||||
|
std::string gFontFamilyRequest;
|
||||||
|
float gFontSizeRequest = 0.0f;
|
||||||
|
std::string gCurrentFontFamily;
|
||||||
|
float gCurrentFontSize = 0.0f;
|
||||||
|
// Request Qt visual font dialog
|
||||||
|
bool gFontDialogRequested = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Keep buffer viewport offsets so that the cursor stays within the visible
|
// Keep buffer viewport offsets so that the cursor stays within the visible
|
||||||
// window based on the editor's current dimensions. The bottom row is reserved
|
// window based on the editor's current dimensions. The bottom row is reserved
|
||||||
@@ -89,6 +109,33 @@ ensure_cursor_visible(const Editor &ed, Buffer &buf)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_center_on_cursor(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
Buffer *buf = ctx.editor.CurrentBuffer();
|
||||||
|
if (!buf)
|
||||||
|
return false;
|
||||||
|
const auto &rows = buf->Rows();
|
||||||
|
std::size_t total = rows.size();
|
||||||
|
std::size_t content = ctx.editor.ContentRows();
|
||||||
|
if (content == 0)
|
||||||
|
content = 1;
|
||||||
|
std::size_t cy = buf->Cury();
|
||||||
|
std::size_t half = content / 2;
|
||||||
|
std::size_t new_rowoffs = (cy > half) ? (cy - half) : 0;
|
||||||
|
// Clamp to valid range
|
||||||
|
if (total > content) {
|
||||||
|
std::size_t max_rowoffs = total - content;
|
||||||
|
if (new_rowoffs > max_rowoffs)
|
||||||
|
new_rowoffs = max_rowoffs;
|
||||||
|
} else {
|
||||||
|
new_rowoffs = 0;
|
||||||
|
}
|
||||||
|
buf->SetOffsets(new_rowoffs, buf->Coloffs());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static void
|
static void
|
||||||
ensure_at_least_one_line(Buffer &buf)
|
ensure_at_least_one_line(Buffer &buf)
|
||||||
{
|
{
|
||||||
@@ -697,6 +744,10 @@ cmd_refresh(CommandContext &ctx)
|
|||||||
ctx.editor.ClearSearchOrigin();
|
ctx.editor.ClearSearchOrigin();
|
||||||
ctx.editor.SetSearchIndex(-1);
|
ctx.editor.SetSearchIndex(-1);
|
||||||
}
|
}
|
||||||
|
// Clear any pending close/overwrite state associated with prompts
|
||||||
|
ctx.editor.SetCloseConfirmPending(false);
|
||||||
|
ctx.editor.SetCloseAfterSave(false);
|
||||||
|
ctx.editor.ClearPendingOverwritePath();
|
||||||
ctx.editor.CancelPrompt();
|
ctx.editor.CancelPrompt();
|
||||||
ctx.editor.SetStatus("Canceled");
|
ctx.editor.SetStatus("Canceled");
|
||||||
return true;
|
return true;
|
||||||
@@ -765,6 +816,15 @@ cmd_unknown_kcommand(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static bool
|
||||||
|
cmd_unknown_esc_command(CommandContext &ctx)
|
||||||
|
{
|
||||||
|
(void) ctx;
|
||||||
|
ctx.editor.SetStatus("invalid escape command");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- Syntax highlighting commands ---
|
// --- Syntax highlighting commands ---
|
||||||
static void
|
static void
|
||||||
apply_filetype(Buffer &buf, const std::string &ft)
|
apply_filetype(Buffer &buf, const std::string &ft)
|
||||||
@@ -901,8 +961,8 @@ cmd_set_option(CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// GUI theme cycling commands (available in GUI build; show message otherwise)
|
// GUI theme cycling commands (available in GUI build; ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_theme_next(CommandContext &ctx)
|
cmd_theme_next(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -938,7 +998,7 @@ cmd_theme_prev(CommandContext &ctx)
|
|||||||
|
|
||||||
|
|
||||||
// Theme set by name command
|
// Theme set by name command
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_theme_set_by_name(const CommandContext &ctx)
|
cmd_theme_set_by_name(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -957,7 +1017,9 @@ cmd_theme_set_by_name(const CommandContext &ctx)
|
|||||||
ltrim(name);
|
ltrim(name);
|
||||||
rtrim(name);
|
rtrim(name);
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
ctx.editor.SetStatus("theme: missing name");
|
// Show current theme when no argument provided
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Current theme: ") + kte::CurrentThemeName());
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (kte::ApplyThemeByName(name)) {
|
if (kte::ApplyThemeByName(name)) {
|
||||||
@@ -980,15 +1042,41 @@ 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)
|
||||||
|
// Qt GUI build: schedule theme change for frontend
|
||||||
|
std::string name = ctx.arg;
|
||||||
|
// trim spaces
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(name);
|
||||||
|
rtrim(name);
|
||||||
|
if (name.empty()) {
|
||||||
|
ctx.editor.SetStatus("theme: provide a name (e.g., nord, solarized-dark, gruvbox-light, eink)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
kte::gThemeChangeRequest = name;
|
||||||
|
kte::gThemeChangePending = true;
|
||||||
|
ctx.editor.SetStatus(std::string("Theme requested: ") + name);
|
||||||
|
return true;
|
||||||
|
# else
|
||||||
(void) ctx;
|
(void) ctx;
|
||||||
// No-op in terminal build
|
// No-op in terminal build
|
||||||
return true;
|
return true;
|
||||||
|
# endif
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Font set by name (GUI)
|
// Font set by name (GUI)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_font_set_by_name(const CommandContext &ctx)
|
cmd_font_set_by_name(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1011,7 +1099,12 @@ cmd_font_set_by_name(const CommandContext &ctx)
|
|||||||
return (char) std::tolower(c);
|
return (char) std::tolower(c);
|
||||||
});
|
});
|
||||||
if (name.empty()) {
|
if (name.empty()) {
|
||||||
ctx.editor.SetStatus("font: missing name");
|
// Show current font when no argument provided
|
||||||
|
auto ® = FontRegistry::Instance();
|
||||||
|
std::string current_font = reg.CurrentFontName();
|
||||||
|
if (current_font.empty())
|
||||||
|
current_font = "default";
|
||||||
|
ctx.editor.SetStatus(std::string("Current font: ") + current_font);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1036,14 +1129,38 @@ cmd_font_set_by_name(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_font_set_by_name(CommandContext &ctx)
|
cmd_font_set_by_name(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
(void) ctx;
|
// Qt build: queue font family change
|
||||||
|
std::string name = ctx.arg;
|
||||||
|
// trim
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(name);
|
||||||
|
rtrim(name);
|
||||||
|
if (name.empty()) {
|
||||||
|
// Show current font when no argument provided
|
||||||
|
std::string cur = kte::gCurrentFontFamily.empty() ? std::string("default") : kte::gCurrentFontFamily;
|
||||||
|
ctx.editor.SetStatus(std::string("Current font: ") + cur);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
kte::gFontFamilyRequest = name;
|
||||||
|
// Keep size if not specified by user; signal change
|
||||||
|
kte::gFontChangePending = true;
|
||||||
|
ctx.editor.SetStatus(std::string("Font requested: ") + name);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Font size set (GUI)
|
// Font size set (GUI, ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_font_set_size(const CommandContext &ctx)
|
cmd_font_set_size(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1062,7 +1179,17 @@ cmd_font_set_size(const CommandContext &ctx)
|
|||||||
ltrim(a);
|
ltrim(a);
|
||||||
rtrim(a);
|
rtrim(a);
|
||||||
if (a.empty()) {
|
if (a.empty()) {
|
||||||
ctx.editor.SetStatus("font-size: missing value");
|
// Show current font size when no argument provided
|
||||||
|
auto ® = FontRegistry::Instance();
|
||||||
|
float current_size = reg.CurrentFontSize();
|
||||||
|
if (current_size <= 0.0f) {
|
||||||
|
// Fallback to current ImGui font size if available
|
||||||
|
current_size = ImGui::GetFontSize();
|
||||||
|
if (current_size <= 0.0f)
|
||||||
|
current_size = 16.0f;
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Current font size: ") + std::to_string((int) std::round(current_size)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
char *endp = nullptr;
|
char *endp = nullptr;
|
||||||
@@ -1092,14 +1219,45 @@ cmd_font_set_size(const CommandContext &ctx)
|
|||||||
static bool
|
static bool
|
||||||
cmd_font_set_size(CommandContext &ctx)
|
cmd_font_set_size(CommandContext &ctx)
|
||||||
{
|
{
|
||||||
(void) ctx;
|
// Qt build: parse size and queue change
|
||||||
|
std::string a = ctx.arg;
|
||||||
|
auto ltrim = [](std::string &s) {
|
||||||
|
s.erase(s.begin(), std::find_if(s.begin(), s.end(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
auto rtrim = [](std::string &s) {
|
||||||
|
s.erase(std::find_if(s.rbegin(), s.rend(), [](unsigned char ch) {
|
||||||
|
return !std::isspace(ch);
|
||||||
|
}).base(), s.end());
|
||||||
|
};
|
||||||
|
ltrim(a);
|
||||||
|
rtrim(a);
|
||||||
|
if (a.empty()) {
|
||||||
|
float cur = (kte::gCurrentFontSize > 0.0f) ? kte::gCurrentFontSize : 18.0f;
|
||||||
|
ctx.editor.SetStatus(std::string("Current font size: ") + std::to_string((int) std::round(cur)));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
char *endp = nullptr;
|
||||||
|
float size = strtof(a.c_str(), &endp);
|
||||||
|
if (endp == a.c_str() || !std::isfinite(size)) {
|
||||||
|
ctx.editor.SetStatus("font-size: expected number");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (size < 6.0f)
|
||||||
|
size = 6.0f;
|
||||||
|
if (size > 96.0f)
|
||||||
|
size = 96.0f;
|
||||||
|
kte::gFontSizeRequest = size;
|
||||||
|
kte::gFontChangePending = true;
|
||||||
|
ctx.editor.SetStatus(std::string("Font size requested: ") + std::to_string((int) std::round(size)));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
// Background set command (GUI)
|
// Background set command (GUI, ImGui-only for now)
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI) && !defined(KTE_USE_QT)
|
||||||
static bool
|
static bool
|
||||||
cmd_background_set(const CommandContext &ctx)
|
cmd_background_set(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1268,6 +1426,20 @@ cmd_visual_file_picker_toggle(const CommandContext &ctx)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// GUI: request visual font picker (Qt frontend will consume flag)
|
||||||
|
static bool
|
||||||
|
cmd_visual_font_picker_toggle(const CommandContext &ctx)
|
||||||
|
{
|
||||||
|
#ifdef KTE_BUILD_GUI
|
||||||
|
kte::gFontDialogRequested = true;
|
||||||
|
ctx.editor.SetStatus("Font chooser");
|
||||||
|
#else
|
||||||
|
ctx.editor.SetStatus("Font chooser not available in terminal");
|
||||||
|
#endif
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
cmd_jump_to_line_start(const CommandContext &ctx)
|
cmd_jump_to_line_start(const CommandContext &ctx)
|
||||||
{
|
{
|
||||||
@@ -1357,6 +1529,14 @@ cmd_buffer_close(const CommandContext &ctx)
|
|||||||
std::size_t idx = ctx.editor.CurrentBufferIndex();
|
std::size_t idx = ctx.editor.CurrentBufferIndex();
|
||||||
Buffer *b = ctx.editor.CurrentBuffer();
|
Buffer *b = ctx.editor.CurrentBuffer();
|
||||||
std::string name = b ? buffer_display_name(*b) : std::string("");
|
std::string name = b ? buffer_display_name(*b) : std::string("");
|
||||||
|
// If buffer is dirty, prompt to save first (for both named and unnamed buffers)
|
||||||
|
if (b && b->Dirty()) {
|
||||||
|
ctx.editor.StartPrompt(Editor::PromptKind::Confirm, "Save", "");
|
||||||
|
ctx.editor.SetCloseConfirmPending(true);
|
||||||
|
ctx.editor.SetStatus(std::string("Save changes to ") + name + "? (y/N)");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise close immediately
|
||||||
if (b && b->Undo())
|
if (b && b->Undo())
|
||||||
b->Undo()->discard_pending();
|
b->Undo()->discard_pending();
|
||||||
ctx.editor.CloseBuffer(idx);
|
ctx.editor.CloseBuffer(idx);
|
||||||
@@ -1561,7 +1741,8 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
std::string argprefix = text.substr(sp + 1);
|
std::string argprefix = text.substr(sp + 1);
|
||||||
// Only special-case argument completion for certain commands
|
// Only special-case argument completion for certain commands
|
||||||
if (cmd == "theme") {
|
if (cmd == "theme") {
|
||||||
#ifdef KTE_BUILD_GUI
|
#if defined(KTE_BUILD_GUI)
|
||||||
|
# if !defined(KTE_USE_QT)
|
||||||
std::vector<std::string> cands;
|
std::vector<std::string> cands;
|
||||||
const auto ® = kte::ThemeRegistry();
|
const auto ® = kte::ThemeRegistry();
|
||||||
for (const auto &t: reg) {
|
for (const auto &t: reg) {
|
||||||
@@ -1569,6 +1750,67 @@ cmd_insert_text(CommandContext &ctx)
|
|||||||
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
||||||
cands.push_back(n);
|
cands.push_back(n);
|
||||||
}
|
}
|
||||||
|
# else
|
||||||
|
// Qt: offer known theme names handled by ApplyQtThemeByName
|
||||||
|
static const char *qt_themes[] = {
|
||||||
|
"nord",
|
||||||
|
"solarized-dark",
|
||||||
|
"solarized-light",
|
||||||
|
"gruvbox-dark",
|
||||||
|
"gruvbox-light",
|
||||||
|
"eink"
|
||||||
|
};
|
||||||
|
std::vector<std::string> cands;
|
||||||
|
for (const char *t: qt_themes) {
|
||||||
|
std::string n(t);
|
||||||
|
if (argprefix.empty() || n.rfind(argprefix, 0) == 0)
|
||||||
|
cands.push_back(n);
|
||||||
|
}
|
||||||
|
# endif
|
||||||
|
if (cands.empty()) {
|
||||||
|
// no change
|
||||||
|
} else if (cands.size() == 1) {
|
||||||
|
ctx.editor.SetPromptText(cmd + std::string(" ") + cands[0]);
|
||||||
|
} else {
|
||||||
|
std::string lcp = cands[0];
|
||||||
|
for (size_t i = 1; i < cands.size(); ++i) {
|
||||||
|
const std::string &s = cands[i];
|
||||||
|
size_t j = 0;
|
||||||
|
while (j < lcp.size() && j < s.size() && lcp[j] == s[j])
|
||||||
|
++j;
|
||||||
|
lcp.resize(j);
|
||||||
|
if (lcp.empty())
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (!lcp.empty() && lcp != argprefix)
|
||||||
|
ctx.editor.SetPromptText(cmd + std::string(" ") + lcp);
|
||||||
|
}
|
||||||
|
ctx.editor.SetStatus(std::string(": ") + ctx.editor.PromptText());
|
||||||
|
return true;
|
||||||
|
#else
|
||||||
|
(void) argprefix; // no completion in non-GUI build
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
QStringList fams = QFontDatabase::families();
|
||||||
|
std::string apfx_lower = argprefix;
|
||||||
|
std::transform(apfx_lower.begin(), apfx_lower.end(), apfx_lower.begin(),
|
||||||
|
[](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
for (const auto &fam: fams) {
|
||||||
|
std::string n = fam.toStdString();
|
||||||
|
std::string nlower = n;
|
||||||
|
std::transform(nlower.begin(), nlower.end(), nlower.begin(),
|
||||||
|
[](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (apfx_lower.empty() || nlower.rfind(apfx_lower, 0) == 0)
|
||||||
|
cands.push_back(n);
|
||||||
|
}
|
||||||
if (cands.empty()) {
|
if (cands.empty()) {
|
||||||
// no change
|
// no change
|
||||||
} else if (cands.size() == 1) {
|
} else if (cands.size() == 1) {
|
||||||
@@ -1590,7 +1832,7 @@ 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
|
#else
|
||||||
(void) argprefix; // no completion in non-GUI build
|
(void) argprefix;
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
// default: no special arg completion
|
// default: no special arg completion
|
||||||
@@ -2174,6 +2416,28 @@ cmd_newline(CommandContext &ctx)
|
|||||||
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();
|
||||||
|
// If a close-after-save was requested (from closing a dirty, unnamed buffer),
|
||||||
|
// close the buffer now.
|
||||||
|
if (ctx.editor.CloseAfterSave()) {
|
||||||
|
ctx.editor.SetCloseAfterSave(false);
|
||||||
|
std::size_t idx_close = ctx.editor.CurrentBufferIndex();
|
||||||
|
std::string name_close = buffer_display_name(*buf);
|
||||||
|
if (buf->Undo())
|
||||||
|
buf->Undo()->discard_pending();
|
||||||
|
ctx.editor.CloseBuffer(idx_close);
|
||||||
|
if (ctx.editor.BufferCount() == 0) {
|
||||||
|
Buffer empty;
|
||||||
|
ctx.editor.AddBuffer(std::move(empty));
|
||||||
|
ctx.editor.SwitchTo(0);
|
||||||
|
}
|
||||||
|
const Buffer *cur = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Closed: ") + name_close +
|
||||||
|
std::string(" Now: ")
|
||||||
|
+ (cur
|
||||||
|
? buffer_display_name(*cur)
|
||||||
|
: std::string("")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2197,11 +2461,86 @@ cmd_newline(CommandContext &ctx)
|
|||||||
ctx.editor.SetStatus("Saved as " + target);
|
ctx.editor.SetStatus("Saved as " + target);
|
||||||
if (auto *u = buf->Undo())
|
if (auto *u = buf->Undo())
|
||||||
u->mark_saved();
|
u->mark_saved();
|
||||||
|
// If this overwrite confirm was part of a close-after-save flow, close now.
|
||||||
|
if (ctx.editor.CloseAfterSave()) {
|
||||||
|
ctx.editor.SetCloseAfterSave(false);
|
||||||
|
std::size_t idx_close = ctx.editor.CurrentBufferIndex();
|
||||||
|
std::string name_close = buffer_display_name(*buf);
|
||||||
|
if (buf->Undo())
|
||||||
|
buf->Undo()->discard_pending();
|
||||||
|
ctx.editor.CloseBuffer(idx_close);
|
||||||
|
if (ctx.editor.BufferCount() == 0) {
|
||||||
|
Buffer empty;
|
||||||
|
ctx.editor.AddBuffer(std::move(empty));
|
||||||
|
ctx.editor.SwitchTo(0);
|
||||||
|
}
|
||||||
|
const Buffer *cur = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Closed: ") + name_close + std::string(
|
||||||
|
" Now: ")
|
||||||
|
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus("Save canceled");
|
ctx.editor.SetStatus("Save canceled");
|
||||||
}
|
}
|
||||||
ctx.editor.ClearPendingOverwritePath();
|
ctx.editor.ClearPendingOverwritePath();
|
||||||
|
// Regardless of answer, end any close-after-save pending state for safety.
|
||||||
|
ctx.editor.SetCloseAfterSave(false);
|
||||||
|
} else if (ctx.editor.CloseConfirmPending() && buf) {
|
||||||
|
bool yes = false;
|
||||||
|
if (!value.empty()) {
|
||||||
|
char c = value[0];
|
||||||
|
yes = (c == 'y' || c == 'Y');
|
||||||
|
}
|
||||||
|
// Prepare close details
|
||||||
|
std::size_t idx_close = ctx.editor.CurrentBufferIndex();
|
||||||
|
std::string name_close = buffer_display_name(*buf);
|
||||||
|
bool proceed_to_close = true;
|
||||||
|
if (yes) {
|
||||||
|
std::string err;
|
||||||
|
if (buf->IsFileBacked()) {
|
||||||
|
if (!buf->Save(err)) {
|
||||||
|
ctx.editor.SetStatus(err);
|
||||||
|
proceed_to_close = false;
|
||||||
|
} else {
|
||||||
|
buf->SetDirty(false);
|
||||||
|
if (auto *u = buf->Undo())
|
||||||
|
u->mark_saved();
|
||||||
|
}
|
||||||
|
} else if (!buf->Filename().empty()) {
|
||||||
|
if (!buf->SaveAs(buf->Filename(), err)) {
|
||||||
|
ctx.editor.SetStatus(err);
|
||||||
|
proceed_to_close = false;
|
||||||
|
} else {
|
||||||
|
buf->SetDirty(false);
|
||||||
|
if (auto *u = buf->Undo())
|
||||||
|
u->mark_saved();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No filename; fall back to Save As flow and set close-after-save
|
||||||
|
ctx.editor.StartPrompt(Editor::PromptKind::SaveAs, "Save as", "");
|
||||||
|
ctx.editor.SetCloseAfterSave(true);
|
||||||
|
ctx.editor.SetStatus("Save as: ");
|
||||||
|
ctx.editor.SetCloseConfirmPending(false);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (proceed_to_close) {
|
||||||
|
if (buf->Undo())
|
||||||
|
buf->Undo()->discard_pending();
|
||||||
|
ctx.editor.CloseBuffer(idx_close);
|
||||||
|
if (ctx.editor.BufferCount() == 0) {
|
||||||
|
Buffer empty;
|
||||||
|
ctx.editor.AddBuffer(std::move(empty));
|
||||||
|
ctx.editor.SwitchTo(0);
|
||||||
|
}
|
||||||
|
const Buffer *cur = ctx.editor.CurrentBuffer();
|
||||||
|
ctx.editor.SetStatus(
|
||||||
|
std::string("Closed: ") + name_close + std::string(" Now: ")
|
||||||
|
+ (cur ? buffer_display_name(*cur) : std::string("")));
|
||||||
|
}
|
||||||
|
ctx.editor.SetCloseConfirmPending(false);
|
||||||
} else {
|
} else {
|
||||||
ctx.editor.SetStatus("Nothing to confirm");
|
ctx.editor.SetStatus("Nothing to confirm");
|
||||||
}
|
}
|
||||||
@@ -3635,9 +3974,11 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
if (!buf)
|
if (!buf)
|
||||||
return false;
|
return false;
|
||||||
ensure_at_least_one_line(*buf);
|
ensure_at_least_one_line(*buf);
|
||||||
auto &rows = buf->Rows();
|
auto &rows = buf->Rows();
|
||||||
std::size_t y = buf->Cury();
|
std::size_t y = buf->Cury();
|
||||||
int width = ctx.count > 0 ? ctx.count : 72;
|
// Treat a universal-argument count of 1 as "no width specified".
|
||||||
|
// Editor::UArgGet() returns 1 when no explicit count was provided.
|
||||||
|
int width = ctx.count > 1 ? ctx.count : 72;
|
||||||
std::size_t para_start = y;
|
std::size_t para_start = y;
|
||||||
while (para_start > 0 && !rows[para_start - 1].empty())
|
while (para_start > 0 && !rows[para_start - 1].empty())
|
||||||
--para_start;
|
--para_start;
|
||||||
@@ -3646,50 +3987,222 @@ cmd_reflow_paragraph(CommandContext &ctx)
|
|||||||
++para_end;
|
++para_end;
|
||||||
if (para_start > para_end)
|
if (para_start > para_end)
|
||||||
return false;
|
return false;
|
||||||
std::string text;
|
|
||||||
for (std::size_t i = para_start; i <= para_end; ++i) {
|
auto is_space = [](char c) {
|
||||||
if (i > para_start && !text.empty() && text.back() != ' ')
|
return c == ' ' || c == '\t';
|
||||||
text += ' ';
|
};
|
||||||
const auto &line = rows[i];
|
|
||||||
for (std::size_t j = 0; j < line.size(); ++j) {
|
auto leading_ws = [&](const std::string &s) {
|
||||||
char c = line[j];
|
std::size_t i = 0;
|
||||||
if (c == '\t')
|
while (i < s.size() && is_space(s[i]))
|
||||||
text += ' ';
|
++i;
|
||||||
else
|
return s.substr(0, i);
|
||||||
text += c;
|
};
|
||||||
|
|
||||||
|
auto starts_with = [](const std::string &s, const std::string &pfx) {
|
||||||
|
return s.size() >= pfx.size() && std::equal(pfx.begin(), pfx.end(), s.begin());
|
||||||
|
};
|
||||||
|
|
||||||
|
auto is_bullet_line = [&](const std::string &s, std::string &indent_out, char &marker_out,
|
||||||
|
std::size_t &after_prefix_idx) -> bool {
|
||||||
|
indent_out = leading_ws(s);
|
||||||
|
std::size_t i = indent_out.size();
|
||||||
|
if (i + 1 < s.size()) {
|
||||||
|
char m = s[i];
|
||||||
|
if ((m == '-' || m == '+' || m == '*') && s[i + 1] == ' ') {
|
||||||
|
marker_out = m;
|
||||||
|
after_prefix_idx = i + 2; // after marker + space
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
auto normalize_spaces = [](const std::string &in) {
|
||||||
|
std::string out;
|
||||||
|
out.reserve(in.size());
|
||||||
|
bool in_space = false;
|
||||||
|
for (char c: in) {
|
||||||
|
char cc = (c == '\t') ? ' ' : c;
|
||||||
|
if (cc == ' ') {
|
||||||
|
if (!in_space) {
|
||||||
|
out.push_back(' ');
|
||||||
|
in_space = true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
out.push_back(cc);
|
||||||
|
in_space = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// trim leading/trailing spaces
|
||||||
|
// leading
|
||||||
|
std::size_t start = 0;
|
||||||
|
while (start < out.size() && out[start] == ' ')
|
||||||
|
++start;
|
||||||
|
// trailing
|
||||||
|
std::size_t end = out.size();
|
||||||
|
while (end > start && out[end - 1] == ' ')
|
||||||
|
--end;
|
||||||
|
return out.substr(start, end - start);
|
||||||
|
};
|
||||||
|
|
||||||
|
auto wrap_with_prefixes = [&](const std::string &content,
|
||||||
|
const std::string &first_prefix,
|
||||||
|
const std::string &cont_prefix,
|
||||||
|
int w,
|
||||||
|
std::vector<std::string> &dst) {
|
||||||
|
// Tokenize by spaces
|
||||||
|
std::vector<std::string> words;
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while (pos < content.size()) {
|
||||||
|
while (pos < content.size() && content[pos] == ' ')
|
||||||
|
++pos;
|
||||||
|
if (pos >= content.size())
|
||||||
|
break;
|
||||||
|
std::size_t ws = pos;
|
||||||
|
while (pos < content.size() && content[pos] != ' ')
|
||||||
|
++pos;
|
||||||
|
words.emplace_back(content.substr(ws, pos - ws));
|
||||||
|
}
|
||||||
|
std::string line = first_prefix;
|
||||||
|
std::size_t cur_len = line.size();
|
||||||
|
bool first_word_on_line = true;
|
||||||
|
auto flush_line = [&]() {
|
||||||
|
dst.emplace_back(line);
|
||||||
|
line = cont_prefix;
|
||||||
|
cur_len = line.size();
|
||||||
|
first_word_on_line = true;
|
||||||
|
};
|
||||||
|
if (words.empty()) {
|
||||||
|
// Still emit a line with just the prefix (e.g., empty bullet)
|
||||||
|
dst.emplace_back(line);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (std::size_t i = 0; i < words.size(); ++i) {
|
||||||
|
const std::string &wrd = words[i];
|
||||||
|
std::size_t needed = wrd.size() + (first_word_on_line ? 0 : 1);
|
||||||
|
if (static_cast<int>(cur_len + needed) > w) {
|
||||||
|
// wrap
|
||||||
|
flush_line();
|
||||||
|
}
|
||||||
|
if (!first_word_on_line) {
|
||||||
|
line.push_back(' ');
|
||||||
|
++cur_len;
|
||||||
|
}
|
||||||
|
line += wrd;
|
||||||
|
cur_len += wrd.size();
|
||||||
|
first_word_on_line = false;
|
||||||
|
}
|
||||||
|
if (!line.empty())
|
||||||
|
dst.emplace_back(line);
|
||||||
|
};
|
||||||
|
|
||||||
std::vector<std::string> new_lines;
|
std::vector<std::string> new_lines;
|
||||||
std::string line;
|
|
||||||
std::size_t pos = 0;
|
// Determine if this region looks like a list: any line starting with bullet
|
||||||
while (pos < text.size()) {
|
bool region_has_bullet = false;
|
||||||
while (pos < text.size() && text[pos] == ' ')
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
++pos;
|
std::string s = static_cast<std::string>(rows[i]);
|
||||||
if (pos >= text.size())
|
std::string indent;
|
||||||
|
char marker;
|
||||||
|
std::size_t idx;
|
||||||
|
if (is_bullet_line(s, indent, marker, idx)) {
|
||||||
|
region_has_bullet = true;
|
||||||
break;
|
break;
|
||||||
std::size_t word_start = pos;
|
|
||||||
while (pos < text.size() && text[pos] != ' ')
|
|
||||||
++pos;
|
|
||||||
std::string word = text.substr(word_start, pos - word_start);
|
|
||||||
if (line.empty()) {
|
|
||||||
line = word;
|
|
||||||
} else if (static_cast<int>(line.size() + 1 + word.size()) <= width) {
|
|
||||||
line += ' ';
|
|
||||||
line += word;
|
|
||||||
} else {
|
|
||||||
new_lines.push_back(line);
|
|
||||||
line = word;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!line.empty())
|
|
||||||
new_lines.push_back(line);
|
if (region_has_bullet) {
|
||||||
|
// Parse as list items; support hanging indent continuations
|
||||||
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
|
std::string s = static_cast<std::string>(rows[i]);
|
||||||
|
std::string indent;
|
||||||
|
char marker = 0;
|
||||||
|
std::size_t after_idx = 0;
|
||||||
|
if (is_bullet_line(s, indent, marker, after_idx)) {
|
||||||
|
std::string first_prefix = indent + std::string(1, marker) + " ";
|
||||||
|
std::string cont_prefix = indent + " ";
|
||||||
|
std::string content = s.substr(after_idx);
|
||||||
|
// consume continuation lines that are part of this bullet item
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
while (j <= para_end) {
|
||||||
|
std::string ns = static_cast<std::string>(rows[j]);
|
||||||
|
if (starts_with(ns, indent + " ")) {
|
||||||
|
content += ' ';
|
||||||
|
content += ns.substr(indent.size() + 2);
|
||||||
|
++j;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
// stop if next bullet at same indentation or different structure
|
||||||
|
std::string nindent;
|
||||||
|
char nmarker;
|
||||||
|
std::size_t nidx;
|
||||||
|
if (is_bullet_line(ns, nindent, nmarker, nidx)) {
|
||||||
|
break; // next item
|
||||||
|
}
|
||||||
|
// Not a continuation and not a bullet: stop (treat as separate paragraph chunk)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
content = normalize_spaces(content);
|
||||||
|
wrap_with_prefixes(content, first_prefix, cont_prefix, width, new_lines);
|
||||||
|
i = j - 1; // advance
|
||||||
|
} else {
|
||||||
|
// A non-bullet line within a list region; treat as its own wrapped paragraph preserving its indent
|
||||||
|
std::string base_indent = leading_ws(s);
|
||||||
|
std::string content = s.substr(base_indent.size());
|
||||||
|
std::size_t j = i + 1;
|
||||||
|
while (j <= para_end) {
|
||||||
|
std::string ns = static_cast<std::string>(rows[j]);
|
||||||
|
std::string nindent = leading_ws(ns);
|
||||||
|
std::string tmp_indent;
|
||||||
|
char tmp_marker;
|
||||||
|
std::size_t tmp_idx;
|
||||||
|
if (is_bullet_line(ns, tmp_indent, tmp_marker, tmp_idx)) {
|
||||||
|
break; // next bullet starts
|
||||||
|
}
|
||||||
|
if (nindent.size() >= base_indent.size()) {
|
||||||
|
content += ' ';
|
||||||
|
content += ns.substr(base_indent.size());
|
||||||
|
++j;
|
||||||
|
} else {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
content = normalize_spaces(content);
|
||||||
|
wrap_with_prefixes(content, base_indent, base_indent, width, new_lines);
|
||||||
|
i = j - 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Normal paragraph: preserve indentation of first line
|
||||||
|
std::string s0 = static_cast<std::string>(rows[para_start]);
|
||||||
|
std::string pfx = leading_ws(s0);
|
||||||
|
std::string content;
|
||||||
|
for (std::size_t i = para_start; i <= para_end; ++i) {
|
||||||
|
std::string si = static_cast<std::string>(rows[i]);
|
||||||
|
// strip the same prefix length if present
|
||||||
|
if (si.size() >= pfx.size() && starts_with(si, pfx))
|
||||||
|
si.erase(0, pfx.size());
|
||||||
|
if (!content.empty())
|
||||||
|
content.push_back(' ');
|
||||||
|
content += si;
|
||||||
|
}
|
||||||
|
content = normalize_spaces(content);
|
||||||
|
wrap_with_prefixes(content, pfx, pfx, width, new_lines);
|
||||||
|
}
|
||||||
|
|
||||||
if (new_lines.empty())
|
if (new_lines.empty())
|
||||||
new_lines.push_back("");
|
new_lines.push_back("");
|
||||||
|
|
||||||
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
rows.erase(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
||||||
rows.begin() + static_cast<std::ptrdiff_t>(para_end + 1));
|
rows.begin() + static_cast<std::ptrdiff_t>(para_end + 1));
|
||||||
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
rows.insert(rows.begin() + static_cast<std::ptrdiff_t>(para_start),
|
||||||
new_lines.begin(), new_lines.end());
|
new_lines.begin(), new_lines.end());
|
||||||
buf->SetCursor(0, para_start);
|
|
||||||
|
// Place cursor at the end of the paragraph
|
||||||
|
std::size_t new_last_y = para_start + (new_lines.empty() ? 0 : new_lines.size() - 1);
|
||||||
|
std::size_t new_last_x = new_lines.empty() ? 0 : new_lines.back().size();
|
||||||
|
buf->SetCursor(new_last_x, new_last_y);
|
||||||
buf->SetDirty(true);
|
buf->SetDirty(true);
|
||||||
ensure_cursor_visible(ctx.editor, *buf);
|
ensure_cursor_visible(ctx.editor, *buf);
|
||||||
return true;
|
return true;
|
||||||
@@ -3798,35 +4311,45 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
CommandRegistry::Register({CommandId::SaveAndQuit, "save-quit", "Save and quit (request)", cmd_save_and_quit});
|
||||||
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
CommandRegistry::Register({CommandId::Refresh, "refresh", "Force redraw", cmd_refresh});
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix});
|
{CommandId::KPrefix, "k-prefix", "Entering k-command prefix (show hint)", cmd_kprefix, false, false});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)",
|
CommandId::UnknownKCommand, "unknown-k", "Unknown k-command (status)",
|
||||||
cmd_unknown_kcommand
|
cmd_unknown_kcommand, false, false
|
||||||
});
|
|
||||||
CommandRegistry::Register({CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start});
|
|
||||||
CommandRegistry::Register({
|
|
||||||
CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start
|
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::RegexpReplace, "regex-replace", "Begin regex search & replace", cmd_regex_replace_start
|
CommandId::UnknownEscCommand, "unknown-esc", "Unknown ESC command (status)",
|
||||||
|
cmd_unknown_esc_command, false, false
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start
|
CommandId::FindStart, "find-start", "Begin incremental search", cmd_find_start, false, false
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start
|
CommandId::RegexFindStart, "regex-find-start", "Begin regex search", cmd_regex_find_start, false, false
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::RegexpReplace, "regex-replace", "Begin regex search & replace", cmd_regex_replace_start,
|
||||||
|
false, false
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::SearchReplace, "search-replace", "Begin search & replace", cmd_search_replace_start, false,
|
||||||
|
false
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::OpenFileStart, "open-file-start", "Begin open-file prompt", cmd_open_file_start, false, false
|
||||||
});
|
});
|
||||||
// Buffers
|
// Buffers
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::BufferSwitchStart, "buffer-switch-start", "Begin buffer switch prompt",
|
CommandId::BufferSwitchStart, "buffer-switch-start", "Begin buffer switch prompt",
|
||||||
cmd_buffer_switch_start
|
cmd_buffer_switch_start, false, false
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({CommandId::BufferNext, "buffer-next", "Switch to next buffer", cmd_buffer_next});
|
CommandRegistry::Register({CommandId::BufferNext, "buffer-next", "Switch to next buffer", cmd_buffer_next});
|
||||||
CommandRegistry::Register({CommandId::BufferPrev, "buffer-prev", "Switch to previous buffer", cmd_buffer_prev});
|
CommandRegistry::Register({CommandId::BufferPrev, "buffer-prev", "Switch to previous buffer", cmd_buffer_prev});
|
||||||
CommandRegistry::Register({CommandId::BufferClose, "buffer-close", "Close current buffer", cmd_buffer_close});
|
CommandRegistry::Register({
|
||||||
|
CommandId::BufferClose, "buffer-close", "Close current buffer", cmd_buffer_close, false, false
|
||||||
|
});
|
||||||
// Editing
|
// Editing
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::InsertText, "insert", "Insert text at cursor (no newlines)", cmd_insert_text
|
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::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
|
CommandRegistry::Register({CommandId::Backspace, "backspace", "Delete char before cursor", cmd_backspace});
|
||||||
@@ -3868,21 +4391,22 @@ InstallDefaultCommands()
|
|||||||
CommandId::DeleteWordNext, "delete-word-next", "Delete next word", cmd_delete_word_next
|
CommandId::DeleteWordNext, "delete-word-next", "Delete next word", cmd_delete_word_next
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to
|
CommandId::MoveCursorTo, "move-cursor-to", "Move cursor to y:x", cmd_move_cursor_to, false, false
|
||||||
});
|
});
|
||||||
// Direct navigation by line number
|
// Direct navigation by line number
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::JumpToLine, "goto-line", "Prompt for line and jump", cmd_jump_to_line_start
|
CommandId::JumpToLine, "goto-line", "Prompt for line and jump", cmd_jump_to_line_start, false, false
|
||||||
});
|
});
|
||||||
// Undo/Redo
|
// Undo/Redo
|
||||||
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo});
|
CommandRegistry::Register({CommandId::Undo, "undo", "Undo last edit", cmd_undo, false, true});
|
||||||
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo});
|
CommandRegistry::Register({CommandId::Redo, "redo", "Redo edit", cmd_redo, false, true});
|
||||||
// Region formatting
|
// Region formatting
|
||||||
CommandRegistry::Register({CommandId::IndentRegion, "indent-region", "Indent region", cmd_indent_region});
|
CommandRegistry::Register({CommandId::IndentRegion, "indent-region", "Indent region", cmd_indent_region});
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
|
{CommandId::UnindentRegion, "unindent-region", "Unindent region", cmd_unindent_region});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReflowParagraph, "reflow-paragraph", "Reflow paragraph to column width", cmd_reflow_paragraph
|
CommandId::ReflowParagraph, "reflow-paragraph",
|
||||||
|
"Reflow paragraph to column width", cmd_reflow_paragraph
|
||||||
});
|
});
|
||||||
// Read-only
|
// Read-only
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
@@ -3893,32 +4417,32 @@ InstallDefaultCommands()
|
|||||||
CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev});
|
CommandRegistry::Register({CommandId::ThemePrev, "theme-prev", "Cycle to previous GUI theme", cmd_theme_prev});
|
||||||
// Theme by name (public in command prompt)
|
// Theme by name (public in command prompt)
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true
|
CommandId::ThemeSetByName, "theme", "Set GUI theme by name", cmd_theme_set_by_name, true, false
|
||||||
});
|
});
|
||||||
// Font by name (public)
|
// Font by name (public)
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::FontSetByName, "font", "Set GUI font by name", cmd_font_set_by_name, true
|
CommandId::FontSetByName, "font", "Set GUI font by name", cmd_font_set_by_name, true, false
|
||||||
});
|
});
|
||||||
// Font size (public)
|
// Font size (public)
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::FontSetSize, "font-size", "Set GUI font size (pixels)", cmd_font_set_size, true
|
CommandId::FontSetSize, "font-size", "Set GUI font size (pixels)", cmd_font_set_size, true, false
|
||||||
});
|
});
|
||||||
// Background light/dark (public)
|
// Background light/dark (public)
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true
|
CommandId::BackgroundSet, "background", "Set GUI background light|dark", cmd_background_set, true, false
|
||||||
});
|
});
|
||||||
// Generic command prompt (C-k ;)
|
// Generic command prompt (C-k ;)
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt",
|
CommandId::CommandPromptStart, "command-prompt-start", "Start generic command prompt",
|
||||||
cmd_command_prompt_start
|
cmd_command_prompt_start, false, false
|
||||||
});
|
});
|
||||||
// Buffer operations
|
// Buffer operations
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer
|
CommandId::ReloadBuffer, "reload-buffer", "Reload buffer from disk", cmd_reload_buffer, false, false
|
||||||
});
|
});
|
||||||
// Help
|
// Help
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ShowHelp, "help", "+HELP+ buffer with manual text", cmd_show_help
|
CommandId::ShowHelp, "help", "+HELP+ buffer with manual text", cmd_show_help, false, false
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
|
CommandId::MarkAllAndJumpEnd, "mark-all-jump-end", "Set mark at beginning and jump to end",
|
||||||
@@ -3927,23 +4451,32 @@ InstallDefaultCommands()
|
|||||||
// GUI
|
// GUI
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
|
CommandId::VisualFilePickerToggle, "file-picker-toggle", "Toggle visual file picker",
|
||||||
cmd_visual_file_picker_toggle
|
cmd_visual_file_picker_toggle, false, false
|
||||||
|
});
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::VisualFontPickerToggle, "font-picker-toggle", "Show visual font picker",
|
||||||
|
cmd_visual_font_picker_toggle, false, false
|
||||||
});
|
});
|
||||||
// Working directory
|
// Working directory
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
|
CommandId::ShowWorkingDirectory, "show-working-directory", "Show current working directory",
|
||||||
cmd_show_working_directory
|
cmd_show_working_directory, false, false
|
||||||
});
|
});
|
||||||
CommandRegistry::Register({
|
CommandRegistry::Register({
|
||||||
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
|
CommandId::ChangeWorkingDirectory, "change-working-directory", "Change current working directory",
|
||||||
cmd_change_working_directory_start
|
cmd_change_working_directory_start, false, false
|
||||||
});
|
});
|
||||||
// UI helpers
|
// UI helpers
|
||||||
CommandRegistry::Register(
|
CommandRegistry::Register(
|
||||||
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status});
|
{CommandId::UArgStatus, "uarg-status", "Update universal-arg status", cmd_uarg_status, false, false});
|
||||||
// Syntax highlighting (public commands)
|
// Syntax highlighting (public commands)
|
||||||
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
CommandRegistry::Register({CommandId::Syntax, "syntax", "Syntax: on|off|reload", cmd_syntax, true});
|
||||||
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
CommandRegistry::Register({CommandId::SetOption, "set", "Set option: key=value", cmd_set_option, true});
|
||||||
|
// Viewport control
|
||||||
|
CommandRegistry::Register({
|
||||||
|
CommandId::CenterOnCursor, "center-on-cursor", "Center viewport on current line", cmd_center_on_cursor,
|
||||||
|
false, false
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -3972,7 +4505,22 @@ Execute(Editor &ed, CommandId id, const std::string &arg, int count)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
CommandContext ctx{ed, arg, count};
|
// Source repeat count from editor-level universal argument per new design.
|
||||||
|
int final_count = 0;
|
||||||
|
if (cmd->repeatable) {
|
||||||
|
final_count = ed.UArgGet(); // returns 1 if no active uarg
|
||||||
|
} else {
|
||||||
|
// Special-case non-repeatables that should NOT consume/clear uarg:
|
||||||
|
// - KPrefix: keeps uarg for the following k-suffix command.
|
||||||
|
// - UnknownKCommand / UnknownEscCommand: user mistyped; keep uarg for next try.
|
||||||
|
if (id != CommandId::KPrefix && id != CommandId::UnknownKCommand && id !=
|
||||||
|
CommandId::UnknownEscCommand) {
|
||||||
|
ed.UArgClear();
|
||||||
|
}
|
||||||
|
final_count = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
CommandContext ctx{ed, arg, final_count};
|
||||||
return cmd->handler ? cmd->handler(ctx) : false;
|
return cmd->handler ? cmd->handler(ctx) : false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,6 +27,8 @@ enum class CommandId {
|
|||||||
SearchReplace, // begin search & replace (two-step prompt)
|
SearchReplace, // begin search & replace (two-step prompt)
|
||||||
OpenFileStart, // begin open-file prompt
|
OpenFileStart, // begin open-file prompt
|
||||||
VisualFilePickerToggle,
|
VisualFilePickerToggle,
|
||||||
|
// GUI-only: toggle/show a visual font selector dialog
|
||||||
|
VisualFontPickerToggle,
|
||||||
// Buffers
|
// Buffers
|
||||||
BufferSwitchStart, // begin buffer switch prompt
|
BufferSwitchStart, // begin buffer switch prompt
|
||||||
BufferClose,
|
BufferClose,
|
||||||
@@ -90,6 +92,7 @@ enum class CommandId {
|
|||||||
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
ShowHelp, // open +HELP+ buffer with manual text (C-k h)
|
||||||
// Meta
|
// Meta
|
||||||
UnknownKCommand, // arg: single character that was not recognized after C-k
|
UnknownKCommand, // arg: single character that was not recognized after C-k
|
||||||
|
UnknownEscCommand, // invalid ESC (meta) command; show status and exit escape mode
|
||||||
// Generic command prompt
|
// Generic command prompt
|
||||||
CommandPromptStart, // begin generic command prompt (C-k ;)
|
CommandPromptStart, // begin generic command prompt (C-k ;)
|
||||||
// Theme by name
|
// Theme by name
|
||||||
@@ -103,6 +106,8 @@ enum class CommandId {
|
|||||||
// Syntax highlighting
|
// Syntax highlighting
|
||||||
Syntax, // ":syntax on|off|reload"
|
Syntax, // ":syntax on|off|reload"
|
||||||
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
SetOption, // generic ":set key=value" (v1: filetype=<lang>)
|
||||||
|
// Viewport control
|
||||||
|
CenterOnCursor, // center the viewport on the current cursor line (C-k k)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
@@ -128,6 +133,9 @@ struct Command {
|
|||||||
CommandHandler handler;
|
CommandHandler handler;
|
||||||
// Public commands are exposed in the ": " prompt (C-k ;)
|
// Public commands are exposed in the ": " prompt (C-k ;)
|
||||||
bool isPublic = false;
|
bool isPublic = false;
|
||||||
|
// Whether this command should consume and honor a universal argument repeat count.
|
||||||
|
// Default true per issue request; authors can turn off per-command.
|
||||||
|
bool repeatable = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
85
Editor.cc
85
Editor.cc
@@ -8,7 +8,10 @@
|
|||||||
#include "syntax/NullHighlighter.h"
|
#include "syntax/NullHighlighter.h"
|
||||||
|
|
||||||
|
|
||||||
Editor::Editor() = default;
|
Editor::Editor()
|
||||||
|
{
|
||||||
|
swap_ = std::make_unique<kte::SwapManager>();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
@@ -123,6 +126,11 @@ std::size_t
|
|||||||
Editor::AddBuffer(const Buffer &buf)
|
Editor::AddBuffer(const Buffer &buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(buf);
|
buffers_.push_back(buf);
|
||||||
|
// Attach swap recorder
|
||||||
|
if (swap_) {
|
||||||
|
buffers_.back().SetSwapRecorder(swap_.get());
|
||||||
|
swap_->Attach(&buffers_.back());
|
||||||
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (buffers_.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
@@ -134,6 +142,10 @@ std::size_t
|
|||||||
Editor::AddBuffer(Buffer &&buf)
|
Editor::AddBuffer(Buffer &&buf)
|
||||||
{
|
{
|
||||||
buffers_.push_back(std::move(buf));
|
buffers_.push_back(std::move(buf));
|
||||||
|
if (swap_) {
|
||||||
|
buffers_.back().SetSwapRecorder(swap_.get());
|
||||||
|
swap_->Attach(&buffers_.back());
|
||||||
|
}
|
||||||
if (buffers_.size() == 1) {
|
if (buffers_.size() == 1) {
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
@@ -157,6 +169,12 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
bool ok = cur.OpenFromFile(path, err);
|
bool ok = cur.OpenFromFile(path, err);
|
||||||
if (!ok)
|
if (!ok)
|
||||||
return false;
|
return false;
|
||||||
|
// Ensure swap recorder is attached for this buffer
|
||||||
|
if (swap_) {
|
||||||
|
cur.SetSwapRecorder(swap_.get());
|
||||||
|
swap_->Attach(&cur);
|
||||||
|
swap_->NotifyFilenameChanged(cur);
|
||||||
|
}
|
||||||
// Setup highlighting using registry (extension + shebang)
|
// Setup highlighting using registry (extension + shebang)
|
||||||
cur.EnsureHighlighter();
|
cur.EnsureHighlighter();
|
||||||
std::string first = "";
|
std::string first = "";
|
||||||
@@ -187,6 +205,12 @@ Editor::OpenFile(const std::string &path, std::string &err)
|
|||||||
if (!b.OpenFromFile(path, err)) {
|
if (!b.OpenFromFile(path, err)) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (swap_) {
|
||||||
|
b.SetSwapRecorder(swap_.get());
|
||||||
|
// path is known, notify
|
||||||
|
swap_->Attach(&b);
|
||||||
|
swap_->NotifyFilenameChanged(b);
|
||||||
|
}
|
||||||
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
// Initialize syntax highlighting by extension + shebang via registry (v2)
|
||||||
b.EnsureHighlighter();
|
b.EnsureHighlighter();
|
||||||
std::string first = "";
|
std::string first = "";
|
||||||
@@ -278,8 +302,67 @@ Editor::Reset()
|
|||||||
msgtm_ = 0;
|
msgtm_ = 0;
|
||||||
uarg_ = 0;
|
uarg_ = 0;
|
||||||
ucount_ = 0;
|
ucount_ = 0;
|
||||||
|
repeatable_ = false;
|
||||||
quit_requested_ = false;
|
quit_requested_ = false;
|
||||||
quit_confirm_pending_ = false;
|
quit_confirm_pending_ = false;
|
||||||
|
// Reset close-confirm/save state
|
||||||
|
close_confirm_pending_ = false;
|
||||||
|
close_after_save_ = false;
|
||||||
buffers_.clear();
|
buffers_.clear();
|
||||||
curbuf_ = 0;
|
curbuf_ = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Universal argument helpers ---
|
||||||
|
void
|
||||||
|
Editor::UArgStart()
|
||||||
|
{
|
||||||
|
// If not active, start fresh; else multiply by 4 per ke semantics
|
||||||
|
if (uarg_ == 0) {
|
||||||
|
ucount_ = 0;
|
||||||
|
} else {
|
||||||
|
if (ucount_ == 0) {
|
||||||
|
ucount_ = 1;
|
||||||
|
}
|
||||||
|
ucount_ *= 4;
|
||||||
|
}
|
||||||
|
uarg_ = 1;
|
||||||
|
char buf[64];
|
||||||
|
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
||||||
|
SetStatus(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Editor::UArgDigit(int d)
|
||||||
|
{
|
||||||
|
if (d < 0)
|
||||||
|
d = 0;
|
||||||
|
if (d > 9)
|
||||||
|
d = 9;
|
||||||
|
if (uarg_ == 0) {
|
||||||
|
uarg_ = 1;
|
||||||
|
ucount_ = 0;
|
||||||
|
}
|
||||||
|
ucount_ = ucount_ * 10 + d;
|
||||||
|
char buf[64];
|
||||||
|
std::snprintf(buf, sizeof(buf), "C-u %d", ucount_);
|
||||||
|
SetStatus(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
Editor::UArgClear()
|
||||||
|
{
|
||||||
|
uarg_ = 0;
|
||||||
|
ucount_ = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
int
|
||||||
|
Editor::UArgGet()
|
||||||
|
{
|
||||||
|
int n = (ucount_ > 0) ? ucount_ : 1;
|
||||||
|
UArgClear();
|
||||||
|
return n;
|
||||||
|
}
|
||||||
70
Editor.h
70
Editor.h
@@ -8,6 +8,7 @@
|
|||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
|
#include "Swap.h"
|
||||||
|
|
||||||
|
|
||||||
class Editor {
|
class Editor {
|
||||||
@@ -156,6 +157,33 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Universal argument control (C-u) ---
|
||||||
|
// Begin or extend a universal argument (like ke's uarg_start)
|
||||||
|
void UArgStart();
|
||||||
|
|
||||||
|
// Add a digit 0..9 to the current universal argument (like ke's uarg_digit)
|
||||||
|
void UArgDigit(int d);
|
||||||
|
|
||||||
|
// Clear universal-argument state (like ke's uarg_clear)
|
||||||
|
void UArgClear();
|
||||||
|
|
||||||
|
// Consume the current universal argument, returning count >= 1.
|
||||||
|
// If no universal argument active, returns 1.
|
||||||
|
int UArgGet();
|
||||||
|
|
||||||
|
// Repeatable command flag: input layer can mark the next command as repeatable
|
||||||
|
void SetRepeatable(bool on)
|
||||||
|
{
|
||||||
|
repeatable_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool Repeatable() const
|
||||||
|
{
|
||||||
|
return repeatable_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Status message storage. Rendering is renderer-dependent; the editor
|
// Status message storage. Rendering is renderer-dependent; the editor
|
||||||
// merely stores the current message and its timestamp.
|
// merely stores the current message and its timestamp.
|
||||||
void SetStatus(const std::string &message);
|
void SetStatus(const std::string &message);
|
||||||
@@ -192,6 +220,31 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// --- Buffer close/save confirmation state ---
|
||||||
|
void SetCloseConfirmPending(bool on)
|
||||||
|
{
|
||||||
|
close_confirm_pending_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool CloseConfirmPending() const
|
||||||
|
{
|
||||||
|
return close_confirm_pending_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetCloseAfterSave(bool on)
|
||||||
|
{
|
||||||
|
close_after_save_ = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
[[nodiscard]] bool CloseAfterSave() const
|
||||||
|
{
|
||||||
|
return close_after_save_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[[nodiscard]] std::time_t StatusTime() const
|
[[nodiscard]] std::time_t StatusTime() const
|
||||||
{
|
{
|
||||||
return msgtm_;
|
return msgtm_;
|
||||||
@@ -465,6 +518,13 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Swap manager access (for advanced integrations/tests)
|
||||||
|
[[nodiscard]] kte::SwapManager *Swap()
|
||||||
|
{
|
||||||
|
return swap_.get();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// --- GUI: Visual File Picker state ---
|
// --- GUI: Visual File Picker state ---
|
||||||
void SetFilePickerVisible(bool on)
|
void SetFilePickerVisible(bool on)
|
||||||
{
|
{
|
||||||
@@ -498,17 +558,23 @@ private:
|
|||||||
std::string msg_;
|
std::string msg_;
|
||||||
std::time_t msgtm_ = 0;
|
std::time_t msgtm_ = 0;
|
||||||
int uarg_ = 0, ucount_ = 0; // C-u support
|
int uarg_ = 0, ucount_ = 0; // C-u support
|
||||||
|
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::size_t curbuf_ = 0; // index into buffers_
|
||||||
|
|
||||||
|
// Swap journaling manager (lifetime = editor)
|
||||||
|
std::unique_ptr<kte::SwapManager> swap_;
|
||||||
|
|
||||||
// Kill ring (Emacs-like)
|
// Kill ring (Emacs-like)
|
||||||
std::vector<std::string> kill_ring_;
|
std::vector<std::string> kill_ring_;
|
||||||
std::size_t kill_ring_max_ = 60;
|
std::size_t kill_ring_max_ = 60;
|
||||||
|
|
||||||
// Quit state
|
// Quit state
|
||||||
bool quit_requested_ = false;
|
bool quit_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_after_save_ = false; // if true, close buffer after successful Save/SaveAs
|
||||||
|
|
||||||
// Search state
|
// Search state
|
||||||
bool search_active_ = false;
|
bool search_active_ = false;
|
||||||
|
|||||||
@@ -1,14 +0,0 @@
|
|||||||
/*
|
|
||||||
* GUIRenderer - ImGui-based renderer for GUI mode
|
|
||||||
*/
|
|
||||||
#pragma once
|
|
||||||
#include "Renderer.h"
|
|
||||||
|
|
||||||
class GUIRenderer final : public Renderer {
|
|
||||||
public:
|
|
||||||
GUIRenderer() = default;
|
|
||||||
|
|
||||||
~GUIRenderer() override = default;
|
|
||||||
|
|
||||||
void Draw(Editor &ed) override;
|
|
||||||
};
|
|
||||||
306
GUITheme.h
306
GUITheme.h
@@ -1,11 +1,307 @@
|
|||||||
// GUITheme.h — ImGui theming helpers and background mode
|
// GUITheme.h — theming helpers and background mode
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <cctype>
|
||||||
|
|
||||||
|
#include "Highlight.h"
|
||||||
|
|
||||||
|
// Cross-frontend theme change request hook: declared here, defined in Command.cc
|
||||||
|
namespace kte {
|
||||||
|
extern bool gThemeChangePending;
|
||||||
|
extern std::string gThemeChangeRequest; // raw user-provided name
|
||||||
|
// Qt GUI: cross-frontend font change hooks and current font state
|
||||||
|
extern bool gFontChangePending;
|
||||||
|
extern std::string gFontFamilyRequest; // requested family (case-insensitive)
|
||||||
|
extern float gFontSizeRequest; // <= 0 means keep size
|
||||||
|
extern std::string gCurrentFontFamily; // last applied family (Qt)
|
||||||
|
extern float gCurrentFontSize; // last applied size (Qt)
|
||||||
|
// Qt GUI: request to show a visual font dialog (set by command handler)
|
||||||
|
extern bool gFontDialogRequested;
|
||||||
|
}
|
||||||
|
|
||||||
|
#if defined(KTE_USE_QT)
|
||||||
|
// Qt build: avoid hard dependency on ImGui headers/types.
|
||||||
|
// Provide a lightweight color vector matching ImVec4 fields used by renderers.
|
||||||
|
struct KteColor {
|
||||||
|
float x{0}, y{0}, z{0}, w{1};
|
||||||
|
};
|
||||||
|
|
||||||
|
static inline KteColor
|
||||||
|
RGBA(unsigned int rgb, float a = 1.0f)
|
||||||
|
{
|
||||||
|
const float r = static_cast<float>((rgb >> 16) & 0xFF) / 255.0f;
|
||||||
|
const float g = static_cast<float>((rgb >> 8) & 0xFF) / 255.0f;
|
||||||
|
const float b = static_cast<float>(rgb & 0xFF) / 255.0f;
|
||||||
|
return {r, g, b, a};
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Background mode selection for light/dark palettes
|
||||||
|
enum class BackgroundMode { Light, Dark };
|
||||||
|
|
||||||
|
// Global background mode; default to Dark to match prior defaults
|
||||||
|
static inline auto gBackgroundMode = BackgroundMode::Dark;
|
||||||
|
|
||||||
|
|
||||||
|
static inline void
|
||||||
|
SetBackgroundMode(const BackgroundMode m)
|
||||||
|
{
|
||||||
|
gBackgroundMode = m;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline BackgroundMode
|
||||||
|
GetBackgroundMode()
|
||||||
|
{
|
||||||
|
return gBackgroundMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Minimal GUI palette for Qt builds. This mirrors the defaults used in the ImGui
|
||||||
|
// frontend (Nord-ish) and switches for light/dark background mode.
|
||||||
|
struct Palette {
|
||||||
|
KteColor bg; // editor background
|
||||||
|
KteColor fg; // default foreground text
|
||||||
|
KteColor sel_bg; // selection background
|
||||||
|
KteColor cur_bg; // cursor cell background
|
||||||
|
KteColor status_bg; // status bar background
|
||||||
|
KteColor status_fg; // status bar foreground
|
||||||
|
};
|
||||||
|
|
||||||
|
// Optional theme override (Qt): when set, GetPalette() will return this instead
|
||||||
|
// of the generic light/dark defaults. This allows honoring theme names in kge.ini.
|
||||||
|
static inline bool gPaletteOverride = false;
|
||||||
|
static inline Palette gOverridePalette{};
|
||||||
|
static inline std::string gOverrideThemeName = ""; // lowercased name
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
GetPalette()
|
||||||
|
{
|
||||||
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
|
if (gPaletteOverride) {
|
||||||
|
return gOverridePalette;
|
||||||
|
}
|
||||||
|
if (dark) {
|
||||||
|
return Palette{
|
||||||
|
/*bg*/ RGBA(0x1C1C1E),
|
||||||
|
/*fg*/ RGBA(0xDCDCDC),
|
||||||
|
/*sel_bg*/ RGBA(0xC8C800, 0.35f),
|
||||||
|
/*cur_bg*/ RGBA(0xC8C8FF, 0.50f),
|
||||||
|
/*status_bg*/ RGBA(0x28282C),
|
||||||
|
/*status_fg*/ RGBA(0xB4B48C)
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
// Light palette tuned for readability
|
||||||
|
return Palette{
|
||||||
|
/*bg*/ RGBA(0xFBFBFC),
|
||||||
|
/*fg*/ RGBA(0x30343A),
|
||||||
|
/*sel_bg*/ RGBA(0x268BD2, 0.22f),
|
||||||
|
/*cur_bg*/ RGBA(0x000000, 0.15f),
|
||||||
|
/*status_bg*/ RGBA(0xE6E8EA),
|
||||||
|
/*status_fg*/ RGBA(0x50555A)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// A few named palettes to provide visible differences between themes in Qt.
|
||||||
|
// These are approximate and palette-based (no widget style changes like ImGuiStyle).
|
||||||
|
static inline Palette
|
||||||
|
NordDark()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0x2E3440), /*fg*/RGBA(0xD8DEE9), /*sel_bg*/RGBA(0x88C0D0, 0.25f),
|
||||||
|
/*cur_bg*/RGBA(0x81A1C1, 0.35f), /*status_bg*/RGBA(0x3B4252), /*status_fg*/RGBA(0xE5E9F0)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
NordLight()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0xECEFF4), /*fg*/RGBA(0x2E3440), /*sel_bg*/RGBA(0x5E81AC, 0.22f),
|
||||||
|
/*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0xE5E9F0), /*status_fg*/RGBA(0x4C566A)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
SolarizedDark()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0x002b36), /*fg*/RGBA(0x93a1a1), /*sel_bg*/RGBA(0x586e75, 0.40f),
|
||||||
|
/*cur_bg*/RGBA(0x657b83, 0.35f), /*status_bg*/RGBA(0x073642), /*status_fg*/RGBA(0xeee8d5)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
SolarizedLight()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0xfdf6e3), /*fg*/RGBA(0x586e75), /*sel_bg*/RGBA(0x268bd2, 0.25f),
|
||||||
|
/*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xeee8d5), /*status_fg*/RGBA(0x657b83)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
GruvboxDark()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0x282828), /*fg*/RGBA(0xebdbb2), /*sel_bg*/RGBA(0xd79921, 0.35f),
|
||||||
|
/*cur_bg*/RGBA(0x458588, 0.40f), /*status_bg*/RGBA(0x3c3836), /*status_fg*/RGBA(0xd5c4a1)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
GruvboxLight()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0xfbf1c7), /*fg*/RGBA(0x3c3836), /*sel_bg*/RGBA(0x076678, 0.22f),
|
||||||
|
/*cur_bg*/RGBA(0x000000, 0.10f), /*status_bg*/RGBA(0xebdbb2), /*status_fg*/RGBA(0x504945)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static inline Palette
|
||||||
|
EInk()
|
||||||
|
{
|
||||||
|
return {
|
||||||
|
/*bg*/RGBA(0xffffff), /*fg*/RGBA(0x000000), /*sel_bg*/RGBA(0x000000, 0.10f),
|
||||||
|
/*cur_bg*/RGBA(0x000000, 0.12f), /*status_bg*/RGBA(0x000000), /*status_fg*/RGBA(0xffffff)
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Apply a Qt theme by name. Returns true on success. Name matching is case-insensitive and
|
||||||
|
// supports common aliases (e.g., "solarized-light" or "solarized light"). If the name conveys
|
||||||
|
// a background (light/dark), BackgroundMode is updated to keep SyntaxInk consistent.
|
||||||
|
static inline bool
|
||||||
|
ApplyQtThemeByName(std::string name)
|
||||||
|
{
|
||||||
|
// normalize
|
||||||
|
std::transform(name.begin(), name.end(), name.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
auto has = [&](const std::string &s) {
|
||||||
|
return name.find(s) != std::string::npos;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (name.empty() || name == "default" || name == "nord") {
|
||||||
|
// Choose variant by current background mode
|
||||||
|
if (GetBackgroundMode() == BackgroundMode::Dark) {
|
||||||
|
gOverridePalette = NordDark();
|
||||||
|
} else {
|
||||||
|
gOverridePalette = NordLight();
|
||||||
|
}
|
||||||
|
gPaletteOverride = true;
|
||||||
|
gOverrideThemeName = "nord";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has("solarized")) {
|
||||||
|
if (has("light")) {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
gOverridePalette = SolarizedLight();
|
||||||
|
} else if (has("dark")) {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
gOverridePalette = SolarizedDark();
|
||||||
|
} else {
|
||||||
|
// pick from current background
|
||||||
|
gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark)
|
||||||
|
? SolarizedDark()
|
||||||
|
: SolarizedLight();
|
||||||
|
}
|
||||||
|
gPaletteOverride = true;
|
||||||
|
gOverrideThemeName = "solarized";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has("gruvbox")) {
|
||||||
|
if (has("light")) {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
gOverridePalette = GruvboxLight();
|
||||||
|
} else if (has("dark")) {
|
||||||
|
SetBackgroundMode(BackgroundMode::Dark);
|
||||||
|
gOverridePalette = GruvboxDark();
|
||||||
|
} else {
|
||||||
|
gOverridePalette = (GetBackgroundMode() == BackgroundMode::Dark)
|
||||||
|
? GruvboxDark()
|
||||||
|
: GruvboxLight();
|
||||||
|
}
|
||||||
|
gPaletteOverride = true;
|
||||||
|
gOverrideThemeName = "gruvbox";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (has("eink") || has("e-ink") || has("paper")) {
|
||||||
|
SetBackgroundMode(BackgroundMode::Light);
|
||||||
|
gOverridePalette = EInk();
|
||||||
|
gPaletteOverride = true;
|
||||||
|
gOverrideThemeName = "eink";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unknown -> clear override so default light/dark applies; return false.
|
||||||
|
gPaletteOverride = false;
|
||||||
|
gOverrideThemeName.clear();
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Minimal SyntaxInk mapping for Qt builds, returning KteColor
|
||||||
|
[[maybe_unused]] static KteColor
|
||||||
|
SyntaxInk(const TokenKind k)
|
||||||
|
{
|
||||||
|
const bool dark = (GetBackgroundMode() == BackgroundMode::Dark);
|
||||||
|
const KteColor def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440);
|
||||||
|
switch (k) {
|
||||||
|
case TokenKind::Keyword:
|
||||||
|
return dark ? RGBA(0x81A1C1) : RGBA(0x5E81AC);
|
||||||
|
case TokenKind::Type:
|
||||||
|
return dark ? RGBA(0x8FBCBB) : RGBA(0x4C566A);
|
||||||
|
case TokenKind::String:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Char:
|
||||||
|
return dark ? RGBA(0xA3BE8C) : RGBA(0x6C8E5E);
|
||||||
|
case TokenKind::Comment:
|
||||||
|
return dark ? RGBA(0x616E88) : RGBA(0x7A869A);
|
||||||
|
case TokenKind::Number:
|
||||||
|
return dark ? RGBA(0xEBCB8B) : RGBA(0xB58900);
|
||||||
|
case TokenKind::Preproc:
|
||||||
|
return dark ? RGBA(0xD08770) : RGBA(0xAF3A03);
|
||||||
|
case TokenKind::Constant:
|
||||||
|
return dark ? RGBA(0xB48EAD) : RGBA(0x7B4B7F);
|
||||||
|
case TokenKind::Function:
|
||||||
|
return dark ? RGBA(0x88C0D0) : RGBA(0x3465A4);
|
||||||
|
case TokenKind::Operator:
|
||||||
|
return dark ? RGBA(0x2E3440) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Punctuation:
|
||||||
|
return dark ? RGBA(0xECEFF4) : RGBA(0x2E3440);
|
||||||
|
case TokenKind::Identifier:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Whitespace:
|
||||||
|
return def;
|
||||||
|
case TokenKind::Error:
|
||||||
|
return dark ? RGBA(0xBF616A) : RGBA(0xCC0000);
|
||||||
|
case TokenKind::Default: default:
|
||||||
|
return def;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
|
|
||||||
|
#else
|
||||||
|
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <cstddef>
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
|
|
||||||
@@ -38,7 +334,7 @@ enum class ThemeId {
|
|||||||
|
|
||||||
// Current theme tracking
|
// Current theme tracking
|
||||||
static inline auto gCurrentTheme = ThemeId::Nord;
|
static inline auto gCurrentTheme = ThemeId::Nord;
|
||||||
static inline std::size_t gCurrentThemeIndex = 0;
|
static inline std::size_t gCurrentThemeIndex = 6; // 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);
|
||||||
@@ -644,4 +940,6 @@ SyntaxInk(const TokenKind k)
|
|||||||
return def;
|
return def;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} // namespace kte
|
} // namespace kte
|
||||||
|
|
||||||
|
#endif // KTE_USE_QT
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
#include <backends/imgui_impl_opengl3.h>
|
#include <backends/imgui_impl_opengl3.h>
|
||||||
#include <backends/imgui_impl_sdl2.h>
|
#include <backends/imgui_impl_sdl2.h>
|
||||||
|
|
||||||
#include "GUIFrontend.h"
|
#include "ImGuiFrontend.h"
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
#include "Editor.h"
|
#include "Editor.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
@@ -31,7 +31,9 @@ static auto kGlslVersion = "#version 150"; // GL 3.2 core (macOS compatible)
|
|||||||
bool
|
bool
|
||||||
GUIFrontend::Init(Editor &ed)
|
GUIFrontend::Init(Editor &ed)
|
||||||
{
|
{
|
||||||
(void) ed; // editor dimensions will be initialized during the first Step() frame
|
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||||
|
input_.Attach(&ed);
|
||||||
|
// editor dimensions will be initialized during the first Step() frame
|
||||||
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_TIMER) != 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -270,10 +272,11 @@ GUIFrontend::Step(Editor &ed, bool &running)
|
|||||||
float disp_w = io.DisplaySize.x > 0 ? io.DisplaySize.x : static_cast<float>(width_);
|
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_);
|
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 GUIRenderer
|
// Account for the GUI window padding and the status bar height used in ImGuiRenderer.
|
||||||
const ImGuiStyle &style = ImGui::GetStyle();
|
// ImGuiRenderer pushes WindowPadding = (6,6) every frame, so use the same constants here
|
||||||
float pad_x = style.WindowPadding.x;
|
// to avoid mismatches that would cause premature scrolling.
|
||||||
float pad_y = style.WindowPadding.y;
|
const float pad_x = 6.0f;
|
||||||
|
const float pad_y = 6.0f;
|
||||||
// Status bar reserves one frame height (with spacing) inside the window
|
// Status bar reserves one frame height (with spacing) inside the window
|
||||||
float status_h = ImGui::GetFrameHeightWithSpacing();
|
float status_h = ImGui::GetFrameHeightWithSpacing();
|
||||||
|
|
||||||
@@ -1,11 +1,11 @@
|
|||||||
/*
|
/*
|
||||||
* GUIFrontend - couples GUIInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
* GUIFrontend - couples ImGuiInputHandler + GUIRenderer and owns SDL2/ImGui lifecycle
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include "Frontend.h"
|
#include "Frontend.h"
|
||||||
#include "GUIConfig.h"
|
#include "GUIConfig.h"
|
||||||
#include "GUIInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "GUIRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
|
|
||||||
|
|
||||||
struct SDL_Window;
|
struct SDL_Window;
|
||||||
@@ -27,8 +27,8 @@ private:
|
|||||||
static bool LoadGuiFont_(const char *path, float size_px);
|
static bool LoadGuiFont_(const char *path, float size_px);
|
||||||
|
|
||||||
GUIConfig config_{};
|
GUIConfig config_{};
|
||||||
GUIInputHandler input_{};
|
ImGuiInputHandler input_{};
|
||||||
GUIRenderer renderer_{};
|
ImGuiRenderer renderer_{};
|
||||||
SDL_Window *window_ = nullptr;
|
SDL_Window *window_ = nullptr;
|
||||||
SDL_GLContext gl_ctx_ = nullptr;
|
SDL_GLContext gl_ctx_ = nullptr;
|
||||||
int width_ = 1280;
|
int width_ = 1280;
|
||||||
@@ -5,8 +5,9 @@
|
|||||||
#include <SDL.h>
|
#include <SDL.h>
|
||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
|
|
||||||
#include "GUIInputHandler.h"
|
#include "ImGuiInputHandler.h"
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
|
||||||
static bool
|
static bool
|
||||||
@@ -14,20 +15,17 @@ map_key(const SDL_Keycode key,
|
|||||||
const SDL_Keymod mod,
|
const SDL_Keymod mod,
|
||||||
bool &k_prefix,
|
bool &k_prefix,
|
||||||
bool &esc_meta,
|
bool &esc_meta,
|
||||||
// universal-argument state (by ref)
|
bool &k_ctrl_pending,
|
||||||
bool &uarg_active,
|
Editor *ed,
|
||||||
bool &uarg_collecting,
|
MappedInput &out,
|
||||||
bool &uarg_negative,
|
bool &suppress_textinput_once)
|
||||||
bool &uarg_had_digits,
|
|
||||||
int &uarg_value,
|
|
||||||
std::string &uarg_text,
|
|
||||||
MappedInput &out)
|
|
||||||
{
|
{
|
||||||
// Ctrl handling
|
// Ctrl handling
|
||||||
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
const bool is_ctrl = (mod & KMOD_CTRL) != 0;
|
||||||
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
const bool is_alt = (mod & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||||
|
|
||||||
// If previous key was ESC, interpret this as Meta via ESC keymap
|
// If previous key was ESC, interpret this as Meta via ESC keymap.
|
||||||
|
// Prefer KEYDOWN when we can derive a printable ASCII; otherwise defer to TEXTINPUT.
|
||||||
if (esc_meta) {
|
if (esc_meta) {
|
||||||
int ascii_key = 0;
|
int ascii_key = 0;
|
||||||
if (key == SDLK_BACKSPACE) {
|
if (key == SDLK_BACKSPACE) {
|
||||||
@@ -45,17 +43,18 @@ map_key(const SDL_Keycode key,
|
|||||||
ascii_key = '>';
|
ascii_key = '>';
|
||||||
}
|
}
|
||||||
if (ascii_key != 0) {
|
if (ascii_key != 0) {
|
||||||
|
esc_meta = false; // consume if we can decide on KEYDOWN
|
||||||
ascii_key = KLowerAscii(ascii_key);
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupEscCommand(ascii_key, id)) {
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
// Only consume the ESC-meta prefix if we actually mapped a command
|
out = {true, id, "", 0};
|
||||||
esc_meta = false;
|
|
||||||
out = {true, id, "", 0};
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
// Known printable but unmapped ESC sequence: report invalid
|
||||||
|
out = {true, CommandId::UnknownEscCommand, "", 0};
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// Unhandled meta chord at KEYDOWN: do not clear esc_meta here.
|
// No usable ASCII from KEYDOWN → keep esc_meta set and let TEXTINPUT handle it
|
||||||
// Leave it set so SDL_TEXTINPUT fallback can translate and suppress insertion.
|
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -65,43 +64,53 @@ map_key(const SDL_Keycode key,
|
|||||||
switch (key) {
|
switch (key) {
|
||||||
case SDLK_LEFT:
|
case SDLK_LEFT:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveLeft, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveLeft, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_RIGHT:
|
case SDLK_RIGHT:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveRight, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveRight, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_UP:
|
case SDLK_UP:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveUp, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveUp, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_DOWN:
|
case SDLK_DOWN:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_HOME:
|
case SDLK_HOME:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveHome, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveHome, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_END:
|
case SDLK_END:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveEnd, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::MoveEnd, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_PAGEUP:
|
case SDLK_PAGEUP:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::PageUp, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::PageUp, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_PAGEDOWN:
|
case SDLK_PAGEDOWN:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::PageDown, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::PageDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_DELETE:
|
case SDLK_DELETE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::DeleteChar, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::DeleteChar, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_BACKSPACE:
|
case SDLK_BACKSPACE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_TAB:
|
case SDLK_TAB:
|
||||||
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
// Insert a literal tab character when not interpreting a k-prefix suffix.
|
||||||
@@ -114,10 +123,13 @@ map_key(const SDL_Keycode key,
|
|||||||
break; // fall through so k-prefix handler can process
|
break; // fall through so k-prefix handler can process
|
||||||
case SDLK_RETURN:
|
case SDLK_RETURN:
|
||||||
case SDLK_KP_ENTER:
|
case SDLK_KP_ENTER:
|
||||||
out = {true, CommandId::Newline, "", 0};
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Newline, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case SDLK_ESCAPE:
|
case SDLK_ESCAPE:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
esc_meta = true; // next key will be treated as Meta
|
esc_meta = true; // next key will be treated as Meta
|
||||||
out.hasCommand = false; // no immediate command for bare ESC in GUI
|
out.hasCommand = false; // no immediate command for bare ESC in GUI
|
||||||
return true;
|
return true;
|
||||||
@@ -127,7 +139,6 @@ map_key(const SDL_Keycode key,
|
|||||||
|
|
||||||
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
|
// If we are in k-prefix, interpret the very next key via the C-k keymap immediately.
|
||||||
if (k_prefix) {
|
if (k_prefix) {
|
||||||
k_prefix = false;
|
|
||||||
esc_meta = false;
|
esc_meta = false;
|
||||||
// Normalize to ASCII; preserve case for letters using Shift
|
// Normalize to ASCII; preserve case for letters using Shift
|
||||||
int ascii_key = 0;
|
int ascii_key = 0;
|
||||||
@@ -147,10 +158,23 @@ map_key(const SDL_Keycode key,
|
|||||||
ascii_key = static_cast<int>(key);
|
ascii_key = static_cast<int>(key);
|
||||||
}
|
}
|
||||||
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
bool ctrl2 = (mod & KMOD_CTRL) != 0;
|
||||||
|
// If user typed a literal 'C' (or '^') as a control qualifier, keep k-prefix active
|
||||||
|
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||||
|
k_ctrl_pending = true;
|
||||||
|
// Keep waiting for the next suffix; show status and suppress ensuing TEXTINPUT
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus("C-k C _");
|
||||||
|
suppress_textinput_once = true;
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// Otherwise, consume the k-prefix now for the actual suffix
|
||||||
|
k_prefix = false;
|
||||||
if (ascii_key != 0) {
|
if (ascii_key != 0) {
|
||||||
int lower = KLowerAscii(ascii_key);
|
int lower = KLowerAscii(ascii_key);
|
||||||
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
||||||
bool pass_ctrl = ctrl2 && ctrl_suffix_supported;
|
bool pass_ctrl = (ctrl2 || k_ctrl_pending) && ctrl_suffix_supported;
|
||||||
|
k_ctrl_pending = false;
|
||||||
CommandId id;
|
CommandId id;
|
||||||
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
// Diagnostics for u/U
|
// Diagnostics for u/U
|
||||||
@@ -167,54 +191,40 @@ map_key(const SDL_Keycode key,
|
|||||||
}
|
}
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus(""); // clear "C-k _" hint after suffix
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
int shown = KLowerAscii(ascii_key);
|
int shown = KLowerAscii(ascii_key);
|
||||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus(""); // clear hint; handler will set unknown status
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
out.hasCommand = false;
|
// Non-printable/unmappable key as k-suffix (e.g., F-keys): report unknown and exit k-mode
|
||||||
|
out = {true, CommandId::UnknownKCommand, std::string("?"), 0};
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus("");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (is_ctrl) {
|
if (is_ctrl) {
|
||||||
// Universal argument: C-u
|
// Universal argument: C-u
|
||||||
if (key == SDLK_u) {
|
if (key == SDLK_u) {
|
||||||
if (!uarg_active) {
|
if (ed)
|
||||||
uarg_active = true;
|
ed->UArgStart();
|
||||||
uarg_collecting = true;
|
|
||||||
uarg_negative = false;
|
|
||||||
uarg_had_digits = false;
|
|
||||||
uarg_value = 4; // default
|
|
||||||
uarg_text.clear();
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
|
||||||
if (uarg_value <= 0)
|
|
||||||
uarg_value = 4;
|
|
||||||
else
|
|
||||||
uarg_value *= 4; // repeated C-u multiplies by 4
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// End collection if already started with digits or '-'
|
|
||||||
uarg_collecting = false;
|
|
||||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
|
||||||
uarg_value = 4;
|
|
||||||
}
|
|
||||||
out.hasCommand = false;
|
out.hasCommand = false;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
|
// Cancel universal arg on C-g as well (it maps to Refresh via ctrl map)
|
||||||
if (key == SDLK_g) {
|
if (key == SDLK_g) {
|
||||||
uarg_active = false;
|
if (ed)
|
||||||
uarg_collecting = false;
|
ed->UArgClear();
|
||||||
uarg_negative = false;
|
// Also cancel any pending k-prefix qualifier
|
||||||
uarg_had_digits = false;
|
k_ctrl_pending = false;
|
||||||
uarg_value = 0;
|
k_prefix = false; // treat as cancel of prefix
|
||||||
uarg_text.clear();
|
|
||||||
}
|
}
|
||||||
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
if (key == SDLK_k || key == SDLK_KP_EQUALS) {
|
||||||
k_prefix = true;
|
k_prefix = true;
|
||||||
@@ -258,29 +268,17 @@ map_key(const SDL_Keycode key,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If collecting universal argument, allow digits/minus on KEYDOWN path too
|
// If collecting universal argument, allow digits on KEYDOWN path too
|
||||||
if (uarg_active && uarg_collecting) {
|
if (ed && ed->UArg() != 0) {
|
||||||
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
|
if ((key >= SDLK_0 && key <= SDLK_9) && !(mod & KMOD_SHIFT)) {
|
||||||
int d = static_cast<int>(key - SDLK_0);
|
int d = static_cast<int>(key - SDLK_0);
|
||||||
if (!uarg_had_digits) {
|
ed->UArgDigit(d);
|
||||||
uarg_value = 0;
|
out.hasCommand = false;
|
||||||
uarg_had_digits = true;
|
// We consumed a digit on KEYDOWN; SDL will often also emit TEXTINPUT for it.
|
||||||
}
|
// Request suppression of the very next TEXTINPUT to avoid double-counting.
|
||||||
if (uarg_value < 100000000) {
|
suppress_textinput_once = true;
|
||||||
uarg_value = uarg_value * 10 + d;
|
|
||||||
}
|
|
||||||
uarg_text.push_back(static_cast<char>('0' + d));
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (key == SDLK_MINUS && !uarg_had_digits && !uarg_negative) {
|
|
||||||
uarg_negative = true;
|
|
||||||
uarg_text = "-";
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Any other key will end collection; process it normally
|
|
||||||
uarg_collecting = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// k_prefix handled earlier
|
// k_prefix handled earlier
|
||||||
@@ -290,7 +288,7 @@ map_key(const SDL_Keycode key,
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
||||||
{
|
{
|
||||||
MappedInput mi;
|
MappedInput mi;
|
||||||
bool produced = false;
|
bool produced = false;
|
||||||
@@ -345,7 +343,9 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
segment = std::string_view(text).substr(start);
|
segment = std::string_view(text).substr(start);
|
||||||
}
|
}
|
||||||
if (!segment.empty()) {
|
if (!segment.empty()) {
|
||||||
MappedInput ins{true, CommandId::InsertText, std::string(segment), 0};
|
MappedInput ins{
|
||||||
|
true, CommandId::InsertText, std::string(segment), 0
|
||||||
|
};
|
||||||
q_.push(ins);
|
q_.push(ins);
|
||||||
}
|
}
|
||||||
if (has_nl) {
|
if (has_nl) {
|
||||||
@@ -362,29 +362,28 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
produced = map_key(key, mods,
|
{
|
||||||
k_prefix_, esc_meta_,
|
bool suppress_req = false;
|
||||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_,
|
produced = map_key(key, mods,
|
||||||
uarg_text_,
|
k_prefix_, esc_meta_,
|
||||||
mi);
|
k_ctrl_pending_,
|
||||||
|
ed_,
|
||||||
// If we inserted a TAB on KEYDOWN, suppress any subsequent SDL_TEXTINPUT
|
mi,
|
||||||
// for this keystroke to avoid double insertion on platforms that emit it.
|
suppress_req);
|
||||||
if (produced && mi.hasCommand && mi.id == CommandId::InsertText && mi.arg == "\t") {
|
if (suppress_req) {
|
||||||
suppress_text_input_once_ = true;
|
// Prevent the corresponding TEXTINPUT from delivering the same digit again
|
||||||
}
|
|
||||||
|
|
||||||
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
|
||||||
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
|
||||||
if (produced && mi.hasCommand && mi.id == CommandId::UArgStatus) {
|
|
||||||
// Digits without shift, or a plain '-'
|
|
||||||
const bool is_digit_key = (key >= SDLK_0 && key <= SDLK_9) && !(mods & KMOD_SHIFT);
|
|
||||||
const bool is_minus_key = (key == SDLK_MINUS);
|
|
||||||
if (uarg_active_ && uarg_collecting_ &&(is_digit_key || is_minus_key)) {
|
|
||||||
suppress_text_input_once_ = true;
|
suppress_text_input_once_ = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Note: Do NOT suppress SDL_TEXTINPUT after inserting a TAB. Most platforms
|
||||||
|
// do not emit TEXTINPUT for Tab, and suppressing here would incorrectly
|
||||||
|
// eat the next character typed if no TEXTINPUT follows the Tab press.
|
||||||
|
|
||||||
|
// If we just consumed a universal-argument digit or '-' on KEYDOWN and emitted UArgStatus,
|
||||||
|
// suppress the subsequent SDL_TEXTINPUT for this keystroke to avoid duplicating the digit in status.
|
||||||
|
// Additional suppression handled above when KEYDOWN consumed a uarg digit
|
||||||
|
|
||||||
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
|
// Suppress the immediate following SDL_TEXTINPUT when a printable KEYDOWN was used as a
|
||||||
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
|
// k-prefix suffix or Meta (Alt/ESC) chord, regardless of whether a command was produced.
|
||||||
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
const bool is_alt = (mods & (KMOD_ALT | KMOD_LALT | KMOD_RALT)) != 0;
|
||||||
@@ -404,7 +403,8 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
// Alt/Meta + letter can also generate TEXTINPUT on some platforms
|
||||||
const bool is_meta_symbol = (
|
const bool is_meta_symbol = (
|
||||||
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key == SDLK_GREATER);
|
key == SDLK_COMMA || key == SDLK_PERIOD || key == SDLK_LESS || key ==
|
||||||
|
SDLK_GREATER);
|
||||||
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
|
if (is_alt && ((key >= SDLK_a && key <= SDLK_z) || is_meta_symbol)) {
|
||||||
should_suppress = true;
|
should_suppress = true;
|
||||||
}
|
}
|
||||||
@@ -428,35 +428,26 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// If universal argument collection is active, consume digit/minus TEXTINPUT
|
// If editor universal argument is active, consume digit TEXTINPUT
|
||||||
if (uarg_active_ && uarg_collecting_) {
|
if (ed_ &&ed_
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
->
|
||||||
|
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]);
|
||||||
if (c0 >= '0' && c0 <= '9') {
|
if (c0 >= '0' && c0 <= '9') {
|
||||||
int d = c0 - '0';
|
int d = c0 - '0';
|
||||||
if (!uarg_had_digits_) {
|
ed_->UArgDigit(d);
|
||||||
uarg_value_ = 0;
|
produced = true; // consumed to update status
|
||||||
uarg_had_digits_ = true;
|
|
||||||
}
|
|
||||||
if (uarg_value_ < 100000000) {
|
|
||||||
uarg_value_ = uarg_value_ * 10 + d;
|
|
||||||
}
|
|
||||||
uarg_text_.push_back(static_cast<char>(c0));
|
|
||||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
|
||||||
produced = true; // consumed and enqueued status update
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
if (c0 == '-' && !uarg_had_digits_ && !uarg_negative_) {
|
|
||||||
uarg_negative_ = true;
|
|
||||||
uarg_text_ = "-";
|
|
||||||
mi = {true, CommandId::UArgStatus, uarg_text_, 0};
|
|
||||||
produced = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// End collection and allow this TEXTINPUT to be processed normally below
|
// Non-digit ends collection; allow processing normally below
|
||||||
uarg_collecting_ = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
|
// If we are still in k-prefix and KEYDOWN path didn't handle the suffix,
|
||||||
@@ -472,9 +463,21 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
ascii_key = static_cast<int>(c0);
|
ascii_key = static_cast<int>(c0);
|
||||||
}
|
}
|
||||||
if (ascii_key != 0) {
|
if (ascii_key != 0) {
|
||||||
|
// Qualifier via TEXTINPUT: 'C' or '^'
|
||||||
|
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||||
|
k_ctrl_pending_ = true;
|
||||||
|
if (ed_)
|
||||||
|
ed_->SetStatus("C-k C _");
|
||||||
|
// Keep k-prefix active; do not emit a command
|
||||||
|
k_prefix_ = true;
|
||||||
|
produced = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
|
// Map via k-prefix table; do not pass Ctrl for TEXTINPUT case
|
||||||
CommandId id;
|
CommandId id;
|
||||||
bool mapped = KLookupKCommand(ascii_key, false, id);
|
bool pass_ctrl = k_ctrl_pending_;
|
||||||
|
k_ctrl_pending_ = false;
|
||||||
|
bool mapped = KLookupKCommand(ascii_key, pass_ctrl, id);
|
||||||
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
// Diagnostics: log any k-prefix TEXTINPUT suffix mapping
|
||||||
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
char disp = (ascii_key >= 0x20 && ascii_key <= 0x7e)
|
||||||
? static_cast<char>(ascii_key)
|
? static_cast<char>(ascii_key)
|
||||||
@@ -485,7 +488,9 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
mapped ? static_cast<int>(id) : -1);
|
mapped ? static_cast<int>(id) : -1);
|
||||||
std::fflush(stderr);
|
std::fflush(stderr);
|
||||||
if (mapped) {
|
if (mapped) {
|
||||||
mi = {true, id, "", 0};
|
mi = {true, id, "", 0};
|
||||||
|
if (ed_)
|
||||||
|
ed_->SetStatus(""); // clear "C-k _" hint after suffix
|
||||||
produced = true;
|
produced = true;
|
||||||
break; // handled; do not insert text
|
break; // handled; do not insert text
|
||||||
} else {
|
} else {
|
||||||
@@ -495,13 +500,18 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
? static_cast<char>(shown)
|
? static_cast<char>(shown)
|
||||||
: '?';
|
: '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
mi = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
|
if (ed_)
|
||||||
|
ed_->SetStatus("");
|
||||||
produced = true;
|
produced = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Consume even if no usable ascii was found
|
// If no usable ASCII was found, still report an unknown k-command and exit k-mode
|
||||||
|
mi = {true, CommandId::UnknownKCommand, std::string("?"), 0};
|
||||||
|
if (ed_)
|
||||||
|
ed_->SetStatus("");
|
||||||
produced = true;
|
produced = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -541,7 +551,8 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// If we get here, swallow the TEXTINPUT (do not insert stray char)
|
// If we get here, unmapped ESC sequence via TEXTINPUT: report invalid
|
||||||
|
mi = {true, CommandId::UnknownEscCommand, "", 0};
|
||||||
produced = true;
|
produced = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -571,31 +582,6 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (produced && mi.hasCommand) {
|
if (produced && mi.hasCommand) {
|
||||||
// Attach universal-argument count if present, then clear the state
|
|
||||||
if (uarg_active_ &&mi
|
|
||||||
|
|
||||||
.
|
|
||||||
id != CommandId::UArgStatus
|
|
||||||
)
|
|
||||||
{
|
|
||||||
int count = 0;
|
|
||||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
|
||||||
// No explicit digits: use current value (default 4 or 4^n)
|
|
||||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
|
||||||
} else {
|
|
||||||
count = uarg_value_;
|
|
||||||
if (uarg_negative_)
|
|
||||||
count = -count;
|
|
||||||
}
|
|
||||||
mi.count = count;
|
|
||||||
// Clear universal-argument state after applying it
|
|
||||||
uarg_active_ = false;
|
|
||||||
uarg_collecting_ = false;
|
|
||||||
uarg_negative_ = false;
|
|
||||||
uarg_had_digits_ = false;
|
|
||||||
uarg_value_ = 0;
|
|
||||||
uarg_text_.clear();
|
|
||||||
}
|
|
||||||
std::lock_guard<std::mutex> lk(mu_);
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
q_.push(mi);
|
q_.push(mi);
|
||||||
}
|
}
|
||||||
@@ -604,7 +590,7 @@ GUIInputHandler::ProcessSDLEvent(const SDL_Event &e)
|
|||||||
|
|
||||||
|
|
||||||
bool
|
bool
|
||||||
GUIInputHandler::Poll(MappedInput &out)
|
ImGuiInputHandler::Poll(MappedInput &out)
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lk(mu_);
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
if (q_.empty())
|
if (q_.empty())
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* GUIInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
* ImGuiInputHandler - ImGui/SDL2-based input mapping for GUI mode
|
||||||
*/
|
*/
|
||||||
#pragma once
|
#pragma once
|
||||||
#include <mutex>
|
#include <mutex>
|
||||||
@@ -10,11 +10,18 @@
|
|||||||
|
|
||||||
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
|
union SDL_Event; // fwd decl to avoid including SDL here (SDL defines SDL_Event as a union)
|
||||||
|
|
||||||
class GUIInputHandler final : public InputHandler {
|
class ImGuiInputHandler final : public InputHandler {
|
||||||
public:
|
public:
|
||||||
GUIInputHandler() = default;
|
ImGuiInputHandler() = default;
|
||||||
|
|
||||||
|
~ImGuiInputHandler() override = default;
|
||||||
|
|
||||||
|
|
||||||
|
void Attach(Editor *ed) override
|
||||||
|
{
|
||||||
|
ed_ = ed;
|
||||||
|
}
|
||||||
|
|
||||||
~GUIInputHandler() override = default;
|
|
||||||
|
|
||||||
// Translate an SDL event to editor command and enqueue if applicable.
|
// Translate an SDL event to editor command and enqueue if applicable.
|
||||||
// Returns true if it produced a mapped command or consumed input.
|
// Returns true if it produced a mapped command or consumed input.
|
||||||
@@ -25,18 +32,13 @@ public:
|
|||||||
private:
|
private:
|
||||||
std::mutex mu_;
|
std::mutex mu_;
|
||||||
std::queue<MappedInput> q_;
|
std::queue<MappedInput> q_;
|
||||||
bool k_prefix_ = false;
|
bool k_prefix_ = false;
|
||||||
|
bool k_ctrl_pending_ = false; // if true, next k-suffix is treated as Ctrl- (qualifier via literal 'C' or '^')
|
||||||
// Treat ESC as a Meta prefix: next key is looked up via ESC keymap
|
// Treat ESC as a Meta prefix: next key is looked up via ESC keymap
|
||||||
bool esc_meta_ = false;
|
bool esc_meta_ = false;
|
||||||
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
// When a printable keydown generated a non-text command, suppress the very next SDL_TEXTINPUT
|
||||||
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
// event produced by SDL for the same keystroke to avoid inserting stray characters.
|
||||||
bool suppress_text_input_once_ = false;
|
bool suppress_text_input_once_ = false;
|
||||||
|
|
||||||
// Universal argument (C-u) state for GUI
|
Editor *ed_ = nullptr; // attached editor for editor-owned uarg handling
|
||||||
bool uarg_active_ = false; // an argument is pending for the next command
|
|
||||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
|
||||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
|
||||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
|
||||||
int uarg_value_ = 0; // current absolute value (>=0)
|
|
||||||
std::string uarg_text_; // raw digits/minus typed for status display
|
|
||||||
};
|
};
|
||||||
@@ -9,7 +9,7 @@
|
|||||||
#include <imgui.h>
|
#include <imgui.h>
|
||||||
#include <regex>
|
#include <regex>
|
||||||
|
|
||||||
#include "GUIRenderer.h"
|
#include "ImGuiRenderer.h"
|
||||||
#include "Highlight.h"
|
#include "Highlight.h"
|
||||||
#include "GUITheme.h"
|
#include "GUITheme.h"
|
||||||
#include "Buffer.h"
|
#include "Buffer.h"
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
|
|
||||||
void
|
void
|
||||||
GUIRenderer::Draw(Editor &ed)
|
ImGuiRenderer::Draw(Editor &ed)
|
||||||
{
|
{
|
||||||
// Make the editor window occupy the entire GUI container/viewport
|
// Make the editor window occupy the entire GUI container/viewport
|
||||||
ImGuiViewport *vp = ImGui::GetMainViewport();
|
ImGuiViewport *vp = ImGui::GetMainViewport();
|
||||||
@@ -140,7 +140,8 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
prev_buf_coloffs = buf_coloffs;
|
prev_buf_coloffs = buf_coloffs;
|
||||||
|
|
||||||
// Synchronize cursor and scrolling.
|
// Synchronize cursor and scrolling.
|
||||||
// Ensure the cursor is visible even on the first frame or when it didn't move.
|
// Ensure the cursor is visible, but avoid aggressive centering so that
|
||||||
|
// the same lines remain visible until the cursor actually goes off-screen.
|
||||||
{
|
{
|
||||||
// Compute visible row range using the child window height
|
// Compute visible row range using the child window height
|
||||||
float child_h = ImGui::GetWindowHeight();
|
float child_h = ImGui::GetWindowHeight();
|
||||||
@@ -151,15 +152,30 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
long last_row = first_row + vis_rows - 1;
|
long last_row = first_row + vis_rows - 1;
|
||||||
|
|
||||||
long cyr = static_cast<long>(cy);
|
long cyr = static_cast<long>(cy);
|
||||||
if (cyr < first_row || cyr > last_row) {
|
if (cyr < first_row) {
|
||||||
float target = (static_cast<float>(cyr) - std::max(0L, vis_rows / 2)) * row_h;
|
// Scroll just enough to bring the cursor line to the top
|
||||||
|
float target = static_cast<float>(cyr) * row_h;
|
||||||
|
if (target < 0.f)
|
||||||
|
target = 0.f;
|
||||||
|
float max_y = ImGui::GetScrollMaxY();
|
||||||
|
if (max_y >= 0.f && target > max_y)
|
||||||
|
target = max_y;
|
||||||
|
ImGui::SetScrollY(target);
|
||||||
|
scroll_y = ImGui::GetScrollY();
|
||||||
|
first_row = static_cast<long>(scroll_y / row_h);
|
||||||
|
last_row = first_row + vis_rows - 1;
|
||||||
|
} else if (cyr > last_row) {
|
||||||
|
// Scroll just enough to bring the cursor line to the bottom
|
||||||
|
long new_first = cyr - vis_rows + 1;
|
||||||
|
if (new_first < 0)
|
||||||
|
new_first = 0;
|
||||||
|
float target = static_cast<float>(new_first) * row_h;
|
||||||
float max_y = ImGui::GetScrollMaxY();
|
float max_y = ImGui::GetScrollMaxY();
|
||||||
if (target < 0.f)
|
if (target < 0.f)
|
||||||
target = 0.f;
|
target = 0.f;
|
||||||
if (max_y >= 0.f && target > max_y)
|
if (max_y >= 0.f && target > max_y)
|
||||||
target = max_y;
|
target = max_y;
|
||||||
ImGui::SetScrollY(target);
|
ImGui::SetScrollY(target);
|
||||||
// refresh local variables
|
|
||||||
scroll_y = ImGui::GetScrollY();
|
scroll_y = ImGui::GetScrollY();
|
||||||
first_row = static_cast<long>(scroll_y / row_h);
|
first_row = static_cast<long>(scroll_y / row_h);
|
||||||
last_row = first_row + vis_rows - 1;
|
last_row = first_row + vis_rows - 1;
|
||||||
@@ -369,8 +385,34 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
|
|
||||||
// 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()) {
|
||||||
const kte::LineHighlight &lh = buf->Highlighter()->GetLine(
|
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
||||||
*buf, static_cast<int>(i), buf->Version());
|
*buf, static_cast<int>(i), buf->Version());
|
||||||
|
// Sanitize spans defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
||||||
|
struct SSpan {
|
||||||
|
std::size_t s;
|
||||||
|
std::size_t e;
|
||||||
|
kte::TokenKind k;
|
||||||
|
};
|
||||||
|
std::vector<SSpan> spans;
|
||||||
|
spans.reserve(lh.spans.size());
|
||||||
|
const std::size_t line_len = line.size();
|
||||||
|
for (const auto &sp: lh.spans) {
|
||||||
|
int s_raw = sp.col_start;
|
||||||
|
int e_raw = sp.col_end;
|
||||||
|
if (e_raw < s_raw)
|
||||||
|
std::swap(e_raw, s_raw);
|
||||||
|
std::size_t s = static_cast<std::size_t>(std::max(
|
||||||
|
0, std::min(s_raw, static_cast<int>(line_len))));
|
||||||
|
std::size_t e = static_cast<std::size_t>(std::max(
|
||||||
|
static_cast<int>(s), std::min(e_raw, static_cast<int>(line_len))));
|
||||||
|
if (e <= s)
|
||||||
|
continue;
|
||||||
|
spans.push_back(SSpan{s, e, sp.kind});
|
||||||
|
}
|
||||||
|
std::sort(spans.begin(), spans.end(), [](const SSpan &a, const SSpan &b) {
|
||||||
|
return a.s < b.s;
|
||||||
|
});
|
||||||
|
|
||||||
// Helper to convert a src column to expanded rx position
|
// Helper to convert a src column to expanded rx position
|
||||||
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||||
std::size_t rx = 0;
|
std::size_t rx = 0;
|
||||||
@@ -379,24 +421,22 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
return rx;
|
return rx;
|
||||||
};
|
};
|
||||||
for (const auto &sp: lh.spans) {
|
|
||||||
std::size_t rx_s = src_to_rx_full(
|
for (const auto &sp: spans) {
|
||||||
static_cast<std::size_t>(std::max(0, sp.col_start)));
|
std::size_t rx_s = src_to_rx_full(sp.s);
|
||||||
std::size_t rx_e = src_to_rx_full(
|
std::size_t rx_e = src_to_rx_full(sp.e);
|
||||||
static_cast<std::size_t>(std::max(sp.col_start, sp.col_end)));
|
|
||||||
if (rx_e <= coloffs_now)
|
if (rx_e <= coloffs_now)
|
||||||
continue;
|
continue; // fully left of viewport
|
||||||
// Clamp rx_s/rx_e to the visible portion
|
// Clamp to visible portion and expanded length
|
||||||
std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now;
|
std::size_t draw_start = (rx_s > coloffs_now) ? rx_s : coloffs_now;
|
||||||
std::size_t draw_end = rx_e;
|
|
||||||
if (draw_start >= expanded.size())
|
if (draw_start >= expanded.size())
|
||||||
continue;
|
continue; // fully right of expanded text
|
||||||
draw_end = std::min<std::size_t>(draw_end, 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 is relative to coloffs_now
|
||||||
std::size_t screen_x = draw_start - coloffs_now;
|
std::size_t screen_x = draw_start - coloffs_now;
|
||||||
ImU32 col = ImGui::GetColorU32(kte::SyntaxInk(sp.kind));
|
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 + static_cast<float>(screen_x) * space_w,
|
||||||
line_pos.y);
|
line_pos.y);
|
||||||
ImGui::GetWindowDrawList()->AddText(
|
ImGui::GetWindowDrawList()->AddText(
|
||||||
@@ -431,7 +471,19 @@ GUIRenderer::Draw(Editor &ed)
|
|||||||
}
|
}
|
||||||
// Convert to viewport x by subtracting horizontal col offset
|
// Convert to viewport x by subtracting horizontal col offset
|
||||||
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
|
std::size_t rx_viewport = (rx_abs > coloffs_now) ? (rx_abs - coloffs_now) : 0;
|
||||||
ImVec2 p0 = ImVec2(line_pos.x + static_cast<float>(rx_viewport) * space_w, line_pos.y);
|
// 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 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
ImVec2 p1 = ImVec2(p0.x + space_w, p0.y + line_h);
|
||||||
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
ImU32 col = IM_COL32(200, 200, 255, 128); // soft highlight
|
||||||
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
ImGui::GetWindowDrawList()->AddRectFilled(p0, p1, col);
|
||||||
14
ImGuiRenderer.h
Normal file
14
ImGuiRenderer.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
/*
|
||||||
|
* ImGuiRenderer - ImGui-based renderer for GUI mode
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
#include "Renderer.h"
|
||||||
|
|
||||||
|
class ImGuiRenderer final : public Renderer {
|
||||||
|
public:
|
||||||
|
ImGuiRenderer() = default;
|
||||||
|
|
||||||
|
~ImGuiRenderer() override = default;
|
||||||
|
|
||||||
|
void Draw(Editor &ed) override;
|
||||||
|
};
|
||||||
@@ -6,6 +6,8 @@
|
|||||||
|
|
||||||
#include "Command.h"
|
#include "Command.h"
|
||||||
|
|
||||||
|
class Editor; // fwd decl
|
||||||
|
|
||||||
|
|
||||||
// Result of translating raw input into an editor command.
|
// Result of translating raw input into an editor command.
|
||||||
struct MappedInput {
|
struct MappedInput {
|
||||||
@@ -19,6 +21,10 @@ class InputHandler {
|
|||||||
public:
|
public:
|
||||||
virtual ~InputHandler() = default;
|
virtual ~InputHandler() = default;
|
||||||
|
|
||||||
|
// Optional: attach current Editor so handlers can consult editor state (e.g., universal argument)
|
||||||
|
// Default implementation does nothing.
|
||||||
|
virtual void Attach(Editor *) {}
|
||||||
|
|
||||||
// Poll for input and translate it to a command. Non-blocking.
|
// Poll for input and translate it to a command. Non-blocking.
|
||||||
// Returns true if a command is available in 'out'. Returns false if no input.
|
// Returns true if a command is available in 'out'. Returns false if no input.
|
||||||
virtual bool Poll(MappedInput &out) = 0;
|
virtual bool Poll(MappedInput &out) = 0;
|
||||||
|
|||||||
@@ -42,6 +42,9 @@ KLookupKCommand(const int ascii_key, const bool ctrl, CommandId &out) -> bool
|
|||||||
case 'a':
|
case 'a':
|
||||||
out = CommandId::MarkAllAndJumpEnd;
|
out = CommandId::MarkAllAndJumpEnd;
|
||||||
return true;
|
return true;
|
||||||
|
case 'k':
|
||||||
|
out = CommandId::CenterOnCursor; // C-k k center current line
|
||||||
|
return true;
|
||||||
case 'b':
|
case 'b':
|
||||||
out = CommandId::BufferSwitchStart;
|
out = CommandId::BufferSwitchStart;
|
||||||
return true;
|
return true;
|
||||||
@@ -215,4 +218,4 @@ KLookupEscCommand(const int ascii_key, CommandId &out) -> bool
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
988
QtFrontend.cc
Normal file
988
QtFrontend.cc
Normal file
@@ -0,0 +1,988 @@
|
|||||||
|
#include "QtFrontend.h"
|
||||||
|
|
||||||
|
#include <QApplication>
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QKeyEvent>
|
||||||
|
#include <QTimer>
|
||||||
|
#include <QScreen>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
#include <QFontDatabase>
|
||||||
|
#include <QFileDialog>
|
||||||
|
#include <QFontDialog>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QWheelEvent>
|
||||||
|
#include <regex>
|
||||||
|
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "Command.h"
|
||||||
|
#include "Buffer.h"
|
||||||
|
#include "GUITheme.h"
|
||||||
|
#include "Highlight.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class MainWindow : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit MainWindow(class QtInputHandler &ih, QWidget *parent = nullptr)
|
||||||
|
: QWidget(parent), input_(ih)
|
||||||
|
{
|
||||||
|
// Match ImGui window title format
|
||||||
|
setWindowTitle(QStringLiteral("kge - kyle's graphical editor ")
|
||||||
|
+ QStringLiteral(KTE_VERSION_STR));
|
||||||
|
resize(1280, 800);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool WasClosed() const
|
||||||
|
{
|
||||||
|
return closed_;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetEditor(Editor *ed)
|
||||||
|
{
|
||||||
|
ed_ = ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetFontFamilyAndSize(QString family, int px)
|
||||||
|
{
|
||||||
|
if (family.isEmpty())
|
||||||
|
family = QStringLiteral("Brass Mono");
|
||||||
|
if (px <= 0)
|
||||||
|
px = 18;
|
||||||
|
font_family_ = std::move(family);
|
||||||
|
font_px_ = px;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void keyPressEvent(QKeyEvent *event) override
|
||||||
|
{
|
||||||
|
// Route to editor keymap; if handled, accept and stop propagation so
|
||||||
|
// Qt doesn't trigger any default widget shortcuts.
|
||||||
|
if (input_.ProcessKeyEvent(*event)) {
|
||||||
|
event->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
QWidget::keyPressEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void paintEvent(QPaintEvent *event) override
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
QPainter p(this);
|
||||||
|
p.setRenderHint(QPainter::TextAntialiasing, true);
|
||||||
|
|
||||||
|
// Colors from GUITheme palette (Qt branch)
|
||||||
|
auto to_qcolor = [](const KteColor &c) -> QColor {
|
||||||
|
int r = int(std::round(c.x * 255.0f));
|
||||||
|
int g = int(std::round(c.y * 255.0f));
|
||||||
|
int b = int(std::round(c.z * 255.0f));
|
||||||
|
int a = int(std::round(c.w * 255.0f));
|
||||||
|
return QColor(r, g, b, a);
|
||||||
|
};
|
||||||
|
const auto pal = kte::GetPalette();
|
||||||
|
const QColor bg = to_qcolor(pal.bg);
|
||||||
|
const QColor fg = to_qcolor(pal.fg);
|
||||||
|
const QColor sel_bg = to_qcolor(pal.sel_bg);
|
||||||
|
const QColor cur_bg = to_qcolor(pal.cur_bg);
|
||||||
|
const QColor status_bg = to_qcolor(pal.status_bg);
|
||||||
|
const QColor status_fg = to_qcolor(pal.status_fg);
|
||||||
|
|
||||||
|
// Background
|
||||||
|
p.fillRect(rect(), bg);
|
||||||
|
|
||||||
|
// Font/metrics (configured or defaults)
|
||||||
|
QFont f(font_family_, font_px_);
|
||||||
|
p.setFont(f);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = fm.height();
|
||||||
|
const int ch_w = std::max(1, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||||
|
|
||||||
|
// Layout metrics
|
||||||
|
const int pad_l = 8;
|
||||||
|
const int pad_t = 6;
|
||||||
|
const int pad_r = 8;
|
||||||
|
const int pad_b = 6;
|
||||||
|
const int status_h = line_h + 6; // status bar height
|
||||||
|
|
||||||
|
// Content area (text viewport)
|
||||||
|
const QRect content_rect(pad_l,
|
||||||
|
pad_t,
|
||||||
|
width() - pad_l - pad_r,
|
||||||
|
height() - pad_t - pad_b - status_h);
|
||||||
|
|
||||||
|
// Text viewport occupies all content area (no extra title row)
|
||||||
|
QRect viewport(content_rect.x(), content_rect.y(), content_rect.width(), content_rect.height());
|
||||||
|
|
||||||
|
// Draw buffer contents
|
||||||
|
if (ed_ && viewport.height() > 0 && viewport.width() > 0) {
|
||||||
|
const Buffer *buf = ed_->CurrentBuffer();
|
||||||
|
if (buf) {
|
||||||
|
const auto &lines = buf->Rows();
|
||||||
|
const std::size_t nrows = lines.size();
|
||||||
|
const std::size_t rowoffs = buf->Rowoffs();
|
||||||
|
const std::size_t coloffs = buf->Coloffs();
|
||||||
|
const std::size_t cy = buf->Cury();
|
||||||
|
const std::size_t cx = buf->Curx();
|
||||||
|
|
||||||
|
// Visible line count
|
||||||
|
const int max_lines = (line_h > 0) ? (viewport.height() / line_h) : 0;
|
||||||
|
const std::size_t last_row = std::min<std::size_t>(
|
||||||
|
nrows, rowoffs + std::max(0, max_lines));
|
||||||
|
|
||||||
|
// Tab width: follow ImGuiRenderer default of 4
|
||||||
|
const std::size_t tabw = 4;
|
||||||
|
|
||||||
|
// Prepare painter clip to viewport
|
||||||
|
p.save();
|
||||||
|
p.setClipRect(viewport);
|
||||||
|
|
||||||
|
// Iterate visible lines
|
||||||
|
for (std::size_t i = rowoffs, vis_idx = 0; i < last_row; ++i, ++vis_idx) {
|
||||||
|
const auto &line = static_cast<const std::string &>(lines[i]);
|
||||||
|
const int y = viewport.y() + static_cast<int>(vis_idx) * line_h;
|
||||||
|
const int baseline = y + fm.ascent();
|
||||||
|
|
||||||
|
// Helper: convert src col -> rx with tab expansion
|
||||||
|
auto src_to_rx_line = [&](std::size_t src_col) -> std::size_t {
|
||||||
|
std::size_t rx = 0;
|
||||||
|
for (std::size_t k = 0; k < src_col && k < line.size(); ++k) {
|
||||||
|
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||||
|
}
|
||||||
|
return rx;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Search-match background highlights first (under text)
|
||||||
|
if (ed_->SearchActive() && !ed_->SearchQuery().empty()) {
|
||||||
|
std::vector<std::pair<std::size_t, std::size_t> > hl_src_ranges;
|
||||||
|
// Compute ranges per line (source indices)
|
||||||
|
if (ed_->PromptActive() &&
|
||||||
|
(ed_->CurrentPromptKind() == Editor::PromptKind::RegexSearch ||
|
||||||
|
ed_->CurrentPromptKind() ==
|
||||||
|
Editor::PromptKind::RegexReplaceFind)) {
|
||||||
|
try {
|
||||||
|
std::regex rx(ed_->SearchQuery());
|
||||||
|
for (auto it = std::sregex_iterator(
|
||||||
|
line.begin(), line.end(), rx);
|
||||||
|
it != std::sregex_iterator(); ++it) {
|
||||||
|
const auto &m = *it;
|
||||||
|
std::size_t sx = static_cast<std::size_t>(m.
|
||||||
|
position());
|
||||||
|
std::size_t ex =
|
||||||
|
sx + static_cast<std::size_t>(m.
|
||||||
|
length());
|
||||||
|
hl_src_ranges.emplace_back(sx, ex);
|
||||||
|
}
|
||||||
|
} catch (const std::regex_error &) {
|
||||||
|
// Invalid regex: ignore, status line already shows errors
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const std::string &q = ed_->SearchQuery();
|
||||||
|
if (!q.empty()) {
|
||||||
|
std::size_t pos = 0;
|
||||||
|
while ((pos = line.find(q, pos)) != std::string::npos) {
|
||||||
|
hl_src_ranges.emplace_back(pos, pos + q.size());
|
||||||
|
pos += q.size();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hl_src_ranges.empty()) {
|
||||||
|
const bool has_current =
|
||||||
|
ed_->SearchMatchLen() > 0 && ed_->SearchMatchY() == i;
|
||||||
|
const std::size_t cur_x = has_current ? ed_->SearchMatchX() : 0;
|
||||||
|
const std::size_t cur_end = has_current
|
||||||
|
? (ed_->SearchMatchX() + ed_->SearchMatchLen())
|
||||||
|
: 0;
|
||||||
|
for (const auto &rg: hl_src_ranges) {
|
||||||
|
std::size_t sx = rg.first, ex = rg.second;
|
||||||
|
std::size_t rx_s = src_to_rx_line(sx);
|
||||||
|
std::size_t rx_e = src_to_rx_line(ex);
|
||||||
|
if (rx_e <= coloffs)
|
||||||
|
continue; // fully left of view
|
||||||
|
int vx0 = viewport.x() + static_cast<int>((
|
||||||
|
(rx_s > coloffs ? rx_s - coloffs : 0)
|
||||||
|
* ch_w));
|
||||||
|
int vx1 = viewport.x() + static_cast<int>((
|
||||||
|
(rx_e - coloffs) * ch_w));
|
||||||
|
QRect r(vx0, y, std::max(0, vx1 - vx0), line_h);
|
||||||
|
if (r.width() <= 0)
|
||||||
|
continue;
|
||||||
|
bool is_current =
|
||||||
|
has_current && sx == cur_x && ex == cur_end;
|
||||||
|
QColor col = is_current
|
||||||
|
? QColor(255, 220, 120, 140)
|
||||||
|
: QColor(200, 200, 0, 90);
|
||||||
|
p.fillRect(r, col);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Selection background (if active on this line)
|
||||||
|
if (buf->MarkSet() && (
|
||||||
|
i == buf->MarkCury() || i == cy || (
|
||||||
|
i > std::min(buf->MarkCury(), cy) && i < std::max(
|
||||||
|
buf->MarkCury(), cy)))) {
|
||||||
|
std::size_t sx = 0, ex = 0;
|
||||||
|
if (buf->MarkCury() == i && cy == i) {
|
||||||
|
sx = std::min(buf->MarkCurx(), cx);
|
||||||
|
ex = std::max(buf->MarkCurx(), cx);
|
||||||
|
} else if (i == buf->MarkCury()) {
|
||||||
|
sx = buf->MarkCurx();
|
||||||
|
ex = line.size();
|
||||||
|
} else if (i == cy) {
|
||||||
|
sx = 0;
|
||||||
|
ex = cx;
|
||||||
|
} else {
|
||||||
|
sx = 0;
|
||||||
|
ex = line.size();
|
||||||
|
}
|
||||||
|
std::size_t rx_s = src_to_rx_line(sx);
|
||||||
|
std::size_t rx_e = src_to_rx_line(ex);
|
||||||
|
if (rx_e > coloffs) {
|
||||||
|
int vx0 = viewport.x() + static_cast<int>((rx_s > coloffs
|
||||||
|
? rx_s - coloffs
|
||||||
|
: 0) * ch_w);
|
||||||
|
int vx1 = viewport.x() + static_cast<int>(
|
||||||
|
(rx_e - coloffs) * ch_w);
|
||||||
|
QRect sel_r(vx0, y, std::max(0, vx1 - vx0), line_h);
|
||||||
|
if (sel_r.width() > 0)
|
||||||
|
p.fillRect(sel_r, sel_bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build expanded line (tabs -> spaces) for drawing
|
||||||
|
std::string expanded;
|
||||||
|
expanded.reserve(line.size() + 8);
|
||||||
|
std::size_t rx_acc = 0;
|
||||||
|
for (char c: line) {
|
||||||
|
if (c == '\t') {
|
||||||
|
std::size_t adv = (tabw - (rx_acc % tabw));
|
||||||
|
expanded.append(adv, ' ');
|
||||||
|
rx_acc += adv;
|
||||||
|
} else {
|
||||||
|
expanded.push_back(c);
|
||||||
|
rx_acc += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Syntax highlighting spans or plain text
|
||||||
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||||
|
HasHighlighter()) {
|
||||||
|
kte::LineHighlight lh = buf->Highlighter()->GetLine(
|
||||||
|
*buf, static_cast<int>(i), buf->Version());
|
||||||
|
struct SSpan {
|
||||||
|
std::size_t s;
|
||||||
|
std::size_t e;
|
||||||
|
kte::TokenKind k;
|
||||||
|
};
|
||||||
|
std::vector<SSpan> spans;
|
||||||
|
spans.reserve(lh.spans.size());
|
||||||
|
const std::size_t line_len = line.size();
|
||||||
|
for (const auto &sp: lh.spans) {
|
||||||
|
int s_raw = sp.col_start;
|
||||||
|
int e_raw = sp.col_end;
|
||||||
|
if (e_raw < s_raw)
|
||||||
|
std::swap(e_raw, s_raw);
|
||||||
|
std::size_t s = static_cast<std::size_t>(std::max(
|
||||||
|
0, std::min(s_raw, (int) line_len)));
|
||||||
|
std::size_t e = static_cast<std::size_t>(std::max(
|
||||||
|
(int) s, std::min(e_raw, (int) line_len)));
|
||||||
|
if (s < e)
|
||||||
|
spans.push_back({s, e, sp.kind});
|
||||||
|
}
|
||||||
|
std::sort(spans.begin(), spans.end(),
|
||||||
|
[](const SSpan &a, const SSpan &b) {
|
||||||
|
return a.s < b.s;
|
||||||
|
});
|
||||||
|
|
||||||
|
auto colorFor = [](kte::TokenKind k) -> QColor {
|
||||||
|
// GUITheme provides colors via ImGui vector; avoid direct dependency types
|
||||||
|
const auto v = kte::SyntaxInk(k);
|
||||||
|
return QColor(int(v.x * 255.0f), int(v.y * 255.0f),
|
||||||
|
int(v.z * 255.0f), int(v.w * 255.0f));
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helper to convert src col to expanded rx
|
||||||
|
auto src_to_rx_full = [&](std::size_t sidx) -> std::size_t {
|
||||||
|
std::size_t rx = 0;
|
||||||
|
for (std::size_t k = 0; k < sidx && k < line.size(); ++k) {
|
||||||
|
rx += (line[k] == '\t') ? (tabw - (rx % tabw)) : 1;
|
||||||
|
}
|
||||||
|
return rx;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (spans.empty()) {
|
||||||
|
// No highlight spans: draw the whole (visible) expanded line in default fg
|
||||||
|
if (coloffs < expanded.size()) {
|
||||||
|
const char *start =
|
||||||
|
expanded.c_str() + static_cast<int>(coloffs);
|
||||||
|
p.setPen(fg);
|
||||||
|
p.drawText(viewport.x(), baseline,
|
||||||
|
QString::fromUtf8(start));
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw colored spans
|
||||||
|
for (const auto &sp: spans) {
|
||||||
|
std::size_t rx_s = src_to_rx_full(sp.s);
|
||||||
|
std::size_t rx_e = src_to_rx_full(sp.e);
|
||||||
|
if (rx_e <= coloffs)
|
||||||
|
continue; // left of viewport
|
||||||
|
std::size_t draw_start = (rx_s > coloffs)
|
||||||
|
? rx_s
|
||||||
|
: coloffs;
|
||||||
|
std::size_t draw_end = std::min<std::size_t>(
|
||||||
|
rx_e, expanded.size());
|
||||||
|
if (draw_end <= draw_start)
|
||||||
|
continue;
|
||||||
|
std::size_t screen_x = draw_start - coloffs;
|
||||||
|
int px = viewport.x() + int(screen_x * ch_w);
|
||||||
|
int len = int(draw_end - draw_start);
|
||||||
|
p.setPen(colorFor(sp.k));
|
||||||
|
p.drawText(px, baseline,
|
||||||
|
QString::fromUtf8(
|
||||||
|
expanded.c_str() + draw_start, len));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Draw expanded text clipped by coloffs
|
||||||
|
if (static_cast<std::size_t>(coloffs) < expanded.size()) {
|
||||||
|
const char *start =
|
||||||
|
expanded.c_str() + static_cast<int>(coloffs);
|
||||||
|
p.setPen(fg);
|
||||||
|
p.drawText(viewport.x(), baseline, QString::fromUtf8(start));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor indicator on current line
|
||||||
|
if (i == cy) {
|
||||||
|
std::size_t rx_cur = src_to_rx_line(cx);
|
||||||
|
if (rx_cur >= coloffs) {
|
||||||
|
// Compute exact pixel x by measuring expanded substring [coloffs, rx_cur)
|
||||||
|
std::size_t start = std::min<std::size_t>(
|
||||||
|
coloffs, expanded.size());
|
||||||
|
std::size_t end = std::min<
|
||||||
|
std::size_t>(rx_cur, expanded.size());
|
||||||
|
int px_advance = 0;
|
||||||
|
if (end > start) {
|
||||||
|
const QString sub = QString::fromUtf8(
|
||||||
|
expanded.c_str() + start,
|
||||||
|
static_cast<int>(end - start));
|
||||||
|
px_advance = fm.horizontalAdvance(sub);
|
||||||
|
}
|
||||||
|
int x0 = viewport.x() + px_advance;
|
||||||
|
QRect r(x0, y, ch_w, line_h);
|
||||||
|
p.fillRect(r, cur_bg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Status bar
|
||||||
|
const int bar_y = height() - status_h;
|
||||||
|
QRect status_rect(0, bar_y, width(), status_h);
|
||||||
|
p.fillRect(status_rect, status_bg);
|
||||||
|
p.setPen(status_fg);
|
||||||
|
if (ed_) {
|
||||||
|
const int pad = 6;
|
||||||
|
const int left_x = status_rect.x() + pad;
|
||||||
|
const int right_x_max = status_rect.x() + status_rect.width() - pad;
|
||||||
|
const int baseline_y = bar_y + (status_h + fm.ascent() - fm.descent()) / 2;
|
||||||
|
|
||||||
|
// If a prompt is active, mirror ImGui/TUI: show only the prompt across the bar
|
||||||
|
if (ed_->PromptActive()) {
|
||||||
|
std::string label = ed_->PromptLabel();
|
||||||
|
std::string text = ed_->PromptText();
|
||||||
|
|
||||||
|
// Map $HOME to ~ for path prompts (Open/Save/Chdir)
|
||||||
|
auto kind = ed_->CurrentPromptKind();
|
||||||
|
if (kind == Editor::PromptKind::OpenFile ||
|
||||||
|
kind == Editor::PromptKind::SaveAs ||
|
||||||
|
kind == Editor::PromptKind::Chdir) {
|
||||||
|
const char *home_c = std::getenv("HOME");
|
||||||
|
if (home_c && *home_c) {
|
||||||
|
std::string home(home_c);
|
||||||
|
if (text.rfind(home, 0) == 0) {
|
||||||
|
std::string rest = text.substr(home.size());
|
||||||
|
if (rest.empty())
|
||||||
|
text = "~";
|
||||||
|
else if (!rest.empty() && (rest[0] == '/' || rest[0] == '\\'))
|
||||||
|
text = std::string("~") + rest;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string prefix;
|
||||||
|
if (kind == Editor::PromptKind::Command)
|
||||||
|
prefix = ": ";
|
||||||
|
else if (!label.empty())
|
||||||
|
prefix = label + ": ";
|
||||||
|
|
||||||
|
// Compose text and elide per behavior:
|
||||||
|
const int max_w = status_rect.width() - 2 * pad;
|
||||||
|
QString qprefix = QString::fromStdString(prefix);
|
||||||
|
QString qtext = QString::fromStdString(text);
|
||||||
|
int avail_w = std::max(0, max_w - fm.horizontalAdvance(qprefix));
|
||||||
|
Qt::TextElideMode mode = Qt::ElideRight;
|
||||||
|
if (kind == Editor::PromptKind::OpenFile ||
|
||||||
|
kind == Editor::PromptKind::SaveAs ||
|
||||||
|
kind == Editor::PromptKind::Chdir) {
|
||||||
|
mode = Qt::ElideLeft;
|
||||||
|
}
|
||||||
|
QString shown = fm.elidedText(qtext, mode, avail_w);
|
||||||
|
p.drawText(left_x, baseline_y, qprefix + shown);
|
||||||
|
} else {
|
||||||
|
// Build left segment: app/version, buffer idx/total, filename [+dirty], line count
|
||||||
|
QString left;
|
||||||
|
left += QStringLiteral("kge ");
|
||||||
|
left += QStringLiteral(KTE_VERSION_STR);
|
||||||
|
|
||||||
|
const Buffer *buf = ed_->CurrentBuffer();
|
||||||
|
if (buf) {
|
||||||
|
// buffer index/total
|
||||||
|
std::size_t total = ed_->BufferCount();
|
||||||
|
if (total > 0) {
|
||||||
|
std::size_t idx1 = ed_->CurrentBufferIndex() + 1; // 1-based
|
||||||
|
left += QStringLiteral(" [");
|
||||||
|
left += QString::number(static_cast<qlonglong>(idx1));
|
||||||
|
left += QStringLiteral("/");
|
||||||
|
left += QString::number(static_cast<qlonglong>(total));
|
||||||
|
left += QStringLiteral("] ");
|
||||||
|
} else {
|
||||||
|
left += QStringLiteral(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
// buffer display name
|
||||||
|
std::string disp;
|
||||||
|
try {
|
||||||
|
disp = ed_->DisplayNameFor(*buf);
|
||||||
|
} catch (...) {
|
||||||
|
disp = buf->Filename();
|
||||||
|
}
|
||||||
|
if (disp.empty())
|
||||||
|
disp = "[No Name]";
|
||||||
|
left += QString::fromStdString(disp);
|
||||||
|
if (buf->Dirty())
|
||||||
|
left += QStringLiteral(" *");
|
||||||
|
|
||||||
|
// total lines suffix " <n>L"
|
||||||
|
unsigned long lcount = static_cast<unsigned long>(buf->Rows().size());
|
||||||
|
left += QStringLiteral(" ");
|
||||||
|
left += QString::number(static_cast<qlonglong>(lcount));
|
||||||
|
left += QStringLiteral("L");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build right segment: cursor and mark
|
||||||
|
QString right;
|
||||||
|
if (buf) {
|
||||||
|
int row1 = static_cast<int>(buf->Cury()) + 1;
|
||||||
|
int col1 = static_cast<int>(buf->Curx()) + 1;
|
||||||
|
bool have_mark = buf->MarkSet();
|
||||||
|
int mrow1 = have_mark ? static_cast<int>(buf->MarkCury()) + 1 : 0;
|
||||||
|
int mcol1 = have_mark ? static_cast<int>(buf->MarkCurx()) + 1 : 0;
|
||||||
|
if (have_mark)
|
||||||
|
right = QString("%1,%2 | M: %3,%4").arg(row1).arg(col1).arg(mrow1).arg(
|
||||||
|
mcol1);
|
||||||
|
else
|
||||||
|
right = QString("%1,%2 | M: not set").arg(row1).arg(col1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Middle message: status text
|
||||||
|
QString mid = QString::fromStdString(ed_->Status());
|
||||||
|
|
||||||
|
// Measure and layout
|
||||||
|
int left_w = fm.horizontalAdvance(left);
|
||||||
|
int right_w = fm.horizontalAdvance(right);
|
||||||
|
int lx = left_x;
|
||||||
|
int rx = std::max(left_x, right_x_max - right_w);
|
||||||
|
|
||||||
|
// If overlap, elide left to make space for right
|
||||||
|
if (lx + left_w + pad > rx) {
|
||||||
|
int max_left_w = std::max(0, rx - lx - pad);
|
||||||
|
left = fm.elidedText(left, Qt::ElideRight, max_left_w);
|
||||||
|
left_w = fm.horizontalAdvance(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw left and right
|
||||||
|
p.drawText(lx, baseline_y, left);
|
||||||
|
if (!right.isEmpty())
|
||||||
|
p.drawText(rx, baseline_y, right);
|
||||||
|
|
||||||
|
// Middle message clipped between end of left and start of right
|
||||||
|
int mid_left = lx + left_w + pad;
|
||||||
|
int mid_right = std::max(mid_left, rx - pad);
|
||||||
|
int mid_w = std::max(0, mid_right - mid_left);
|
||||||
|
if (mid_w > 0 && !mid.isEmpty()) {
|
||||||
|
QString mid_show = fm.elidedText(mid, Qt::ElideRight, mid_w);
|
||||||
|
p.save();
|
||||||
|
p.setClipRect(QRect(mid_left, bar_y, mid_w, status_h));
|
||||||
|
p.drawText(mid_left, baseline_y, mid_show);
|
||||||
|
p.restore();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void resizeEvent(QResizeEvent *event) override
|
||||||
|
{
|
||||||
|
QWidget::resizeEvent(event);
|
||||||
|
if (!ed_)
|
||||||
|
return;
|
||||||
|
// Update editor dimensions based on new size
|
||||||
|
QFont f(font_family_, font_px_);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = std::max(12, fm.height());
|
||||||
|
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||||
|
const int pad_l = 8, pad_r = 8, pad_t = 6, pad_b = 6;
|
||||||
|
const int status_h = line_h + 6;
|
||||||
|
const int avail_w = std::max(0, width() - pad_l - pad_r);
|
||||||
|
const int avail_h = std::max(0, height() - pad_t - pad_b - status_h);
|
||||||
|
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h));
|
||||||
|
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||||
|
ed_->SetDimensions(rows, cols);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void wheelEvent(QWheelEvent *event) override
|
||||||
|
{
|
||||||
|
if (!ed_) {
|
||||||
|
QWidget::wheelEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Buffer *buf = ed_->CurrentBuffer();
|
||||||
|
if (!buf) {
|
||||||
|
QWidget::wheelEvent(event);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Recompute metrics to map pixel deltas to rows/cols
|
||||||
|
QFont f(font_family_, font_px_);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = std::max(12, fm.height());
|
||||||
|
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral(" ")));
|
||||||
|
|
||||||
|
// Determine scroll intent: use pixelDelta when available (trackpads), otherwise angleDelta
|
||||||
|
QPoint pixel = event->pixelDelta();
|
||||||
|
QPoint angle = event->angleDelta();
|
||||||
|
|
||||||
|
double v_lines_delta = 0.0;
|
||||||
|
double h_cols_delta = 0.0;
|
||||||
|
|
||||||
|
// Horizontal scroll with Shift or explicit horizontal delta
|
||||||
|
bool horiz_mode = (event->modifiers() & Qt::ShiftModifier) || (!pixel.isNull() && pixel.x() != 0) || (
|
||||||
|
!angle.isNull() && angle.x() != 0);
|
||||||
|
|
||||||
|
if (!pixel.isNull()) {
|
||||||
|
// Trackpad smooth scrolling (pixels)
|
||||||
|
v_lines_delta = -static_cast<double>(pixel.y()) / std::max(1, line_h);
|
||||||
|
h_cols_delta = -static_cast<double>(pixel.x()) / std::max(1, ch_w);
|
||||||
|
} else if (!angle.isNull()) {
|
||||||
|
// Mouse wheel: 120 units per notch; map one notch to 3 lines similar to ImGui UX
|
||||||
|
v_lines_delta = -static_cast<double>(angle.y()) / 120.0 * 3.0;
|
||||||
|
// For horizontal wheels, each notch scrolls 8 columns
|
||||||
|
h_cols_delta = -static_cast<double>(angle.x()) / 120.0 * 8.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Accumulate fractional deltas across events
|
||||||
|
v_scroll_accum_ += v_lines_delta;
|
||||||
|
h_scroll_accum_ += h_cols_delta;
|
||||||
|
|
||||||
|
int d_rows = 0;
|
||||||
|
int d_cols = 0;
|
||||||
|
if (std::fabs(v_scroll_accum_) >= 1.0 && (!horiz_mode || std::fabs(v_scroll_accum_) > std::fabs(
|
||||||
|
h_scroll_accum_))) {
|
||||||
|
d_rows = static_cast<int>(v_scroll_accum_);
|
||||||
|
v_scroll_accum_ -= d_rows;
|
||||||
|
}
|
||||||
|
if (std::fabs(h_scroll_accum_) >= 1.0 && (horiz_mode || std::fabs(h_scroll_accum_) >= std::fabs(
|
||||||
|
v_scroll_accum_))) {
|
||||||
|
d_cols = static_cast<int>(h_scroll_accum_);
|
||||||
|
h_scroll_accum_ -= d_cols;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (d_rows != 0 || d_cols != 0) {
|
||||||
|
std::size_t new_rowoffs = buf->Rowoffs();
|
||||||
|
std::size_t new_coloffs = buf->Coloffs();
|
||||||
|
// Clamp vertical between 0 and last row (leaving at least one visible line)
|
||||||
|
if (d_rows != 0) {
|
||||||
|
long nr = static_cast<long>(new_rowoffs) + d_rows;
|
||||||
|
if (nr < 0)
|
||||||
|
nr = 0;
|
||||||
|
const auto nrows = static_cast<long>(buf->Rows().size());
|
||||||
|
if (nr > std::max(0L, nrows - 1))
|
||||||
|
nr = std::max(0L, nrows - 1);
|
||||||
|
new_rowoffs = static_cast<std::size_t>(nr);
|
||||||
|
}
|
||||||
|
if (d_cols != 0) {
|
||||||
|
long nc = static_cast<long>(new_coloffs) + d_cols;
|
||||||
|
if (nc < 0)
|
||||||
|
nc = 0;
|
||||||
|
new_coloffs = static_cast<std::size_t>(nc);
|
||||||
|
}
|
||||||
|
buf->SetOffsets(new_rowoffs, new_coloffs);
|
||||||
|
update();
|
||||||
|
event->accept();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QWidget::wheelEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void closeEvent(QCloseEvent *event) override
|
||||||
|
{
|
||||||
|
closed_ = true;
|
||||||
|
QWidget::closeEvent(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
QtInputHandler &input_;
|
||||||
|
bool closed_ = false;
|
||||||
|
Editor *ed_ = nullptr;
|
||||||
|
double v_scroll_accum_ = 0.0;
|
||||||
|
double h_scroll_accum_ = 0.0;
|
||||||
|
QString font_family_ = QStringLiteral("Brass Mono");
|
||||||
|
int font_px_ = 18;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
bool
|
||||||
|
GUIFrontend::Init(Editor &ed)
|
||||||
|
{
|
||||||
|
int argc = 0;
|
||||||
|
char **argv = nullptr;
|
||||||
|
app_ = new QApplication(argc, argv);
|
||||||
|
|
||||||
|
window_ = new MainWindow(input_);
|
||||||
|
window_->show();
|
||||||
|
// Ensure the window becomes the active, focused window so it receives key events
|
||||||
|
window_->activateWindow();
|
||||||
|
window_->raise();
|
||||||
|
window_->setFocus(Qt::OtherFocusReason);
|
||||||
|
|
||||||
|
renderer_.Attach(window_);
|
||||||
|
input_.Attach(&ed);
|
||||||
|
if (auto *mw = dynamic_cast<MainWindow *>(window_))
|
||||||
|
mw->SetEditor(&ed);
|
||||||
|
|
||||||
|
// Load GUI configuration (kge.ini) and configure font for Qt
|
||||||
|
config_ = GUIConfig::Load();
|
||||||
|
|
||||||
|
// Apply background mode from config to match ImGui frontend behavior
|
||||||
|
if (config_.background == "light")
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Light);
|
||||||
|
else
|
||||||
|
kte::SetBackgroundMode(kte::BackgroundMode::Dark);
|
||||||
|
|
||||||
|
// Apply theme by name for Qt palette-based theming (maps to named palettes).
|
||||||
|
// If unknown, falls back to the generic light/dark palette.
|
||||||
|
(void) kte::ApplyQtThemeByName(config_.theme);
|
||||||
|
if (window_)
|
||||||
|
window_->update();
|
||||||
|
|
||||||
|
// Map GUIConfig font name to a system family (Qt uses installed fonts)
|
||||||
|
auto choose_family = [](const std::string &name) -> QString {
|
||||||
|
QString fam;
|
||||||
|
std::string n = name;
|
||||||
|
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
if (n.empty() || n == "default" || n == "brassmono" || n == "brassmonocode") {
|
||||||
|
fam = QStringLiteral("Brass Mono");
|
||||||
|
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
|
||||||
|
fam = QStringLiteral("JetBrains Mono");
|
||||||
|
} else if (n == "iosevka") {
|
||||||
|
fam = QStringLiteral("Iosevka");
|
||||||
|
} else if (n == "inconsolata" || n == "inconsolataex") {
|
||||||
|
fam = QStringLiteral("Inconsolata");
|
||||||
|
} else if (n == "space" || n == "spacemono" || n == "space mono") {
|
||||||
|
fam = QStringLiteral("Space Mono");
|
||||||
|
} else if (n == "go") {
|
||||||
|
fam = QStringLiteral("Go Mono");
|
||||||
|
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
|
||||||
|
fam = QStringLiteral("IBM Plex Mono");
|
||||||
|
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
|
||||||
|
fam = QStringLiteral("Fira Code");
|
||||||
|
} else if (!name.empty()) {
|
||||||
|
fam = QString::fromStdString(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate availability; choose a fallback if needed
|
||||||
|
const auto families = QFontDatabase::families();
|
||||||
|
if (!fam.isEmpty() && families.contains(fam)) {
|
||||||
|
return fam;
|
||||||
|
}
|
||||||
|
// Preferred fallback chain on macOS; otherwise, try common monospace families
|
||||||
|
const QStringList fallbacks = {
|
||||||
|
QStringLiteral("Brass Mono"),
|
||||||
|
QStringLiteral("JetBrains Mono"),
|
||||||
|
QStringLiteral("SF Mono"),
|
||||||
|
QStringLiteral("Menlo"),
|
||||||
|
QStringLiteral("Monaco"),
|
||||||
|
QStringLiteral("Courier New"),
|
||||||
|
QStringLiteral("Courier"),
|
||||||
|
QStringLiteral("Monospace")
|
||||||
|
};
|
||||||
|
for (const auto &fb: fallbacks) {
|
||||||
|
if (families.contains(fb))
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
|
// As a last resort, return the request (Qt will substitute)
|
||||||
|
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
|
||||||
|
};
|
||||||
|
|
||||||
|
QString family = choose_family(config_.font);
|
||||||
|
int px_size = (config_.font_size > 0.0f) ? (int) std::lround(config_.font_size) : 18;
|
||||||
|
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||||
|
mw->SetFontFamilyAndSize(family, px_size);
|
||||||
|
}
|
||||||
|
// Track current font in globals for command/status queries
|
||||||
|
kte::gCurrentFontFamily = family.toStdString();
|
||||||
|
kte::gCurrentFontSize = static_cast<float>(px_size);
|
||||||
|
|
||||||
|
// Set initial dimensions based on font metrics
|
||||||
|
QFont f(family, px_size);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = std::max(12, fm.height());
|
||||||
|
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
|
||||||
|
const int w = window_->width();
|
||||||
|
const int h = window_->height();
|
||||||
|
const int pad = 16;
|
||||||
|
const int status_h = line_h + 4;
|
||||||
|
const int avail_w = std::max(0, w - 2 * pad);
|
||||||
|
const int avail_h = std::max(0, h - 2 * pad - status_h);
|
||||||
|
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
|
||||||
|
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||||
|
ed.SetDimensions(rows, cols);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
GUIFrontend::Step(Editor &ed, bool &running)
|
||||||
|
{
|
||||||
|
// Pump Qt events
|
||||||
|
if (app_)
|
||||||
|
app_->processEvents();
|
||||||
|
|
||||||
|
// Drain input queue
|
||||||
|
for (;;) {
|
||||||
|
MappedInput mi;
|
||||||
|
if (!input_.Poll(mi))
|
||||||
|
break;
|
||||||
|
if (mi.hasCommand) {
|
||||||
|
Execute(ed, mi.id, mi.arg, mi.count);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ed.QuitRequested()) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- Visual File Picker (Qt): invoked via CommandId::VisualFilePickerToggle ---
|
||||||
|
if (ed.FilePickerVisible()) {
|
||||||
|
QString startDir;
|
||||||
|
if (!ed.FilePickerDir().empty()) {
|
||||||
|
startDir = QString::fromStdString(ed.FilePickerDir());
|
||||||
|
}
|
||||||
|
QFileDialog dlg(window_, QStringLiteral("Open File"), startDir);
|
||||||
|
dlg.setFileMode(QFileDialog::ExistingFile);
|
||||||
|
if (dlg.exec() == QDialog::Accepted) {
|
||||||
|
const QStringList files = dlg.selectedFiles();
|
||||||
|
if (!files.isEmpty()) {
|
||||||
|
const QString fp = files.front();
|
||||||
|
std::string err;
|
||||||
|
if (ed.OpenFile(fp.toStdString(), err)) {
|
||||||
|
ed.SetStatus(std::string("Opened: ") + fp.toStdString());
|
||||||
|
} else if (!err.empty()) {
|
||||||
|
ed.SetStatus(std::string("Open failed: ") + err);
|
||||||
|
} else {
|
||||||
|
ed.SetStatus("Open failed");
|
||||||
|
}
|
||||||
|
// Update picker dir for next time
|
||||||
|
QFileInfo info(fp);
|
||||||
|
ed.SetFilePickerDir(info.dir().absolutePath().toStdString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Close picker overlay regardless of outcome
|
||||||
|
ed.SetFilePickerVisible(false);
|
||||||
|
if (window_)
|
||||||
|
window_->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply any queued theme change requests (from command handler)
|
||||||
|
if (kte::gThemeChangePending) {
|
||||||
|
if (!kte::gThemeChangeRequest.empty()) {
|
||||||
|
// Apply Qt palette theme by name; if unknown, keep current palette
|
||||||
|
(void) kte::ApplyQtThemeByName(kte::gThemeChangeRequest);
|
||||||
|
}
|
||||||
|
kte::gThemeChangePending = false;
|
||||||
|
kte::gThemeChangeRequest.clear();
|
||||||
|
if (window_)
|
||||||
|
window_->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Visual font picker request (Qt only)
|
||||||
|
if (kte::gFontDialogRequested) {
|
||||||
|
// Seed initial font from current or default
|
||||||
|
QFont seed;
|
||||||
|
if (!kte::gCurrentFontFamily.empty()) {
|
||||||
|
seed = QFont(QString::fromStdString(kte::gCurrentFontFamily),
|
||||||
|
(int) std::lround(kte::gCurrentFontSize > 0 ? kte::gCurrentFontSize : 18));
|
||||||
|
} else {
|
||||||
|
seed = window_ ? window_->font() : QFont();
|
||||||
|
}
|
||||||
|
bool ok = false;
|
||||||
|
const QFont chosen = QFontDialog::getFont(&ok, seed, window_, QStringLiteral("Choose Editor Font"));
|
||||||
|
if (ok) {
|
||||||
|
// Queue font change via existing hooks
|
||||||
|
kte::gFontFamilyRequest = chosen.family().toStdString();
|
||||||
|
// Use pixel size if available, otherwise convert from point size approximately
|
||||||
|
int px = chosen.pixelSize();
|
||||||
|
if (px <= 0) {
|
||||||
|
// Approximate points to pixels (96 DPI assumption); Qt will rasterize appropriately
|
||||||
|
px = (int) std::lround(chosen.pointSizeF() * 96.0 / 72.0);
|
||||||
|
if (px <= 0)
|
||||||
|
px = 18;
|
||||||
|
}
|
||||||
|
kte::gFontSizeRequest = static_cast<float>(px);
|
||||||
|
kte::gFontChangePending = true;
|
||||||
|
}
|
||||||
|
kte::gFontDialogRequested = false;
|
||||||
|
if (window_)
|
||||||
|
window_->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply any queued font change requests (Qt)
|
||||||
|
if (kte::gFontChangePending) {
|
||||||
|
// Derive target family
|
||||||
|
auto map_family = [](const std::string &name) -> QString {
|
||||||
|
std::string n = name;
|
||||||
|
std::transform(n.begin(), n.end(), n.begin(), [](unsigned char c) {
|
||||||
|
return (char) std::tolower(c);
|
||||||
|
});
|
||||||
|
QString fam;
|
||||||
|
if (n == "brass" || n == "brassmono" || n == "brass mono") {
|
||||||
|
fam = QStringLiteral("Brass Mono");
|
||||||
|
} else if (n == "jetbrains" || n == "jetbrains mono" || n == "jetbrains-mono") {
|
||||||
|
fam = QStringLiteral("JetBrains Mono");
|
||||||
|
} else if (n == "iosevka") {
|
||||||
|
fam = QStringLiteral("Iosevka");
|
||||||
|
} else if (n == "inconsolata" || n == "inconsolataex") {
|
||||||
|
fam = QStringLiteral("Inconsolata");
|
||||||
|
} else if (n == "space" || n == "spacemono" || n == "space mono") {
|
||||||
|
fam = QStringLiteral("Space Mono");
|
||||||
|
} else if (n == "go") {
|
||||||
|
fam = QStringLiteral("Go Mono");
|
||||||
|
} else if (n == "ibm" || n == "ibm plex mono" || n == "ibm-plex-mono") {
|
||||||
|
fam = QStringLiteral("IBM Plex Mono");
|
||||||
|
} else if (n == "fira" || n == "fira code" || n == "fira-code") {
|
||||||
|
fam = QStringLiteral("Fira Code");
|
||||||
|
} else if (!name.empty()) {
|
||||||
|
fam = QString::fromStdString(name);
|
||||||
|
}
|
||||||
|
// Validate availability; choose fallback if needed
|
||||||
|
const auto families = QFontDatabase::families();
|
||||||
|
if (!fam.isEmpty() && families.contains(fam)) {
|
||||||
|
return fam;
|
||||||
|
}
|
||||||
|
// Fallback chain
|
||||||
|
const QStringList fallbacks = {
|
||||||
|
QStringLiteral("Brass Mono"),
|
||||||
|
QStringLiteral("JetBrains Mono"),
|
||||||
|
QStringLiteral("SF Mono"),
|
||||||
|
QStringLiteral("Menlo"),
|
||||||
|
QStringLiteral("Monaco"),
|
||||||
|
QStringLiteral("Courier New"),
|
||||||
|
QStringLiteral("Courier"),
|
||||||
|
QStringLiteral("Monospace")
|
||||||
|
};
|
||||||
|
for (const auto &fb: fallbacks) {
|
||||||
|
if (families.contains(fb))
|
||||||
|
return fb;
|
||||||
|
}
|
||||||
|
return fam.isEmpty() ? QStringLiteral("Monospace") : fam;
|
||||||
|
};
|
||||||
|
|
||||||
|
QString target_family;
|
||||||
|
if (!kte::gFontFamilyRequest.empty()) {
|
||||||
|
target_family = map_family(kte::gFontFamilyRequest);
|
||||||
|
} else if (!kte::gCurrentFontFamily.empty()) {
|
||||||
|
target_family = QString::fromStdString(kte::gCurrentFontFamily);
|
||||||
|
}
|
||||||
|
int target_px = 0;
|
||||||
|
if (kte::gFontSizeRequest > 0.0f) {
|
||||||
|
target_px = (int) std::lround(kte::gFontSizeRequest);
|
||||||
|
} else if (kte::gCurrentFontSize > 0.0f) {
|
||||||
|
target_px = (int) std::lround(kte::gCurrentFontSize);
|
||||||
|
}
|
||||||
|
if (target_px <= 0)
|
||||||
|
target_px = 18;
|
||||||
|
if (target_family.isEmpty())
|
||||||
|
target_family = QStringLiteral("Monospace");
|
||||||
|
|
||||||
|
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||||
|
mw->SetFontFamilyAndSize(target_family, target_px);
|
||||||
|
}
|
||||||
|
// Update globals
|
||||||
|
kte::gCurrentFontFamily = target_family.toStdString();
|
||||||
|
kte::gCurrentFontSize = static_cast<float>(target_px);
|
||||||
|
// Reset requests
|
||||||
|
kte::gFontChangePending = false;
|
||||||
|
kte::gFontFamilyRequest.clear();
|
||||||
|
kte::gFontSizeRequest = 0.0f;
|
||||||
|
|
||||||
|
// Recompute editor dimensions to match new metrics
|
||||||
|
QFont f(target_family, target_px);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = std::max(12, fm.height());
|
||||||
|
const int ch_w = std::max(6, fm.horizontalAdvance(QStringLiteral("M")));
|
||||||
|
const int w = window_ ? window_->width() : 0;
|
||||||
|
const int h = window_ ? window_->height() : 0;
|
||||||
|
const int pad = 16;
|
||||||
|
const int status_h = line_h + 4;
|
||||||
|
const int avail_w = std::max(0, w - 2 * pad);
|
||||||
|
const int avail_h = std::max(0, h - 2 * pad - status_h);
|
||||||
|
std::size_t rows = std::max<std::size_t>(1, (avail_h / line_h) + 1); // + status
|
||||||
|
std::size_t cols = std::max<std::size_t>(1, (avail_w / ch_w));
|
||||||
|
ed.SetDimensions(rows, cols);
|
||||||
|
|
||||||
|
if (window_)
|
||||||
|
window_->update();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current frame (request repaint)
|
||||||
|
renderer_.Draw(ed);
|
||||||
|
|
||||||
|
// Detect window close
|
||||||
|
if (auto *mw = dynamic_cast<MainWindow *>(window_)) {
|
||||||
|
if (mw->WasClosed()) {
|
||||||
|
running = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
GUIFrontend::Shutdown()
|
||||||
|
{
|
||||||
|
if (window_) {
|
||||||
|
window_->close();
|
||||||
|
delete window_;
|
||||||
|
window_ = nullptr;
|
||||||
|
}
|
||||||
|
if (app_) {
|
||||||
|
delete app_;
|
||||||
|
app_ = nullptr;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
QtFrontend.h
Normal file
36
QtFrontend.h
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*
|
||||||
|
* QtFrontend - couples QtInputHandler + QtRenderer and owns Qt lifecycle
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Frontend.h"
|
||||||
|
#include "GUIConfig.h"
|
||||||
|
#include "QtInputHandler.h"
|
||||||
|
#include "QtRenderer.h"
|
||||||
|
|
||||||
|
class QApplication;
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
// Keep the public class name GUIFrontend to match main.cc selection logic.
|
||||||
|
class GUIFrontend final : public Frontend {
|
||||||
|
public:
|
||||||
|
GUIFrontend() = default;
|
||||||
|
|
||||||
|
~GUIFrontend() override = default;
|
||||||
|
|
||||||
|
bool Init(Editor &ed) override;
|
||||||
|
|
||||||
|
void Step(Editor &ed, bool &running) override;
|
||||||
|
|
||||||
|
void Shutdown() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
GUIConfig config_{};
|
||||||
|
QtInputHandler input_{};
|
||||||
|
QtRenderer renderer_{};
|
||||||
|
|
||||||
|
QApplication *app_ = nullptr; // owned
|
||||||
|
QWidget *window_ = nullptr; // owned
|
||||||
|
int width_ = 1280;
|
||||||
|
int height_ = 800;
|
||||||
|
};
|
||||||
538
QtInputHandler.cc
Normal file
538
QtInputHandler.cc
Normal file
@@ -0,0 +1,538 @@
|
|||||||
|
// Refactor: route all key processing through command subsystem, mirroring ImGuiInputHandler
|
||||||
|
|
||||||
|
#include "QtInputHandler.h"
|
||||||
|
|
||||||
|
#include <QKeyEvent>
|
||||||
|
|
||||||
|
#include <ncurses.h>
|
||||||
|
|
||||||
|
#include "Editor.h"
|
||||||
|
#include "KKeymap.h"
|
||||||
|
|
||||||
|
// Temporary verbose logging to debug macOS Qt key translation issues
|
||||||
|
// Default to off; enable by defining QT_IH_DEBUG=1 at compile time when needed.
|
||||||
|
#ifndef QT_IH_DEBUG
|
||||||
|
#define QT_IH_DEBUG 0
|
||||||
|
#endif
|
||||||
|
|
||||||
|
#if QT_IH_DEBUG
|
||||||
|
#include <cstdio>
|
||||||
|
static const char *
|
||||||
|
mods_str(Qt::KeyboardModifiers m)
|
||||||
|
{
|
||||||
|
static thread_local char buf[64];
|
||||||
|
buf[0] = '\0';
|
||||||
|
bool first = true;
|
||||||
|
auto add = [&](const char *s) {
|
||||||
|
if (!first)
|
||||||
|
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "|");
|
||||||
|
std::snprintf(buf + std::strlen(buf), sizeof(buf) - std::strlen(buf), "%s", s);
|
||||||
|
first = false;
|
||||||
|
};
|
||||||
|
if (m & Qt::ShiftModifier)
|
||||||
|
add("Shift");
|
||||||
|
if (m & Qt::ControlModifier)
|
||||||
|
add("Ctrl");
|
||||||
|
if (m & Qt::AltModifier)
|
||||||
|
add("Alt");
|
||||||
|
if (m & Qt::MetaModifier)
|
||||||
|
add("Meta");
|
||||||
|
if (first)
|
||||||
|
std::snprintf(buf, sizeof(buf), "none");
|
||||||
|
return buf;
|
||||||
|
}
|
||||||
|
#define LOGF(...) std::fprintf(stderr, __VA_ARGS__)
|
||||||
|
#else
|
||||||
|
#define LOGF(...) ((void)0)
|
||||||
|
#endif
|
||||||
|
|
||||||
|
static bool
|
||||||
|
IsPrintableQt(const QKeyEvent &e)
|
||||||
|
{
|
||||||
|
// Printable if it yields non-empty text and no Ctrl/Meta modifier
|
||||||
|
if (e.modifiers() & (Qt::ControlModifier | Qt::MetaModifier))
|
||||||
|
return false;
|
||||||
|
const QString t = e.text();
|
||||||
|
return !t.isEmpty() && !t.at(0).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int
|
||||||
|
ToAsciiKey(const QKeyEvent &e)
|
||||||
|
{
|
||||||
|
const QString t = e.text();
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
const QChar c = t.at(0);
|
||||||
|
if (!c.isNull())
|
||||||
|
return KLowerAscii(c.unicode());
|
||||||
|
}
|
||||||
|
// When modifiers (like Control) are held, Qt::text() can be empty on macOS.
|
||||||
|
// Fall back to mapping common virtual keys to ASCII.
|
||||||
|
switch (e.key()) {
|
||||||
|
case Qt::Key_A:
|
||||||
|
return 'a';
|
||||||
|
case Qt::Key_B:
|
||||||
|
return 'b';
|
||||||
|
case Qt::Key_C:
|
||||||
|
return 'c';
|
||||||
|
case Qt::Key_D:
|
||||||
|
return 'd';
|
||||||
|
case Qt::Key_E:
|
||||||
|
return 'e';
|
||||||
|
case Qt::Key_F:
|
||||||
|
return 'f';
|
||||||
|
case Qt::Key_G:
|
||||||
|
return 'g';
|
||||||
|
case Qt::Key_H:
|
||||||
|
return 'h';
|
||||||
|
case Qt::Key_I:
|
||||||
|
return 'i';
|
||||||
|
case Qt::Key_J:
|
||||||
|
return 'j';
|
||||||
|
case Qt::Key_K:
|
||||||
|
return 'k';
|
||||||
|
case Qt::Key_L:
|
||||||
|
return 'l';
|
||||||
|
case Qt::Key_M:
|
||||||
|
return 'm';
|
||||||
|
case Qt::Key_N:
|
||||||
|
return 'n';
|
||||||
|
case Qt::Key_O:
|
||||||
|
return 'o';
|
||||||
|
case Qt::Key_P:
|
||||||
|
return 'p';
|
||||||
|
case Qt::Key_Q:
|
||||||
|
return 'q';
|
||||||
|
case Qt::Key_R:
|
||||||
|
return 'r';
|
||||||
|
case Qt::Key_S:
|
||||||
|
return 's';
|
||||||
|
case Qt::Key_T:
|
||||||
|
return 't';
|
||||||
|
case Qt::Key_U:
|
||||||
|
return 'u';
|
||||||
|
case Qt::Key_V:
|
||||||
|
return 'v';
|
||||||
|
case Qt::Key_W:
|
||||||
|
return 'w';
|
||||||
|
case Qt::Key_X:
|
||||||
|
return 'x';
|
||||||
|
case Qt::Key_Y:
|
||||||
|
return 'y';
|
||||||
|
case Qt::Key_Z:
|
||||||
|
return 'z';
|
||||||
|
case Qt::Key_0:
|
||||||
|
return '0';
|
||||||
|
case Qt::Key_1:
|
||||||
|
return '1';
|
||||||
|
case Qt::Key_2:
|
||||||
|
return '2';
|
||||||
|
case Qt::Key_3:
|
||||||
|
return '3';
|
||||||
|
case Qt::Key_4:
|
||||||
|
return '4';
|
||||||
|
case Qt::Key_5:
|
||||||
|
return '5';
|
||||||
|
case Qt::Key_6:
|
||||||
|
return '6';
|
||||||
|
case Qt::Key_7:
|
||||||
|
return '7';
|
||||||
|
case Qt::Key_8:
|
||||||
|
return '8';
|
||||||
|
case Qt::Key_9:
|
||||||
|
return '9';
|
||||||
|
case Qt::Key_Comma:
|
||||||
|
return ',';
|
||||||
|
case Qt::Key_Period:
|
||||||
|
return '.';
|
||||||
|
case Qt::Key_Semicolon:
|
||||||
|
return ';';
|
||||||
|
case Qt::Key_Apostrophe:
|
||||||
|
return '\'';
|
||||||
|
case Qt::Key_Minus:
|
||||||
|
return '-';
|
||||||
|
case Qt::Key_Equal:
|
||||||
|
return '=';
|
||||||
|
case Qt::Key_Slash:
|
||||||
|
return '/';
|
||||||
|
case Qt::Key_Backslash:
|
||||||
|
return '\\';
|
||||||
|
case Qt::Key_BracketLeft:
|
||||||
|
return '[';
|
||||||
|
case Qt::Key_BracketRight:
|
||||||
|
return ']';
|
||||||
|
case Qt::Key_QuoteLeft:
|
||||||
|
return '`';
|
||||||
|
case Qt::Key_Space:
|
||||||
|
return ' ';
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Case-preserving ASCII derivation for k-prefix handling where we need to
|
||||||
|
// distinguish between 'C' and 'c'. Falls back to virtual-key mapping if
|
||||||
|
// event text is unavailable (common when Control/Meta held on macOS).
|
||||||
|
static int
|
||||||
|
ToAsciiKeyPreserveCase(const QKeyEvent &e)
|
||||||
|
{
|
||||||
|
const QString t = e.text();
|
||||||
|
if (!t.isEmpty()) {
|
||||||
|
const QChar c = t.at(0);
|
||||||
|
if (!c.isNull())
|
||||||
|
return c.unicode();
|
||||||
|
}
|
||||||
|
// Fall back to virtual key mapping (letters as uppercase A..Z)
|
||||||
|
switch (e.key()) {
|
||||||
|
case Qt::Key_A:
|
||||||
|
return 'A';
|
||||||
|
case Qt::Key_B:
|
||||||
|
return 'B';
|
||||||
|
case Qt::Key_C:
|
||||||
|
return 'C';
|
||||||
|
case Qt::Key_D:
|
||||||
|
return 'D';
|
||||||
|
case Qt::Key_E:
|
||||||
|
return 'E';
|
||||||
|
case Qt::Key_F:
|
||||||
|
return 'F';
|
||||||
|
case Qt::Key_G:
|
||||||
|
return 'G';
|
||||||
|
case Qt::Key_H:
|
||||||
|
return 'H';
|
||||||
|
case Qt::Key_I:
|
||||||
|
return 'I';
|
||||||
|
case Qt::Key_J:
|
||||||
|
return 'J';
|
||||||
|
case Qt::Key_K:
|
||||||
|
return 'K';
|
||||||
|
case Qt::Key_L:
|
||||||
|
return 'L';
|
||||||
|
case Qt::Key_M:
|
||||||
|
return 'M';
|
||||||
|
case Qt::Key_N:
|
||||||
|
return 'N';
|
||||||
|
case Qt::Key_O:
|
||||||
|
return 'O';
|
||||||
|
case Qt::Key_P:
|
||||||
|
return 'P';
|
||||||
|
case Qt::Key_Q:
|
||||||
|
return 'Q';
|
||||||
|
case Qt::Key_R:
|
||||||
|
return 'R';
|
||||||
|
case Qt::Key_S:
|
||||||
|
return 'S';
|
||||||
|
case Qt::Key_T:
|
||||||
|
return 'T';
|
||||||
|
case Qt::Key_U:
|
||||||
|
return 'U';
|
||||||
|
case Qt::Key_V:
|
||||||
|
return 'V';
|
||||||
|
case Qt::Key_W:
|
||||||
|
return 'W';
|
||||||
|
case Qt::Key_X:
|
||||||
|
return 'X';
|
||||||
|
case Qt::Key_Y:
|
||||||
|
return 'Y';
|
||||||
|
case Qt::Key_Z:
|
||||||
|
return 'Z';
|
||||||
|
case Qt::Key_Comma:
|
||||||
|
return ',';
|
||||||
|
case Qt::Key_Period:
|
||||||
|
return '.';
|
||||||
|
case Qt::Key_Semicolon:
|
||||||
|
return ';';
|
||||||
|
case Qt::Key_Apostrophe:
|
||||||
|
return '\'';
|
||||||
|
case Qt::Key_Minus:
|
||||||
|
return '-';
|
||||||
|
case Qt::Key_Equal:
|
||||||
|
return '=';
|
||||||
|
case Qt::Key_Slash:
|
||||||
|
return '/';
|
||||||
|
case Qt::Key_Backslash:
|
||||||
|
return '\\';
|
||||||
|
case Qt::Key_BracketLeft:
|
||||||
|
return '[';
|
||||||
|
case Qt::Key_BracketRight:
|
||||||
|
return ']';
|
||||||
|
case Qt::Key_QuoteLeft:
|
||||||
|
return '`';
|
||||||
|
case Qt::Key_Space:
|
||||||
|
return ' ';
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
QtInputHandler::ProcessKeyEvent(const QKeyEvent &e)
|
||||||
|
{
|
||||||
|
const Qt::KeyboardModifiers mods = e.modifiers();
|
||||||
|
LOGF("[QtIH] keyPress key=0x%X mods=%s text='%s' k_prefix=%d k_ctrl_pending=%d esc_meta=%d\n",
|
||||||
|
e.key(), mods_str(mods), e.text().toUtf8().constData(), (int)k_prefix_, (int)k_ctrl_pending_,
|
||||||
|
(int)esc_meta_);
|
||||||
|
|
||||||
|
// Control-chord detection: only treat the physical Control key as control-like.
|
||||||
|
// Do NOT include Meta (Command) here so that ⌘-letter shortcuts do not fall into
|
||||||
|
// the Ctrl map (prevents ⌘-T being mistaken for C-t).
|
||||||
|
const bool ctrl_like = (mods & Qt::ControlModifier);
|
||||||
|
|
||||||
|
// 1) Universal argument digits (when active), consume digits without enqueuing commands
|
||||||
|
if (ed_ &&ed_
|
||||||
|
|
||||||
|
->
|
||||||
|
UArg() != 0
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!(mods & (Qt::ControlModifier | Qt::AltModifier | Qt::MetaModifier))) {
|
||||||
|
if (e.key() >= Qt::Key_0 && e.key() <= Qt::Key_9) {
|
||||||
|
int d = e.key() - Qt::Key_0;
|
||||||
|
ed_->UArgDigit(d);
|
||||||
|
// request status refresh
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::UArgStatus, std::string(), 0});
|
||||||
|
LOGF("[QtIH] UArg digit %d -> enqueue UArgStatus\n", d);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2) Enter k-prefix on C-k
|
||||||
|
if (ctrl_like && (e.key() == Qt::Key_K)) {
|
||||||
|
k_prefix_ = true;
|
||||||
|
k_ctrl_pending_ = false;
|
||||||
|
LOGF("[QtIH] Enter KPrefix\n");
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::KPrefix, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3) If currently in k-prefix, resolve next key via KLookupKCommand
|
||||||
|
if (k_prefix_) {
|
||||||
|
// ESC/meta prefix should not interfere with k-suffix resolution
|
||||||
|
esc_meta_ = false;
|
||||||
|
// Support literal 'C' (uppercase) or '^' to indicate the next key is Ctrl-qualified.
|
||||||
|
// Use case-preserving derivation so that 'c' (lowercase) can still be a valid suffix
|
||||||
|
// like C-k c (BufferClose).
|
||||||
|
int ascii_raw = ToAsciiKeyPreserveCase(e);
|
||||||
|
if (ascii_raw == 'C' || ascii_raw == '^') {
|
||||||
|
k_ctrl_pending_ = true;
|
||||||
|
if (ed_)
|
||||||
|
ed_->SetStatus("C-k C _");
|
||||||
|
LOGF("[QtIH] KPrefix: set k_ctrl_pending via '%c'\n", (ascii_raw == 'C') ? 'C' : '^');
|
||||||
|
return true; // consume, wait for next key
|
||||||
|
}
|
||||||
|
int ascii_key = (ascii_raw != 0) ? ascii_raw : ToAsciiKey(e);
|
||||||
|
int lower = KLowerAscii(ascii_key);
|
||||||
|
// Only pass a control suffix for specific supported keys (d/x/q),
|
||||||
|
// matching ImGui behavior so that holding Ctrl during the suffix
|
||||||
|
// doesn't break other mappings like C-k c (BufferClose).
|
||||||
|
bool ctrl_suffix_supported = (lower == 'd' || lower == 'x' || lower == 'q');
|
||||||
|
bool pass_ctrl = (ctrl_like || k_ctrl_pending_) && ctrl_suffix_supported;
|
||||||
|
k_ctrl_pending_ = false; // consume pending qualifier on any suffix
|
||||||
|
LOGF("[QtIH] KPrefix: ascii_key=%d lower=%d pass_ctrl=%d\n", ascii_key, lower, (int)pass_ctrl);
|
||||||
|
if (ascii_key != 0) {
|
||||||
|
CommandId id;
|
||||||
|
if (KLookupKCommand(ascii_key, pass_ctrl, id)) {
|
||||||
|
LOGF("[QtIH] KPrefix: mapped to command id=%d\n", (int)id);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, id, std::string(), 0});
|
||||||
|
} else {
|
||||||
|
// Unknown k-command: notify
|
||||||
|
std::string a;
|
||||||
|
a.push_back(static_cast<char>(ascii_key));
|
||||||
|
LOGF("[QtIH] KPrefix: unknown command for '%c'\n", (char)ascii_key);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::UnknownKCommand, a, 0});
|
||||||
|
}
|
||||||
|
k_prefix_ = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// If not resolvable, consume and exit k-prefix
|
||||||
|
k_prefix_ = false;
|
||||||
|
LOGF("[QtIH] KPrefix: unresolved key; exiting prefix\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3.5) GUI shortcut: Command/Meta + T opens the visual font picker (Qt only).
|
||||||
|
// Require Meta present and Control NOT present so Ctrl-T never triggers this.
|
||||||
|
if ((mods & Qt::MetaModifier) && !(mods & Qt::ControlModifier) && e.key() == Qt::Key_T) {
|
||||||
|
LOGF("[QtIH] Meta/Super-T -> VisualFontPickerToggle\n");
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::VisualFontPickerToggle, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4) ESC as Meta prefix (set state). Alt/Meta chord handled below directly.
|
||||||
|
if (e.key() == Qt::Key_Escape) {
|
||||||
|
esc_meta_ = true;
|
||||||
|
LOGF("[QtIH] ESC: set esc_meta\n");
|
||||||
|
return true; // consumed
|
||||||
|
}
|
||||||
|
|
||||||
|
// 5) Alt/Meta bindings (ESC f/b equivalent). Handle either Alt/Meta or pending esc_meta_
|
||||||
|
// ESC/meta chords: on macOS, do NOT treat Meta as ESC; only Alt (Option) should trigger.
|
||||||
|
#if defined(__APPLE__)
|
||||||
|
if (esc_meta_ || (mods & Qt::AltModifier)) {
|
||||||
|
|
||||||
|
|
||||||
|
#else
|
||||||
|
if (esc_meta_ || (mods & (Qt::AltModifier | Qt::MetaModifier))) {
|
||||||
|
#endif
|
||||||
|
int ascii_key = 0;
|
||||||
|
if (e.key() == Qt::Key_Backspace) {
|
||||||
|
ascii_key = KEY_BACKSPACE;
|
||||||
|
} else if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
|
||||||
|
ascii_key = 'a' + (e.key() - Qt::Key_A);
|
||||||
|
} else if (e.key() == Qt::Key_Comma) {
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (e.key() == Qt::Key_Period) {
|
||||||
|
ascii_key = '>';
|
||||||
|
}
|
||||||
|
// If still unknown, try deriving from text (covers digits, punctuation, locale)
|
||||||
|
if (ascii_key == 0) {
|
||||||
|
ascii_key = ToAsciiKey(e);
|
||||||
|
}
|
||||||
|
esc_meta_ = false; // one-shot regardless
|
||||||
|
if (ascii_key != 0) {
|
||||||
|
ascii_key = KLowerAscii(ascii_key);
|
||||||
|
CommandId id;
|
||||||
|
if (KLookupEscCommand(ascii_key, id)) {
|
||||||
|
LOGF("[QtIH] ESC/Meta: mapped '%d' -> id=%d\n", ascii_key, (int)id);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, id, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
// Report invalid ESC sequence just like ImGui path
|
||||||
|
LOGF("[QtIH] ESC/Meta: unknown command for ascii=%d\n", ascii_key);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::UnknownEscCommand, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Nothing derivable: consume (ESC prefix cleared) and do not insert text
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 6) Control-chord direct mappings (e.g., C-n/C-p/C-f/C-b...)
|
||||||
|
if (ctrl_like) {
|
||||||
|
// Universal argument handling: C-u starts collection; C-g cancels
|
||||||
|
if (e.key() == Qt::Key_U) {
|
||||||
|
if (ed_)
|
||||||
|
ed_->UArgStart();
|
||||||
|
LOGF("[QtIH] Ctrl-chord: start universal argument\n");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (e.key() == Qt::Key_G) {
|
||||||
|
if (ed_)
|
||||||
|
ed_->UArgClear();
|
||||||
|
k_ctrl_pending_ = false;
|
||||||
|
k_prefix_ = false;
|
||||||
|
LOGF("[QtIH] Ctrl-chord: cancel universal argument and k-prefix via C-g\n");
|
||||||
|
// Fall through to map C-g to Refresh via ctrl map
|
||||||
|
}
|
||||||
|
if (e.key() >= Qt::Key_A && e.key() <= Qt::Key_Z) {
|
||||||
|
int ascii_key = 'a' + (e.key() - Qt::Key_A);
|
||||||
|
CommandId id;
|
||||||
|
if (KLookupCtrlCommand(ascii_key, id)) {
|
||||||
|
LOGF("[QtIH] Ctrl-chord: 'C-%c' -> id=%d\n", (char)ascii_key, (int)id);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, id, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If no mapping, continue to allow other keys below
|
||||||
|
}
|
||||||
|
|
||||||
|
// 7) Special navigation/edit keys (match ImGui behavior)
|
||||||
|
{
|
||||||
|
CommandId id;
|
||||||
|
bool has = false;
|
||||||
|
switch (e.key()) {
|
||||||
|
case Qt::Key_Return:
|
||||||
|
case Qt::Key_Enter:
|
||||||
|
id = CommandId::Newline;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Backspace:
|
||||||
|
id = CommandId::Backspace;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Delete:
|
||||||
|
id = CommandId::DeleteChar;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Left:
|
||||||
|
id = CommandId::MoveLeft;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Right:
|
||||||
|
id = CommandId::MoveRight;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Up:
|
||||||
|
id = CommandId::MoveUp;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Down:
|
||||||
|
id = CommandId::MoveDown;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_Home:
|
||||||
|
id = CommandId::MoveHome;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_End:
|
||||||
|
id = CommandId::MoveEnd;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_PageUp:
|
||||||
|
id = CommandId::PageUp;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
case Qt::Key_PageDown:
|
||||||
|
id = CommandId::PageDown;
|
||||||
|
has = true;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
if (has) {
|
||||||
|
LOGF("[QtIH] Special key -> id=%d\n", (int)id);
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, id, std::string(), 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 8) Insert printable text
|
||||||
|
if (IsPrintableQt(e)) {
|
||||||
|
std::string s = e.text().toStdString();
|
||||||
|
if (!s.empty()) {
|
||||||
|
LOGF("[QtIH] InsertText '%s'\n", s.c_str());
|
||||||
|
std::lock_guard<std::mutex> lk(mu_);
|
||||||
|
q_.push(MappedInput{true, CommandId::InsertText, s, 0});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGF("[QtIH] Unhandled key\n");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
QtInputHandler::Poll(MappedInput &out)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lock(mu_);
|
||||||
|
if (q_.empty())
|
||||||
|
return false;
|
||||||
|
out = q_.front();
|
||||||
|
q_.pop();
|
||||||
|
return true;
|
||||||
|
}
|
||||||
40
QtInputHandler.h
Normal file
40
QtInputHandler.h
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
/*
|
||||||
|
* QtInputHandler - Qt-based input mapping for GUI mode
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <mutex>
|
||||||
|
#include <queue>
|
||||||
|
|
||||||
|
#include "InputHandler.h"
|
||||||
|
|
||||||
|
class QKeyEvent;
|
||||||
|
|
||||||
|
class QtInputHandler final : public InputHandler {
|
||||||
|
public:
|
||||||
|
QtInputHandler() = default;
|
||||||
|
|
||||||
|
~QtInputHandler() override = default;
|
||||||
|
|
||||||
|
|
||||||
|
void Attach(Editor *ed) override
|
||||||
|
{
|
||||||
|
ed_ = ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// Translate a Qt key event to editor command and enqueue if applicable.
|
||||||
|
// Returns true if it produced a mapped command or consumed input.
|
||||||
|
bool ProcessKeyEvent(const QKeyEvent &e);
|
||||||
|
|
||||||
|
bool Poll(MappedInput &out) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mutex mu_;
|
||||||
|
std::queue<MappedInput> q_;
|
||||||
|
bool k_prefix_ = false;
|
||||||
|
bool k_ctrl_pending_ = false; // C-k C-… qualifier
|
||||||
|
bool esc_meta_ = false; // ESC-prefix for next key
|
||||||
|
bool suppress_text_input_once_ = false; // reserved (Qt sends text in keyPressEvent)
|
||||||
|
Editor *ed_ = nullptr;
|
||||||
|
};
|
||||||
76
QtRenderer.cc
Normal file
76
QtRenderer.cc
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
#include "QtRenderer.h"
|
||||||
|
|
||||||
|
#include <QWidget>
|
||||||
|
#include <QPainter>
|
||||||
|
#include <QPaintEvent>
|
||||||
|
#include <QFont>
|
||||||
|
#include <QFontMetrics>
|
||||||
|
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
class EditorWidget : public QWidget {
|
||||||
|
public:
|
||||||
|
explicit EditorWidget(QWidget *parent = nullptr) : QWidget(parent)
|
||||||
|
{
|
||||||
|
setAttribute(Qt::WA_OpaquePaintEvent);
|
||||||
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void SetEditor(Editor *ed)
|
||||||
|
{
|
||||||
|
ed_ = ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected:
|
||||||
|
void paintEvent(QPaintEvent *event) override
|
||||||
|
{
|
||||||
|
Q_UNUSED(event);
|
||||||
|
QPainter p(this);
|
||||||
|
// Background
|
||||||
|
const QColor bg(28, 28, 30);
|
||||||
|
p.fillRect(rect(), bg);
|
||||||
|
|
||||||
|
// Font and metrics
|
||||||
|
QFont f("JetBrains Mono", 13);
|
||||||
|
p.setFont(f);
|
||||||
|
QFontMetrics fm(f);
|
||||||
|
const int line_h = fm.height();
|
||||||
|
|
||||||
|
// Title
|
||||||
|
p.setPen(QColor(220, 220, 220));
|
||||||
|
p.drawText(8, fm.ascent() + 4, QStringLiteral("kte (Qt frontend)"));
|
||||||
|
|
||||||
|
// Status bar at bottom
|
||||||
|
const int bar_h = line_h + 6; // padding
|
||||||
|
const int bar_y = height() - bar_h;
|
||||||
|
QRect status_rect(0, bar_y, width(), bar_h);
|
||||||
|
p.fillRect(status_rect, QColor(40, 40, 44));
|
||||||
|
p.setPen(QColor(180, 180, 140));
|
||||||
|
if (ed_) {
|
||||||
|
const QString status = QString::fromStdString(ed_->Status());
|
||||||
|
// draw at baseline within the bar
|
||||||
|
const int baseline = bar_y + 3 + fm.ascent();
|
||||||
|
p.drawText(8, baseline, status);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
Editor *ed_ = nullptr;
|
||||||
|
};
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void
|
||||||
|
QtRenderer::Draw(Editor &ed)
|
||||||
|
{
|
||||||
|
if (!widget_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// If our widget is an EditorWidget, pass the editor pointer for painting
|
||||||
|
if (auto *ew = dynamic_cast<EditorWidget *>(widget_)) {
|
||||||
|
ew->SetEditor(&ed);
|
||||||
|
}
|
||||||
|
// Request a repaint
|
||||||
|
widget_->update();
|
||||||
|
}
|
||||||
27
QtRenderer.h
Normal file
27
QtRenderer.h
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
* QtRenderer - minimal Qt-based renderer
|
||||||
|
*/
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "Renderer.h"
|
||||||
|
|
||||||
|
class QWidget;
|
||||||
|
|
||||||
|
class QtRenderer final : public Renderer {
|
||||||
|
public:
|
||||||
|
QtRenderer() = default;
|
||||||
|
|
||||||
|
~QtRenderer() override = default;
|
||||||
|
|
||||||
|
|
||||||
|
void Attach(QWidget *widget)
|
||||||
|
{
|
||||||
|
widget_ = widget;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void Draw(Editor &ed) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
QWidget *widget_ = nullptr; // not owned
|
||||||
|
};
|
||||||
@@ -8,5 +8,6 @@ ROADMAP / TODO:
|
|||||||
- [x] When the filename is longer than the message window, scoot left to
|
- [x] When the filename is longer than the message window, scoot left to
|
||||||
keep it in view
|
keep it in view
|
||||||
- [x] Syntax highlighting
|
- [x] Syntax highlighting
|
||||||
|
- [ ] Swap files (crash recovery). See `docs/plans/swap-files.md`
|
||||||
- [ ] The undo system should actually work
|
- [ ] The undo system should actually work
|
||||||
- [ ] LSP integration
|
- [ ] LSP integration
|
||||||
|
|||||||
434
Swap.cc
Normal file
434
Swap.cc
Normal file
@@ -0,0 +1,434 @@
|
|||||||
|
#include "Swap.h"
|
||||||
|
#include "Buffer.h"
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdio>
|
||||||
|
#include <cstring>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fcntl.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
#include <sys/stat.h>
|
||||||
|
#include <cerrno>
|
||||||
|
|
||||||
|
namespace fs = std::filesystem;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
namespace {
|
||||||
|
constexpr std::uint8_t MAGIC[8] = {'K', 'T', 'E', '_', 'S', 'W', 'P', '\0'};
|
||||||
|
constexpr std::uint32_t VERSION = 1;
|
||||||
|
|
||||||
|
// Write all bytes in buf to fd, handling EINTR and partial writes.
|
||||||
|
static bool write_full(int fd, const void *buf, size_t len)
|
||||||
|
{
|
||||||
|
const std::uint8_t *p = static_cast<const std::uint8_t *>(buf);
|
||||||
|
while (len > 0) {
|
||||||
|
ssize_t n = ::write(fd, p, len);
|
||||||
|
if (n < 0) {
|
||||||
|
if (errno == EINTR)
|
||||||
|
continue;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (n == 0)
|
||||||
|
return false; // shouldn't happen for regular files; treat as error
|
||||||
|
p += static_cast<size_t>(n);
|
||||||
|
len -= static_cast<size_t>(n);
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapManager::SwapManager()
|
||||||
|
{
|
||||||
|
running_.store(true);
|
||||||
|
worker_ = std::thread([this] {
|
||||||
|
this->writer_loop();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapManager::~SwapManager()
|
||||||
|
{
|
||||||
|
running_.store(false);
|
||||||
|
cv_.notify_all();
|
||||||
|
if (worker_.joinable())
|
||||||
|
worker_.join();
|
||||||
|
// Close all journals
|
||||||
|
for (auto &kv: journals_) {
|
||||||
|
close_ctx(kv.second);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::Attach(Buffer * /*buf*/)
|
||||||
|
{
|
||||||
|
// Stage 1: lazy-open on first record; nothing to do here.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::Detach(Buffer * /*buf*/)
|
||||||
|
{
|
||||||
|
// Stage 1: keep files open until manager destruction; future work can close per-buffer.
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::NotifyFilenameChanged(Buffer &buf)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
auto it = journals_.find(&buf);
|
||||||
|
if (it == journals_.end())
|
||||||
|
return;
|
||||||
|
JournalCtx &ctx = it->second;
|
||||||
|
// Close existing file handle, update path; lazily reopen on next write
|
||||||
|
close_ctx(ctx);
|
||||||
|
ctx.path = ComputeSidecarPath(buf);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::SetSuspended(Buffer &buf, bool on)
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
auto path = ComputeSidecarPath(buf);
|
||||||
|
// Create/update context for this buffer
|
||||||
|
JournalCtx &ctx = journals_[&buf];
|
||||||
|
ctx.path = path;
|
||||||
|
ctx.suspended = on;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapManager::SuspendGuard::SuspendGuard(SwapManager &m, Buffer *b)
|
||||||
|
: m_(m), buf_(b), prev_(false)
|
||||||
|
{
|
||||||
|
// Suspend recording while guard is alive
|
||||||
|
if (buf_)
|
||||||
|
m_.SetSuspended(*buf_, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
SwapManager::SuspendGuard::~SuspendGuard()
|
||||||
|
{
|
||||||
|
if (buf_)
|
||||||
|
m_.SetSuspended(*buf_, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::string
|
||||||
|
SwapManager::ComputeSidecarPath(const Buffer &buf)
|
||||||
|
{
|
||||||
|
if (buf.IsFileBacked() || !buf.Filename().empty()) {
|
||||||
|
fs::path p(buf.Filename());
|
||||||
|
fs::path dir = p.parent_path();
|
||||||
|
std::string base = p.filename().string();
|
||||||
|
std::string side = "." + base + ".kte.swp";
|
||||||
|
return (dir / side).string();
|
||||||
|
}
|
||||||
|
// unnamed: $TMPDIR/kte/unnamed-<ptr>.kte.swp (best-effort)
|
||||||
|
const char *tmp = std::getenv("TMPDIR");
|
||||||
|
fs::path t = tmp ? fs::path(tmp) : fs::temp_directory_path();
|
||||||
|
fs::path d = t / "kte";
|
||||||
|
char bufptr[32];
|
||||||
|
std::snprintf(bufptr, sizeof(bufptr), "%p", (const void *) &buf);
|
||||||
|
return (d / (std::string("unnamed-") + bufptr + ".kte.swp")).string();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::uint64_t
|
||||||
|
SwapManager::now_ns()
|
||||||
|
{
|
||||||
|
using namespace std::chrono;
|
||||||
|
return duration_cast<nanoseconds>(steady_clock::now().time_since_epoch()).count();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SwapManager::ensure_parent_dir(const std::string &path)
|
||||||
|
{
|
||||||
|
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 (...) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SwapManager::write_header(JournalCtx &ctx)
|
||||||
|
{
|
||||||
|
if (ctx.fd < 0)
|
||||||
|
return false;
|
||||||
|
// Write a simple 64-byte header
|
||||||
|
std::uint8_t hdr[64];
|
||||||
|
std::memset(hdr, 0, sizeof(hdr));
|
||||||
|
std::memcpy(hdr, MAGIC, 8);
|
||||||
|
std::uint32_t ver = VERSION;
|
||||||
|
std::memcpy(hdr + 8, &ver, sizeof(ver));
|
||||||
|
std::uint64_t ts = static_cast<std::uint64_t>(std::time(nullptr));
|
||||||
|
std::memcpy(hdr + 16, &ts, sizeof(ts));
|
||||||
|
ssize_t w = ::write(ctx.fd, hdr, sizeof(hdr));
|
||||||
|
return (w == (ssize_t) sizeof(hdr));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool
|
||||||
|
SwapManager::open_ctx(JournalCtx &ctx)
|
||||||
|
{
|
||||||
|
if (ctx.fd >= 0)
|
||||||
|
return true;
|
||||||
|
if (!ensure_parent_dir(ctx.path))
|
||||||
|
return false;
|
||||||
|
// Create or open with 0600 perms
|
||||||
|
int fd = ::open(ctx.path.c_str(), O_CREAT | O_RDWR, 0600);
|
||||||
|
if (fd < 0)
|
||||||
|
return false;
|
||||||
|
// Detect if file is new/empty to write header
|
||||||
|
struct stat st{};
|
||||||
|
if (fstat(fd, &st) != 0) {
|
||||||
|
::close(fd);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
ctx.fd = fd;
|
||||||
|
ctx.file = fdopen(fd, "ab");
|
||||||
|
if (!ctx.file) {
|
||||||
|
::close(fd);
|
||||||
|
ctx.fd = -1;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (st.st_size == 0) {
|
||||||
|
ctx.header_ok = write_header(ctx);
|
||||||
|
} else {
|
||||||
|
ctx.header_ok = true; // trust existing file for stage 1
|
||||||
|
// Seek to end to append
|
||||||
|
::lseek(ctx.fd, 0, SEEK_END);
|
||||||
|
}
|
||||||
|
return ctx.header_ok;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::close_ctx(JournalCtx &ctx)
|
||||||
|
{
|
||||||
|
if (ctx.file) {
|
||||||
|
std::fflush((FILE *) ctx.file);
|
||||||
|
::fsync(ctx.fd);
|
||||||
|
std::fclose((FILE *) ctx.file);
|
||||||
|
ctx.file = nullptr;
|
||||||
|
}
|
||||||
|
if (ctx.fd >= 0) {
|
||||||
|
::close(ctx.fd);
|
||||||
|
ctx.fd = -1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
std::uint32_t
|
||||||
|
SwapManager::crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::put_varu64(std::vector<std::uint8_t> &out, std::uint64_t v)
|
||||||
|
{
|
||||||
|
while (v >= 0x80) {
|
||||||
|
out.push_back(static_cast<std::uint8_t>(v) | 0x80);
|
||||||
|
v >>= 7;
|
||||||
|
}
|
||||||
|
out.push_back(static_cast<std::uint8_t>(v));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::put_u24(std::uint8_t dst[3], std::uint32_t v)
|
||||||
|
{
|
||||||
|
dst[0] = static_cast<std::uint8_t>((v >> 16) & 0xFF);
|
||||||
|
dst[1] = static_cast<std::uint8_t>((v >> 8) & 0xFF);
|
||||||
|
dst[2] = static_cast<std::uint8_t>(v & 0xFF);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::enqueue(Pending &&p)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
queue_.emplace_back(std::move(p));
|
||||||
|
}
|
||||||
|
cv_.notify_one();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::RecordInsert(Buffer &buf, int row, int col, std::string_view text)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (journals_[&buf].suspended)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pending p;
|
||||||
|
p.buf = &buf;
|
||||||
|
p.type = SwapRecType::INS;
|
||||||
|
// payload: varint row, varint col, varint len, bytes
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(text.size()));
|
||||||
|
p.payload.insert(p.payload.end(), reinterpret_cast<const std::uint8_t *>(text.data()),
|
||||||
|
reinterpret_cast<const std::uint8_t *>(text.data()) + text.size());
|
||||||
|
enqueue(std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::RecordDelete(Buffer &buf, int row, int col, std::size_t len)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (journals_[&buf].suspended)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pending p;
|
||||||
|
p.buf = &buf;
|
||||||
|
p.type = SwapRecType::DEL;
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(len));
|
||||||
|
enqueue(std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::RecordSplit(Buffer &buf, int row, int col)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (journals_[&buf].suspended)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pending p;
|
||||||
|
p.buf = &buf;
|
||||||
|
p.type = SwapRecType::SPLIT;
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, col)));
|
||||||
|
enqueue(std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::RecordJoin(Buffer &buf, int row)
|
||||||
|
{
|
||||||
|
{
|
||||||
|
std::lock_guard<std::mutex> lg(mtx_);
|
||||||
|
if (journals_[&buf].suspended)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Pending p;
|
||||||
|
p.buf = &buf;
|
||||||
|
p.type = SwapRecType::JOIN;
|
||||||
|
put_varu64(p.payload, static_cast<std::uint64_t>(std::max(0, row)));
|
||||||
|
enqueue(std::move(p));
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::writer_loop()
|
||||||
|
{
|
||||||
|
while (running_.load()) {
|
||||||
|
std::vector<Pending> batch;
|
||||||
|
{
|
||||||
|
std::unique_lock<std::mutex> lk(mtx_);
|
||||||
|
if (queue_.empty()) {
|
||||||
|
cv_.wait_for(lk, std::chrono::milliseconds(cfg_.flush_interval_ms));
|
||||||
|
}
|
||||||
|
if (!queue_.empty()) {
|
||||||
|
batch.swap(queue_);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (batch.empty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
// Group by buffer path to minimize fsyncs
|
||||||
|
for (const Pending &p: batch) {
|
||||||
|
process_one(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Throttled fsync: best-effort
|
||||||
|
// Iterate unique contexts and fsync if needed
|
||||||
|
// For stage 1, fsync all once per interval
|
||||||
|
std::uint64_t now = now_ns();
|
||||||
|
for (auto &kv: journals_) {
|
||||||
|
JournalCtx &ctx = kv.second;
|
||||||
|
if (ctx.fd >= 0) {
|
||||||
|
if (ctx.last_fsync_ns == 0 || (now - ctx.last_fsync_ns) / 1000000ULL >= cfg_.
|
||||||
|
fsync_interval_ms) {
|
||||||
|
::fsync(ctx.fd);
|
||||||
|
ctx.last_fsync_ns = now;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
void
|
||||||
|
SwapManager::process_one(const Pending &p)
|
||||||
|
{
|
||||||
|
Buffer &buf = *p.buf;
|
||||||
|
// Resolve context by path derived from buffer
|
||||||
|
std::string path = ComputeSidecarPath(buf);
|
||||||
|
// Get or create context keyed by this buffer pointer (stage 1 simplification)
|
||||||
|
JournalCtx &ctx = journals_[p.buf];
|
||||||
|
if (ctx.path.empty())
|
||||||
|
ctx.path = path;
|
||||||
|
if (!open_ctx(ctx))
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Build record: [type u8][len u24][payload][crc32 u32]
|
||||||
|
std::uint8_t len3[3];
|
||||||
|
put_u24(len3, static_cast<std::uint32_t>(p.payload.size()));
|
||||||
|
|
||||||
|
std::uint8_t head[4];
|
||||||
|
head[0] = static_cast<std::uint8_t>(p.type);
|
||||||
|
head[1] = len3[0];
|
||||||
|
head[2] = len3[1];
|
||||||
|
head[3] = len3[2];
|
||||||
|
|
||||||
|
std::uint32_t c = 0;
|
||||||
|
c = crc32(head, sizeof(head), c);
|
||||||
|
if (!p.payload.empty())
|
||||||
|
c = crc32(p.payload.data(), p.payload.size(), c);
|
||||||
|
|
||||||
|
// Write (handle partial writes and check results)
|
||||||
|
bool ok = write_full(ctx.fd, head, sizeof(head));
|
||||||
|
if (ok && !p.payload.empty())
|
||||||
|
ok = write_full(ctx.fd, p.payload.data(), p.payload.size());
|
||||||
|
if (ok)
|
||||||
|
ok = write_full(ctx.fd, &c, sizeof(c));
|
||||||
|
(void) ok; // stage 1: best-effort; future work could mark ctx error state
|
||||||
|
}
|
||||||
|
} // namespace kte
|
||||||
145
Swap.h
Normal file
145
Swap.h
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Swap.h - swap journal (crash recovery) writer/manager for kte
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <cstdint>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <mutex>
|
||||||
|
#include <condition_variable>
|
||||||
|
#include <thread>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
class Buffer;
|
||||||
|
|
||||||
|
namespace kte {
|
||||||
|
// Minimal record types for stage 1
|
||||||
|
enum class SwapRecType : std::uint8_t {
|
||||||
|
INS = 1,
|
||||||
|
DEL = 2,
|
||||||
|
SPLIT = 3,
|
||||||
|
JOIN = 4,
|
||||||
|
META = 0xF0,
|
||||||
|
CHKPT = 0xFE,
|
||||||
|
};
|
||||||
|
|
||||||
|
struct SwapConfig {
|
||||||
|
// Grouping and durability knobs (stage 1 defaults)
|
||||||
|
unsigned flush_interval_ms{200}; // group small writes
|
||||||
|
unsigned fsync_interval_ms{1000}; // at most once per second
|
||||||
|
};
|
||||||
|
|
||||||
|
// Lightweight interface that Buffer can call without depending on full manager impl
|
||||||
|
class SwapRecorder {
|
||||||
|
public:
|
||||||
|
virtual ~SwapRecorder() = default;
|
||||||
|
|
||||||
|
virtual void RecordInsert(Buffer &buf, int row, int col, std::string_view text) = 0;
|
||||||
|
|
||||||
|
virtual void RecordDelete(Buffer &buf, int row, int col, std::size_t len) = 0;
|
||||||
|
|
||||||
|
virtual void RecordSplit(Buffer &buf, int row, int col) = 0;
|
||||||
|
|
||||||
|
virtual void RecordJoin(Buffer &buf, int row) = 0;
|
||||||
|
|
||||||
|
virtual void NotifyFilenameChanged(Buffer &buf) = 0;
|
||||||
|
|
||||||
|
virtual void SetSuspended(Buffer &buf, bool on) = 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
// SwapManager manages sidecar swap files and a single background writer thread.
|
||||||
|
class SwapManager final : public SwapRecorder {
|
||||||
|
public:
|
||||||
|
SwapManager();
|
||||||
|
|
||||||
|
~SwapManager() override;
|
||||||
|
|
||||||
|
// Attach a buffer to begin journaling. Safe to call multiple times; idempotent.
|
||||||
|
void Attach(Buffer *buf);
|
||||||
|
|
||||||
|
// Detach and close journal.
|
||||||
|
void Detach(Buffer *buf);
|
||||||
|
|
||||||
|
// SwapRecorder: Notify that the buffer's filename changed (e.g., SaveAs)
|
||||||
|
void NotifyFilenameChanged(Buffer &buf) override;
|
||||||
|
|
||||||
|
// SwapRecorder
|
||||||
|
void RecordInsert(Buffer &buf, int row, int col, std::string_view text) override;
|
||||||
|
|
||||||
|
void RecordDelete(Buffer &buf, int row, int col, std::size_t len) override;
|
||||||
|
|
||||||
|
void RecordSplit(Buffer &buf, int row, int col) override;
|
||||||
|
|
||||||
|
void RecordJoin(Buffer &buf, int row) override;
|
||||||
|
|
||||||
|
// RAII guard to suspend recording for internal operations
|
||||||
|
class SuspendGuard {
|
||||||
|
public:
|
||||||
|
SuspendGuard(SwapManager &m, Buffer *b);
|
||||||
|
|
||||||
|
~SuspendGuard();
|
||||||
|
|
||||||
|
private:
|
||||||
|
SwapManager &m_;
|
||||||
|
Buffer *buf_;
|
||||||
|
bool prev_;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Per-buffer toggle
|
||||||
|
void SetSuspended(Buffer &buf, bool on) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
struct JournalCtx {
|
||||||
|
std::string path;
|
||||||
|
void *file{nullptr}; // FILE*
|
||||||
|
int fd{-1};
|
||||||
|
bool header_ok{false};
|
||||||
|
bool suspended{false};
|
||||||
|
std::uint64_t last_flush_ns{0};
|
||||||
|
std::uint64_t last_fsync_ns{0};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Pending {
|
||||||
|
Buffer *buf{nullptr};
|
||||||
|
SwapRecType type{SwapRecType::INS};
|
||||||
|
std::vector<std::uint8_t> payload; // framed payload only
|
||||||
|
bool urgent_flush{false};
|
||||||
|
};
|
||||||
|
|
||||||
|
// Helpers
|
||||||
|
static std::string ComputeSidecarPath(const Buffer &buf);
|
||||||
|
|
||||||
|
static std::uint64_t now_ns();
|
||||||
|
|
||||||
|
static bool ensure_parent_dir(const std::string &path);
|
||||||
|
|
||||||
|
static bool write_header(JournalCtx &ctx);
|
||||||
|
|
||||||
|
static bool open_ctx(JournalCtx &ctx);
|
||||||
|
|
||||||
|
static void close_ctx(JournalCtx &ctx);
|
||||||
|
|
||||||
|
static std::uint32_t crc32(const std::uint8_t *data, std::size_t len, std::uint32_t seed = 0);
|
||||||
|
|
||||||
|
static void put_varu64(std::vector<std::uint8_t> &out, std::uint64_t v);
|
||||||
|
|
||||||
|
static void put_u24(std::uint8_t dst[3], std::uint32_t v);
|
||||||
|
|
||||||
|
void enqueue(Pending &&p);
|
||||||
|
|
||||||
|
void writer_loop();
|
||||||
|
|
||||||
|
void process_one(const Pending &p);
|
||||||
|
|
||||||
|
// State
|
||||||
|
SwapConfig cfg_{};
|
||||||
|
std::unordered_map<Buffer *, JournalCtx> journals_;
|
||||||
|
std::mutex mtx_;
|
||||||
|
std::condition_variable cv_;
|
||||||
|
std::vector<Pending> queue_;
|
||||||
|
std::atomic<bool> running_{false};
|
||||||
|
std::thread worker_;
|
||||||
|
};
|
||||||
|
} // namespace kte
|
||||||
@@ -55,6 +55,8 @@ TerminalFrontend::Init(Editor &ed)
|
|||||||
prev_r_ = r;
|
prev_r_ = r;
|
||||||
prev_c_ = c;
|
prev_c_ = c;
|
||||||
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
ed.SetDimensions(static_cast<std::size_t>(r), static_cast<std::size_t>(c));
|
||||||
|
// Attach editor to input handler for editor-owned features (e.g., universal argument)
|
||||||
|
input_.Attach(&ed);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -100,4 +102,4 @@ TerminalFrontend::Shutdown()
|
|||||||
have_orig_tio_ = false;
|
have_orig_tio_ = false;
|
||||||
}
|
}
|
||||||
endwin();
|
endwin();
|
||||||
}
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
#include "TerminalInputHandler.h"
|
#include "TerminalInputHandler.h"
|
||||||
#include "KKeymap.h"
|
#include "KKeymap.h"
|
||||||
|
#include "Editor.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
constexpr int
|
constexpr int
|
||||||
@@ -21,96 +22,103 @@ static bool
|
|||||||
map_key_to_command(const int ch,
|
map_key_to_command(const int ch,
|
||||||
bool &k_prefix,
|
bool &k_prefix,
|
||||||
bool &esc_meta,
|
bool &esc_meta,
|
||||||
// universal-argument state (by ref)
|
bool &k_ctrl_pending,
|
||||||
bool &uarg_active,
|
Editor *ed,
|
||||||
bool &uarg_collecting,
|
|
||||||
bool &uarg_negative,
|
|
||||||
bool &uarg_had_digits,
|
|
||||||
int &uarg_value,
|
|
||||||
std::string &uarg_text,
|
|
||||||
MappedInput &out)
|
MappedInput &out)
|
||||||
{
|
{
|
||||||
// Handle special keys from ncurses
|
// Handle special keys from ncurses
|
||||||
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
// These keys exit k-prefix mode if active (user pressed C-k then a special key).
|
||||||
switch (ch) {
|
switch (ch) {
|
||||||
case KEY_MOUSE: {
|
case KEY_MOUSE: {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
MEVENT ev{};
|
k_ctrl_pending = false;
|
||||||
if (getmouse(&ev) == OK) {
|
MEVENT ev{};
|
||||||
// Mouse wheel → scroll viewport without moving cursor
|
if (getmouse(&ev) == OK) {
|
||||||
|
// Mouse wheel → scroll viewport without moving cursor
|
||||||
#ifdef BUTTON4_PRESSED
|
#ifdef BUTTON4_PRESSED
|
||||||
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
if (ev.bstate & (BUTTON4_PRESSED | BUTTON4_RELEASED | BUTTON4_CLICKED)) {
|
||||||
out = {true, CommandId::ScrollUp, "", 0};
|
out = {true, CommandId::ScrollUp, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
#ifdef BUTTON5_PRESSED
|
#ifdef BUTTON5_PRESSED
|
||||||
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
if (ev.bstate & (BUTTON5_PRESSED | BUTTON5_RELEASED | BUTTON5_CLICKED)) {
|
||||||
out = {true, CommandId::ScrollDown, "", 0};
|
out = {true, CommandId::ScrollDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
#endif
|
#endif
|
||||||
// React to left button click/press
|
// React to left button click/press
|
||||||
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
if (ev.bstate & (BUTTON1_CLICKED | BUTTON1_PRESSED | BUTTON1_RELEASED)) {
|
||||||
char buf[64];
|
char buf[64];
|
||||||
// Use screen coordinates; command handler will translate via offsets
|
// Use screen coordinates; command handler will translate via offsets
|
||||||
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
std::snprintf(buf, sizeof(buf), "@%d:%d", ev.y, ev.x);
|
||||||
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
out = {true, CommandId::MoveCursorTo, std::string(buf), 0};
|
||||||
return true;
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
// No actionable mouse event
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
// No actionable mouse event
|
case KEY_LEFT:
|
||||||
out.hasCommand = false;
|
k_prefix = false;
|
||||||
return true;
|
k_ctrl_pending = false;
|
||||||
}
|
out = {true, CommandId::MoveLeft, "", 0};
|
||||||
case KEY_LEFT:
|
return true;
|
||||||
k_prefix = false;
|
case KEY_RIGHT:
|
||||||
out = {true, CommandId::MoveLeft, "", 0};
|
k_prefix = false;
|
||||||
return true;
|
k_ctrl_pending = false;
|
||||||
case KEY_RIGHT:
|
out = {true, CommandId::MoveRight, "", 0};
|
||||||
k_prefix = false;
|
return true;
|
||||||
out = {true, CommandId::MoveRight, "", 0};
|
case KEY_UP:
|
||||||
return true;
|
k_prefix = false;
|
||||||
case KEY_UP:
|
k_ctrl_pending = false;
|
||||||
k_prefix = false;
|
out = {true, CommandId::MoveUp, "", 0};
|
||||||
out = {true, CommandId::MoveUp, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_DOWN:
|
||||||
case KEY_DOWN:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::MoveDown, "", 0};
|
out = {true, CommandId::MoveDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case KEY_HOME:
|
case KEY_HOME:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::MoveHome, "", 0};
|
k_ctrl_pending = false;
|
||||||
return true;
|
out = {true, CommandId::MoveHome, "", 0};
|
||||||
case KEY_END:
|
return true;
|
||||||
k_prefix = false;
|
case KEY_END:
|
||||||
out = {true, CommandId::MoveEnd, "", 0};
|
k_prefix = false;
|
||||||
return true;
|
k_ctrl_pending = false;
|
||||||
case KEY_PPAGE:
|
out = {true, CommandId::MoveEnd, "", 0};
|
||||||
k_prefix = false;
|
return true;
|
||||||
out = {true, CommandId::PageUp, "", 0};
|
case KEY_PPAGE:
|
||||||
return true;
|
k_prefix = false;
|
||||||
case KEY_NPAGE:
|
k_ctrl_pending = false;
|
||||||
k_prefix = false;
|
out = {true, CommandId::PageUp, "", 0};
|
||||||
out = {true, CommandId::PageDown, "", 0};
|
return true;
|
||||||
return true;
|
case KEY_NPAGE:
|
||||||
case KEY_DC:
|
k_prefix = false;
|
||||||
k_prefix = false;
|
k_ctrl_pending = false;
|
||||||
out = {true, CommandId::DeleteChar, "", 0};
|
out = {true, CommandId::PageDown, "", 0};
|
||||||
return true;
|
return true;
|
||||||
case KEY_RESIZE:
|
case KEY_DC:
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::Refresh, "", 0};
|
k_ctrl_pending = false;
|
||||||
return true;
|
out = {true, CommandId::DeleteChar, "", 0};
|
||||||
default:
|
return true;
|
||||||
break;
|
case KEY_RESIZE:
|
||||||
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Refresh, "", 0};
|
||||||
|
return true;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
// ESC as cancel of prefix; many terminals send meta sequences as ESC+...
|
||||||
if (ch == 27) {
|
if (ch == 27) {
|
||||||
// ESC
|
// ESC
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
esc_meta = true; // next key will be considered meta-modified
|
esc_meta = true; // next key will be considered meta-modified
|
||||||
out.hasCommand = false; // no command yet
|
out.hasCommand = false; // no command yet
|
||||||
return true;
|
return true;
|
||||||
@@ -119,59 +127,33 @@ map_key_to_command(const int ch,
|
|||||||
// Control keys
|
// Control keys
|
||||||
if (ch == CTRL('K')) {
|
if (ch == CTRL('K')) {
|
||||||
// C-k prefix
|
// C-k prefix
|
||||||
k_prefix = true;
|
k_prefix = true;
|
||||||
out = {true, CommandId::KPrefix, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::KPrefix, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
if (ch == CTRL('G')) {
|
if (ch == CTRL('G')) {
|
||||||
// cancel
|
// cancel
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
esc_meta = false;
|
k_ctrl_pending = false;
|
||||||
|
esc_meta = false;
|
||||||
// cancel universal argument as well
|
// cancel universal argument as well
|
||||||
uarg_active = false;
|
if (ed)
|
||||||
uarg_collecting = false;
|
ed->UArgClear();
|
||||||
uarg_negative = false;
|
|
||||||
uarg_had_digits = false;
|
|
||||||
uarg_value = 0;
|
|
||||||
uarg_text.clear();
|
|
||||||
out = {true, CommandId::Refresh, "", 0};
|
out = {true, CommandId::Refresh, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Universal argument: C-u
|
// Universal argument: C-u
|
||||||
if (ch == CTRL('U')) {
|
if (ch == CTRL('U')) {
|
||||||
// Start or extend universal argument
|
if (ed)
|
||||||
if (!uarg_active) {
|
ed->UArgStart();
|
||||||
uarg_active = true;
|
out.hasCommand = false; // C-u itself doesn't issue a command
|
||||||
uarg_collecting = true;
|
|
||||||
uarg_negative = false;
|
|
||||||
uarg_had_digits = false;
|
|
||||||
uarg_value = 4; // default
|
|
||||||
// Reset collected text and emit status update
|
|
||||||
uarg_text.clear();
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
} else if (uarg_collecting && !uarg_had_digits && !uarg_negative) {
|
|
||||||
// Bare repeated C-u multiplies by 4
|
|
||||||
if (uarg_value <= 0)
|
|
||||||
uarg_value = 4;
|
|
||||||
else
|
|
||||||
uarg_value *= 4;
|
|
||||||
// Keep showing status (no digits yet)
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
} else {
|
|
||||||
// If digits or '-' have been entered, C-u ends the argument (ready for next command)
|
|
||||||
uarg_collecting = false;
|
|
||||||
if (!uarg_had_digits && !uarg_negative && uarg_value <= 0)
|
|
||||||
uarg_value = 4;
|
|
||||||
}
|
|
||||||
// No command produced by C-u itself
|
|
||||||
out.hasCommand = false;
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
// Tab (note: terminals encode Tab and C-i as the same code 9)
|
||||||
if (ch == '\t') {
|
if (ch == '\t') {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
|
k_ctrl_pending = false;
|
||||||
out.hasCommand = true;
|
out.hasCommand = true;
|
||||||
out.id = CommandId::InsertText;
|
out.id = CommandId::InsertText;
|
||||||
out.arg = "\t";
|
out.arg = "\t";
|
||||||
@@ -182,22 +164,39 @@ map_key_to_command(const int ch,
|
|||||||
// IMPORTANT: if we're in k-prefix, the very next key must be interpreted
|
// IMPORTANT: if we're in k-prefix, the very next key must be interpreted
|
||||||
// via the C-k keymap first, even if it's a Control chord like C-d.
|
// via the C-k keymap first, even if it's a Control chord like C-d.
|
||||||
if (k_prefix) {
|
if (k_prefix) {
|
||||||
k_prefix = false; // consume the prefix for this one key
|
// In k-prefix: allow a control qualifier via literal 'C' or '^'
|
||||||
|
// Detect Control keycodes first
|
||||||
bool ctrl = false;
|
bool ctrl = false;
|
||||||
int ascii_key = ch;
|
int ascii_key = ch;
|
||||||
if (ch >= 1 && ch <= 26) {
|
if (ch >= 1 && ch <= 26) {
|
||||||
ctrl = true;
|
ctrl = true;
|
||||||
ascii_key = 'a' + (ch - 1);
|
ascii_key = 'a' + (ch - 1);
|
||||||
}
|
}
|
||||||
|
// If user typed literal 'C'/'c' or '^' as a qualifier, keep k-prefix and set pending
|
||||||
|
if (ascii_key == 'C' || ascii_key == 'c' || ascii_key == '^') {
|
||||||
|
k_ctrl_pending = true;
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus("C-k C _");
|
||||||
|
out.hasCommand = false;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
// For actual suffix, consume the k-prefix
|
||||||
|
k_prefix = false;
|
||||||
// Do NOT lowercase here; KLookupKCommand handles case-sensitive bindings
|
// Do NOT lowercase here; KLookupKCommand handles case-sensitive bindings
|
||||||
CommandId id;
|
CommandId id;
|
||||||
if (KLookupKCommand(ascii_key, ctrl, id)) {
|
bool pass_ctrl = (ctrl || k_ctrl_pending);
|
||||||
|
k_ctrl_pending = false;
|
||||||
|
if (KLookupKCommand(ascii_key, pass_ctrl, id)) {
|
||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus(""); // clear "C-k _" hint after suffix
|
||||||
} else {
|
} else {
|
||||||
int shown = KLowerAscii(ascii_key);
|
int shown = KLowerAscii(ascii_key);
|
||||||
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
char c = (shown >= 0x20 && shown <= 0x7e) ? static_cast<char>(shown) : '?';
|
||||||
std::string arg(1, c);
|
std::string arg(1, c);
|
||||||
out = {true, CommandId::UnknownKCommand, arg, 0};
|
out = {true, CommandId::UnknownKCommand, arg, 0};
|
||||||
|
if (ed)
|
||||||
|
ed->SetStatus(""); // clear hint; handler will set unknown status
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -213,8 +212,9 @@ map_key_to_command(const int ch,
|
|||||||
|
|
||||||
// Enter
|
// Enter
|
||||||
if (ch == '\n' || ch == '\r') {
|
if (ch == '\n' || ch == '\r') {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::Newline, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Newline, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// If previous key was ESC, interpret as meta and use ESC keymap
|
// If previous key was ESC, interpret as meta and use ESC keymap
|
||||||
@@ -224,6 +224,12 @@ map_key_to_command(const int ch,
|
|||||||
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
// Handle ESC + BACKSPACE (meta-backspace, Alt-Backspace)
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
||||||
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
ascii_key = KEY_BACKSPACE; // normalized value for lookup
|
||||||
|
} else if (ch == ',') {
|
||||||
|
// Some terminals emit ',' when Shift state is lost after ESC; treat as '<'
|
||||||
|
ascii_key = '<';
|
||||||
|
} else if (ch == '.') {
|
||||||
|
// Likewise, map '.' to '>'
|
||||||
|
ascii_key = '>';
|
||||||
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
|
} else if (ascii_key >= 'A' && ascii_key <= 'Z') {
|
||||||
ascii_key = ascii_key - 'A' + 'a';
|
ascii_key = ascii_key - 'A' + 'a';
|
||||||
}
|
}
|
||||||
@@ -232,48 +238,26 @@ map_key_to_command(const int ch,
|
|||||||
out = {true, id, "", 0};
|
out = {true, id, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
// Unhandled meta key: no command
|
// Unhandled ESC sequence: exit escape mode and show status
|
||||||
out.hasCommand = false;
|
out = {true, CommandId::UnknownEscCommand, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
// Backspace in ncurses can be KEY_BACKSPACE or 127
|
||||||
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
if (ch == KEY_BACKSPACE || ch == 127 || ch == CTRL('H')) {
|
||||||
k_prefix = false;
|
k_prefix = false;
|
||||||
out = {true, CommandId::Backspace, "", 0};
|
k_ctrl_pending = false;
|
||||||
|
out = {true, CommandId::Backspace, "", 0};
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
// k_prefix handled earlier
|
// k_prefix handled earlier
|
||||||
|
|
||||||
// If collecting universal arg, handle digits and optional leading '-'
|
// If universal argument is active at editor level and we get a digit, feed it
|
||||||
if (uarg_active && uarg_collecting) {
|
if (ed && ed->UArg() != 0 && ch >= '0' && ch <= '9') {
|
||||||
if (ch >= '0' && ch <= '9') {
|
ed->UArgDigit(ch - '0');
|
||||||
int d = ch - '0';
|
out.hasCommand = false; // keep collecting, no command yet
|
||||||
if (!uarg_had_digits) {
|
return true;
|
||||||
// First digit overrides any 4^n default
|
|
||||||
uarg_value = 0;
|
|
||||||
uarg_had_digits = true;
|
|
||||||
}
|
|
||||||
if (uarg_value < 100000000) {
|
|
||||||
// avoid overflow
|
|
||||||
uarg_value = uarg_value * 10 + d;
|
|
||||||
}
|
|
||||||
// Update raw text and status to reflect collected digits
|
|
||||||
uarg_text.push_back(static_cast<char>(ch));
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (ch == '-' && !uarg_had_digits && !uarg_negative) {
|
|
||||||
uarg_negative = true;
|
|
||||||
// Show leading minus in status
|
|
||||||
uarg_text = "-";
|
|
||||||
out = {true, CommandId::UArgStatus, uarg_text, 0};
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
// Any other key will be processed as a command; fall through to mapping below
|
|
||||||
// but mark collection finished so we apply the argument to that command
|
|
||||||
uarg_collecting = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Printable ASCII
|
// Printable ASCII
|
||||||
@@ -300,29 +284,11 @@ TerminalInputHandler::decode_(MappedInput &out)
|
|||||||
bool consumed = map_key_to_command(
|
bool consumed = map_key_to_command(
|
||||||
ch,
|
ch,
|
||||||
k_prefix_, esc_meta_,
|
k_prefix_, esc_meta_,
|
||||||
uarg_active_, uarg_collecting_, uarg_negative_, uarg_had_digits_, uarg_value_, uarg_text_,
|
k_ctrl_pending_,
|
||||||
|
ed_,
|
||||||
out);
|
out);
|
||||||
if (!consumed)
|
if (!consumed)
|
||||||
return false;
|
return false;
|
||||||
// If a command was produced and a universal argument is active, attach it and clear state
|
|
||||||
if (out.hasCommand && uarg_active_ && out.id != CommandId::UArgStatus) {
|
|
||||||
int count = 0;
|
|
||||||
if (!uarg_had_digits_ && !uarg_negative_) {
|
|
||||||
// No explicit digits: use current value (default 4 or 4^n)
|
|
||||||
count = (uarg_value_ > 0) ? uarg_value_ : 4;
|
|
||||||
} else {
|
|
||||||
count = uarg_value_;
|
|
||||||
if (uarg_negative_)
|
|
||||||
count = -count;
|
|
||||||
}
|
|
||||||
out.count = count;
|
|
||||||
// Clear state
|
|
||||||
uarg_active_ = false;
|
|
||||||
uarg_collecting_ = false;
|
|
||||||
uarg_negative_ = false;
|
|
||||||
uarg_had_digits_ = false;
|
|
||||||
uarg_value_ = 0;
|
|
||||||
}
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,13 @@ public:
|
|||||||
|
|
||||||
~TerminalInputHandler() override;
|
~TerminalInputHandler() override;
|
||||||
|
|
||||||
|
|
||||||
|
void Attach(Editor *ed) override
|
||||||
|
{
|
||||||
|
ed_ = ed;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
bool Poll(MappedInput &out) override;
|
bool Poll(MappedInput &out) override;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
@@ -18,14 +25,10 @@ private:
|
|||||||
|
|
||||||
// ke-style prefix state
|
// ke-style prefix state
|
||||||
bool k_prefix_ = false; // true after C-k until next key or ESC
|
bool k_prefix_ = false; // true after C-k until next key or ESC
|
||||||
|
// Optional control qualifier inside k-prefix (e.g., user typed literal 'C' or '^')
|
||||||
|
bool k_ctrl_pending_ = false;
|
||||||
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
// Simple meta (ESC) state for ESC sequences like ESC b/f
|
||||||
bool esc_meta_ = false;
|
bool esc_meta_ = false;
|
||||||
|
|
||||||
// Universal argument (C-u) state
|
Editor *ed_ = nullptr; // attached editor for uarg handling
|
||||||
bool uarg_active_ = false; // an argument is pending for the next command
|
|
||||||
bool uarg_collecting_ = false; // collecting digits / '-' right now
|
|
||||||
bool uarg_negative_ = false; // whether a leading '-' was supplied
|
|
||||||
bool uarg_had_digits_ = false; // whether any digits were supplied
|
|
||||||
int uarg_value_ = 0; // current absolute value (>=0)
|
|
||||||
std::string uarg_text_; // raw digits/minus typed for status display
|
|
||||||
};
|
};
|
||||||
@@ -111,19 +111,44 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
std::string line = static_cast<std::string>(lines[li]);
|
std::string line = static_cast<std::string>(lines[li]);
|
||||||
src_i = 0;
|
src_i = 0;
|
||||||
render_col = 0;
|
render_col = 0;
|
||||||
// Syntax highlighting: fetch per-line spans
|
// Syntax highlighting: fetch per-line spans (sanitized copy)
|
||||||
const kte::LineHighlight *lh_ptr = nullptr;
|
std::vector<kte::HighlightSpan> sane_spans;
|
||||||
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
if (buf->SyntaxEnabled() && buf->Highlighter() && buf->Highlighter()->
|
||||||
HasHighlighter()) {
|
HasHighlighter()) {
|
||||||
lh_ptr = &buf->Highlighter()->GetLine(
|
kte::LineHighlight lh_val = buf->Highlighter()->GetLine(
|
||||||
*buf, static_cast<int>(li), buf->Version());
|
*buf, static_cast<int>(li), buf->Version());
|
||||||
|
// Sanitize defensively: clamp to [0, line.size()], ensure end>=start, drop empties
|
||||||
|
const std::size_t line_len = line.size();
|
||||||
|
sane_spans.reserve(lh_val.spans.size());
|
||||||
|
for (const auto &sp: lh_val.spans) {
|
||||||
|
int s_raw = sp.col_start;
|
||||||
|
int e_raw = sp.col_end;
|
||||||
|
if (e_raw < s_raw)
|
||||||
|
std::swap(e_raw, s_raw);
|
||||||
|
std::size_t s = static_cast<std::size_t>(std::max(
|
||||||
|
0, std::min(s_raw, static_cast<int>(line_len))));
|
||||||
|
std::size_t e = static_cast<std::size_t>(std::max(
|
||||||
|
static_cast<int>(s),
|
||||||
|
std::min(e_raw, static_cast<int>(line_len))));
|
||||||
|
if (e <= s)
|
||||||
|
continue;
|
||||||
|
sane_spans.push_back(kte::HighlightSpan{
|
||||||
|
static_cast<int>(s), static_cast<int>(e), sp.kind
|
||||||
|
});
|
||||||
|
}
|
||||||
|
std::sort(sane_spans.begin(), sane_spans.end(),
|
||||||
|
[](const kte::HighlightSpan &a, const kte::HighlightSpan &b) {
|
||||||
|
return a.col_start < b.col_start;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
auto token_at = [&](std::size_t src_index) -> kte::TokenKind {
|
||||||
if (!lh_ptr)
|
if (sane_spans.empty())
|
||||||
return kte::TokenKind::Default;
|
return kte::TokenKind::Default;
|
||||||
for (const auto &sp: lh_ptr->spans) {
|
int si = static_cast<int>(src_index);
|
||||||
if (static_cast<int>(src_index) >= sp.col_start && static_cast<int>(
|
for (const auto &sp: sane_spans) {
|
||||||
src_index) < sp.col_end)
|
if (si < sp.col_start)
|
||||||
|
break;
|
||||||
|
if (si >= sp.col_start && si < sp.col_end)
|
||||||
return sp.kind;
|
return sp.kind;
|
||||||
}
|
}
|
||||||
return kte::TokenKind::Default;
|
return kte::TokenKind::Default;
|
||||||
@@ -132,23 +157,23 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
// Map to simple attributes; search highlight uses A_STANDOUT which takes precedence below
|
||||||
attrset(A_NORMAL);
|
attrset(A_NORMAL);
|
||||||
switch (k) {
|
switch (k) {
|
||||||
case kte::TokenKind::Keyword:
|
case kte::TokenKind::Keyword:
|
||||||
case kte::TokenKind::Type:
|
case kte::TokenKind::Type:
|
||||||
case kte::TokenKind::Constant:
|
case kte::TokenKind::Constant:
|
||||||
case kte::TokenKind::Function:
|
case kte::TokenKind::Function:
|
||||||
attron(A_BOLD);
|
attron(A_BOLD);
|
||||||
break;
|
break;
|
||||||
case kte::TokenKind::Comment:
|
case kte::TokenKind::Comment:
|
||||||
attron(A_DIM);
|
attron(A_DIM);
|
||||||
break;
|
break;
|
||||||
case kte::TokenKind::String:
|
case kte::TokenKind::String:
|
||||||
case kte::TokenKind::Char:
|
case kte::TokenKind::Char:
|
||||||
case kte::TokenKind::Number:
|
case kte::TokenKind::Number:
|
||||||
// standout a bit using A_UNDERLINE if available
|
// standout a bit using A_UNDERLINE if available
|
||||||
attron(A_UNDERLINE);
|
attron(A_UNDERLINE);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
while (written < cols) {
|
while (written < cols) {
|
||||||
@@ -269,11 +294,31 @@ TerminalRenderer::Draw(Editor &ed)
|
|||||||
clrtoeol();
|
clrtoeol();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Place terminal cursor at logical position accounting for tabs and coloffs
|
// Place terminal cursor at logical position accounting for tabs and coloffs.
|
||||||
|
// Recompute the rendered X using the same logic as the drawing loop to avoid
|
||||||
|
// any drift between the command-layer computation and the terminal renderer.
|
||||||
std::size_t cy = buf->Cury();
|
std::size_t cy = buf->Cury();
|
||||||
std::size_t rx = buf->Rx(); // render x computed by command layer
|
std::size_t cx = buf->Curx();
|
||||||
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
int cur_y = static_cast<int>(cy) - static_cast<int>(buf->Rowoffs());
|
||||||
int cur_x = static_cast<int>(rx) - static_cast<int>(buf->Coloffs());
|
std::size_t rx_recomputed = 0;
|
||||||
|
if (cy < lines.size()) {
|
||||||
|
const std::string line_for_cursor = static_cast<std::string>(lines[cy]);
|
||||||
|
std::size_t src_i_cur = 0;
|
||||||
|
std::size_t render_col_cur = 0;
|
||||||
|
while (src_i_cur < line_for_cursor.size() && src_i_cur < cx) {
|
||||||
|
unsigned char ccur = static_cast<unsigned char>(line_for_cursor[src_i_cur]);
|
||||||
|
if (ccur == '\t') {
|
||||||
|
std::size_t next_tab = tabw - (render_col_cur % tabw);
|
||||||
|
render_col_cur += next_tab;
|
||||||
|
++src_i_cur;
|
||||||
|
} else {
|
||||||
|
++render_col_cur;
|
||||||
|
++src_i_cur;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rx_recomputed = render_col_cur;
|
||||||
|
}
|
||||||
|
int cur_x = static_cast<int>(rx_recomputed) - static_cast<int>(buf->Coloffs());
|
||||||
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
if (cur_y >= 0 && cur_y < content_rows && cur_x >= 0 && cur_x < cols) {
|
||||||
// remember where to leave the terminal cursor after status is drawn
|
// remember where to leave the terminal cursor after status is drawn
|
||||||
saved_cur_y = cur_y;
|
saved_cur_y = cur_y;
|
||||||
|
|||||||
@@ -23,5 +23,9 @@
|
|||||||
<key>LSMinimumSystemVersion</key>
|
<key>LSMinimumSystemVersion</key>
|
||||||
<string>10.13</string>
|
<string>10.13</string>
|
||||||
<key>NSHighResolutionCapable</key>
|
<key>NSHighResolutionCapable</key>
|
||||||
|
<true/>
|
||||||
|
<!-- Allow running multiple instances of the app -->
|
||||||
|
<key>LSMultipleInstancesProhibited</key>
|
||||||
|
<false/>
|
||||||
</dict>
|
</dict>
|
||||||
</plist>
|
</plist>
|
||||||
@@ -9,6 +9,7 @@
|
|||||||
installShellFiles,
|
installShellFiles,
|
||||||
|
|
||||||
graphical ? false,
|
graphical ? false,
|
||||||
|
graphical-qt ? false,
|
||||||
...
|
...
|
||||||
}:
|
}:
|
||||||
let
|
let
|
||||||
@@ -34,10 +35,15 @@ stdenv.mkDerivation {
|
|||||||
SDL2
|
SDL2
|
||||||
libGL
|
libGL
|
||||||
xorg.libX11
|
xorg.libX11
|
||||||
|
]
|
||||||
|
++ lib.optionals graphical-qt [
|
||||||
|
qt5Full
|
||||||
|
qtcreator ## not sure if this is actually needed
|
||||||
];
|
];
|
||||||
|
|
||||||
cmakeFlags = [
|
cmakeFlags = [
|
||||||
"-DBUILD_GUI=${if graphical then "ON" else "OFF"}"
|
"-DBUILD_GUI=${if graphical or graphical-qt then "ON" else "OFF"}"
|
||||||
|
"-DKTE_USE_QT=${if graphical-qt then "ON" else "OFF"}"
|
||||||
"-DCMAKE_BUILD_TYPE=Debug"
|
"-DCMAKE_BUILD_TYPE=Debug"
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -2,27 +2,43 @@
|
|||||||
|
|
||||||
## Overview
|
## Overview
|
||||||
|
|
||||||
`TestFrontend` is a headless implementation of the `Frontend` interface designed to facilitate programmatic testing of editor features. It allows you to queue commands and text input manually, execute them step-by-step, and inspect the editor/buffer state.
|
`TestFrontend` is a headless implementation of the `Frontend` interface
|
||||||
|
designed to facilitate programmatic testing of editor features. It
|
||||||
|
allows you to queue commands and text input manually, execute them
|
||||||
|
step-by-step, and inspect the editor/buffer state.
|
||||||
|
|
||||||
## Components
|
## Components
|
||||||
|
|
||||||
### TestInputHandler
|
### TestInputHandler
|
||||||
|
|
||||||
A programmable input handler that uses a queue-based system:
|
A programmable input handler that uses a queue-based system:
|
||||||
- `QueueCommand(CommandId id, const std::string &arg = "", int count = 0)` - Queue a specific command
|
|
||||||
- `QueueText(const std::string &text)` - Queue text for insertion (character by character)
|
-
|
||||||
|
`QueueCommand(CommandId id, const std::string &arg = "", int count = 0)` -
|
||||||
|
Queue a specific command
|
||||||
|
- `QueueText(const std::string &text)` - Queue text for insertion (
|
||||||
|
character by character)
|
||||||
- `Poll(MappedInput &out)` - Returns queued commands one at a time
|
- `Poll(MappedInput &out)` - Returns queued commands one at a time
|
||||||
- `IsEmpty()` - Check if the input queue is empty
|
- `IsEmpty()` - Check if the input queue is empty
|
||||||
|
|
||||||
### TestRenderer
|
### TestRenderer
|
||||||
|
|
||||||
A minimal no-op renderer for testing:
|
A minimal no-op renderer for testing:
|
||||||
- `Draw(Editor &ed)` - No-op implementation, just increments draw counter
|
|
||||||
|
- `Draw(Editor &ed)` - No-op implementation, just increments draw
|
||||||
|
counter
|
||||||
- `GetDrawCount()` - Returns the number of times Draw() was called
|
- `GetDrawCount()` - Returns the number of times Draw() was called
|
||||||
- `ResetDrawCount()` - Resets the draw counter
|
- `ResetDrawCount()` - Resets the draw counter
|
||||||
|
|
||||||
### TestFrontend
|
### TestFrontend
|
||||||
The main frontend class that integrates TestInputHandler and TestRenderer:
|
|
||||||
- `Init(Editor &ed)` - Initializes the frontend (sets editor dimensions to 24x80)
|
The main frontend class that integrates TestInputHandler and
|
||||||
- `Step(Editor &ed, bool &running)` - Processes one command from the queue and renders
|
TestRenderer:
|
||||||
|
|
||||||
|
- `Init(Editor &ed)` - Initializes the frontend (sets editor dimensions
|
||||||
|
to 24x80)
|
||||||
|
- `Step(Editor &ed, bool &running)` - Processes one command from the
|
||||||
|
queue and renders
|
||||||
- `Shutdown()` - Cleanup (no-op for TestFrontend)
|
- `Shutdown()` - Cleanup (no-op for TestFrontend)
|
||||||
- `Input()` - Access the TestInputHandler
|
- `Input()` - Access the TestInputHandler
|
||||||
- `Renderer()` - Access the TestRenderer
|
- `Renderer()` - Access the TestRenderer
|
||||||
@@ -75,31 +91,55 @@ int main() {
|
|||||||
|
|
||||||
## Key Features
|
## Key Features
|
||||||
|
|
||||||
1. **Programmable Input**: Queue any sequence of commands or text programmatically
|
1. **Programmable Input**: Queue any sequence of commands or text
|
||||||
|
programmatically
|
||||||
2. **Step-by-Step Execution**: Run the editor one command at a time
|
2. **Step-by-Step Execution**: Run the editor one command at a time
|
||||||
3. **State Inspection**: Access and verify editor/buffer state between commands
|
3. **State Inspection**: Access and verify editor/buffer state between
|
||||||
4. **No UI Dependencies**: Headless operation, no terminal or GUI required
|
commands
|
||||||
5. **Integration Testing**: Test command sequences, undo/redo, multi-line editing, etc.
|
4. **No UI Dependencies**: Headless operation, no terminal or GUI
|
||||||
|
required
|
||||||
|
5. **Integration Testing**: Test command sequences, undo/redo,
|
||||||
|
multi-line editing, etc.
|
||||||
|
|
||||||
## Available Commands
|
## Available Commands
|
||||||
|
|
||||||
All commands from `CommandId` enum can be queued, including:
|
All commands from `CommandId` enum can be queued, including:
|
||||||
|
|
||||||
- `CommandId::InsertText` - Insert text (use `QueueText()` helper)
|
- `CommandId::InsertText` - Insert text (use `QueueText()` helper)
|
||||||
- `CommandId::Newline` - Insert newline
|
- `CommandId::Newline` - Insert newline
|
||||||
- `CommandId::Backspace` - Delete character before cursor
|
- `CommandId::Backspace` - Delete character before cursor
|
||||||
- `CommandId::DeleteChar` - Delete character at cursor
|
- `CommandId::DeleteChar` - Delete character at cursor
|
||||||
- `CommandId::MoveLeft`, `MoveRight`, `MoveUp`, `MoveDown` - Cursor movement
|
- `CommandId::MoveLeft`, `MoveRight`, `MoveUp`, `MoveDown` - Cursor
|
||||||
|
movement
|
||||||
- `CommandId::Undo`, `CommandId::Redo` - Undo/redo operations
|
- `CommandId::Undo`, `CommandId::Redo` - Undo/redo operations
|
||||||
- `CommandId::Save`, `CommandId::Quit` - File operations
|
- `CommandId::Save`, `CommandId::Quit` - File operations
|
||||||
- And many more (see Command.h)
|
- And many more (see Command.h)
|
||||||
|
|
||||||
## Integration
|
## Integration
|
||||||
|
|
||||||
TestFrontend is built into both `kte` and `kge` executables as part of the common source files. You can create standalone test programs by linking against the same source files and ncurses.
|
TestFrontend is built into both `kte` and `kge` executables as part of
|
||||||
|
the common source files. You can create standalone test programs by
|
||||||
|
linking against the same source files and ncurses.
|
||||||
|
|
||||||
## Notes
|
## Notes
|
||||||
|
|
||||||
- Always call `InstallDefaultCommands()` before using any commands
|
- Always call `InstallDefaultCommands()` before using any commands
|
||||||
- Buffer must be initialized (via `OpenFile()` or `AddBuffer()`) before queuing edit commands
|
- Buffer must be initialized (via `OpenFile()` or `AddBuffer()`) before
|
||||||
|
queuing edit commands
|
||||||
- Undo/redo requires the buffer to have an UndoSystem attached
|
- Undo/redo requires the buffer to have an UndoSystem attached
|
||||||
- The test frontend sets editor dimensions to 24x80 by default
|
- The test frontend sets editor dimensions to 24x80 by default
|
||||||
|
|
||||||
|
## Highlighter stress harness
|
||||||
|
|
||||||
|
For renderer/highlighter race testing without a UI, `kte` provides a
|
||||||
|
lightweight stress mode:
|
||||||
|
|
||||||
|
```
|
||||||
|
kte --stress-highlighter=5
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs a short synthetic workload (5 seconds by default) that edits
|
||||||
|
and scrolls a buffer while
|
||||||
|
exercising `HighlighterEngine::PrefetchViewport` and `GetLine`
|
||||||
|
concurrently. Use Debug builds with
|
||||||
|
AddressSanitizer enabled for best effect.
|
||||||
|
|||||||
124
docs/plans/qt-frontend.md
Normal file
124
docs/plans/qt-frontend.md
Normal file
@@ -0,0 +1,124 @@
|
|||||||
|
Based on the project structure and the presence of files like
|
||||||
|
`imgui.ini`, `GUIFrontend.h`, and `TerminalFrontend.h`, here is an
|
||||||
|
analysis of the difficulty and challenges involved in adding a GTK or Qt
|
||||||
|
version of the GUI.
|
||||||
|
|
||||||
|
### **Executive Summary: Difficulty Level - Moderate**
|
||||||
|
|
||||||
|
The project is well-architected for this task. It already supports
|
||||||
|
multiple frontends (Terminal vs. GUI), meaning the "Core Logic" (
|
||||||
|
Buffers, Syntax, Commands) is successfully decoupled from the "View" (
|
||||||
|
Rendering/Input). However, the specific move from an **Immediate Mode**
|
||||||
|
GUI (likely Dear ImGui, implied by `imgui.ini` and standard naming
|
||||||
|
patterns) to a **Retained Mode** GUI (Qt/GTK) introduces specific
|
||||||
|
architectural frictions regarding the event loop and state management.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### **1. Architectural Analysis**
|
||||||
|
|
||||||
|
The existence of abstract interfaces—likely `Frontend`, `Renderer`, and
|
||||||
|
`InputHandler`—is the biggest asset here.
|
||||||
|
|
||||||
|
* **Current State:**
|
||||||
|
* **Abstract Layer:** `Frontend.h`, `Renderer.h`, `InputHandler.h`
|
||||||
|
likely define the contract.
|
||||||
|
* **Implementations:**
|
||||||
|
* `Terminal*` files implement the TUI (likely ncurses or VT100).
|
||||||
|
* `GUI*` files (currently ImGui) implement the graphical
|
||||||
|
version.
|
||||||
|
* **The Path Forward:**
|
||||||
|
* You would create `QtFrontend`, `QtRenderer`, `QtInputHandler` (or
|
||||||
|
GTK equivalents).
|
||||||
|
* Because the core logic (`Editor.cc`, `Buffer.cc`) calls these
|
||||||
|
interfaces, you theoretically don't need to touch the core text
|
||||||
|
manipulation code.
|
||||||
|
|
||||||
|
### **2. Key Challenges**
|
||||||
|
|
||||||
|
#### **A. The Event Loop Inversion (Main Challenge)**
|
||||||
|
|
||||||
|
* **Current (ImGui):** Typically, the application owns the loop:
|
||||||
|
`while (running) { HandleInput(); Update(); Render(); }`. The
|
||||||
|
application explicitly tells the GUI to draw every frame.
|
||||||
|
* **Target (Qt/GTK):** The framework owns the loop: `app.exec()` or
|
||||||
|
`gtk_main()`. The framework calls *you* when events happen.
|
||||||
|
* **Difficulty:** You will need to refactor `main.cc` or the entry point
|
||||||
|
to hand over control to the Qt/GTK application object. The Editor's "
|
||||||
|
tick" function might need to be connected to a timer or an idle event
|
||||||
|
in the new framework to ensure logic updates happen.
|
||||||
|
|
||||||
|
#### **B. Rendering Paradigm: Canvas vs. Widgets**
|
||||||
|
|
||||||
|
* **The "Easy" Way (Custom Canvas):**
|
||||||
|
* Implement the `QtRenderer` by subclassing `QWidget` and overriding
|
||||||
|
`paintEvent`.
|
||||||
|
* Use `QPainter` (or Cairo in GTK) to draw text, cursors, and
|
||||||
|
selections exactly where the `Renderer` interface says to.
|
||||||
|
* **Pros:** Keeps the code similar to the current ImGui/Terminal
|
||||||
|
renderers.
|
||||||
|
* **Cons:** You lose native accessibility and some native "feel" (
|
||||||
|
scrolling physics, native text context menus).
|
||||||
|
* **The "Hard" Way (Native Widgets):**
|
||||||
|
* Trying to map an internal `Buffer` directly to a `QTextEdit` or
|
||||||
|
`GtkTextView`.
|
||||||
|
* **Difficulty:** This is usually very hard because the Editor core
|
||||||
|
likely manages its own cursor, selection, and syntax highlighting.
|
||||||
|
Syncing that internal state with a complex native widget often
|
||||||
|
leads to conflicts.
|
||||||
|
* **Recommendation:** Stick to the "Custom Canvas" approach (drawing
|
||||||
|
text manually on a surface) to preserve the custom editor
|
||||||
|
behavior (vim-like modes, specific syntax highlighting).
|
||||||
|
|
||||||
|
#### **C. Input Handling**
|
||||||
|
|
||||||
|
* **Challenge:** Mapping Qt/GTK key events to the internal `Keymap`.
|
||||||
|
* **Detail:** ImGui and Terminal libraries often provide raw scancodes
|
||||||
|
or simple chars. Qt/GTK provide complex Event objects. You will need a
|
||||||
|
translation layer in `QtInputHandler::keyPressEvent` that converts
|
||||||
|
`Qt::Key_Escape` -> `KKey::Escape` (or your internal equivalent).
|
||||||
|
|
||||||
|
### **3. Portability of Assets**
|
||||||
|
|
||||||
|
#### **Themes (Colors)**
|
||||||
|
|
||||||
|
* **Feasibility:** High.
|
||||||
|
* **Approach:** `GUITheme.h` likely contains structs with RGB/Hex
|
||||||
|
values. Qt supports stylesheets (QSS) and GTK uses CSS. You can write
|
||||||
|
a converter that reads your current theme configuration and generates
|
||||||
|
a CSS string to apply to your window, or simply use the RGB values
|
||||||
|
directly in your custom `QPainter`/Cairo drawing logic.
|
||||||
|
|
||||||
|
#### **Fonts**
|
||||||
|
|
||||||
|
* **Feasibility:** Moderate.
|
||||||
|
* **Approach:**
|
||||||
|
* **ImGui:** Usually loads a TTF into a texture atlas.
|
||||||
|
* **Qt/GTK:** Uses the system font engine (Freetype/Pango).
|
||||||
|
* **Challenge:** You won't use the texture atlas anymore. You will
|
||||||
|
simply request a font family and size (e.g.,
|
||||||
|
`QFont("JetBrains Mono", 12)`). You may need to ensure your custom
|
||||||
|
renderer calculates character width/height metrics correctly using
|
||||||
|
`QFontMetrics` (Qt) or `PangoLayout` (GTK) to align the grid
|
||||||
|
correctly.
|
||||||
|
|
||||||
|
### **4. Summary Recommendation**
|
||||||
|
|
||||||
|
If you proceed, **Qt** is generally considered easier to integrate with
|
||||||
|
C++ projects than GTK (which is C-based, though `gtkmm` exists).
|
||||||
|
|
||||||
|
1. **Create a `QtFrontend`** class inheriting from `Frontend`.
|
||||||
|
2. **Create a `QtWindow`** class inheriting from `QWidget`.
|
||||||
|
3. **Implement `QtRenderer`** that holds a pointer to the `QtWindow`.
|
||||||
|
When the core calls `DrawText()`, `QtRenderer` should queue that
|
||||||
|
command or draw directly to the widget's paint buffer.
|
||||||
|
4. **Refactor `main.cc`** to instantiate `QApplication` instead of the
|
||||||
|
current manual loop.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Note (2025-12): The Qt frontend defers all key processing to the
|
||||||
|
existing command subsystem and keymaps, mirroring the ImGui path. There
|
||||||
|
are no Qt-only keybindings; `QtInputHandler` translates Qt key events
|
||||||
|
into the shared keymap flow (C-k prefix, Ctrl chords, ESC/Meta,
|
||||||
|
universal-argument digits, printable insertion).
|
||||||
144
docs/plans/swap-files.md
Normal file
144
docs/plans/swap-files.md
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
Swap files for kte — design plan
|
||||||
|
================================
|
||||||
|
|
||||||
|
Goals
|
||||||
|
-----
|
||||||
|
|
||||||
|
- Preserve user work across crashes, power failures, and OS kills.
|
||||||
|
- Keep the editor responsive; avoid blocking the UI on disk I/O.
|
||||||
|
- Bound recovery time and swap size.
|
||||||
|
- Favor simple, robust primitives that work well on POSIX and macOS;
|
||||||
|
keep Windows feasibility in mind.
|
||||||
|
|
||||||
|
Model overview
|
||||||
|
--------------
|
||||||
|
Per open buffer, maintain a sidecar swap journal next to the file:
|
||||||
|
|
||||||
|
- Path: `.<basename>.kte.swp` in the same directory as the file (for
|
||||||
|
unnamed/unsaved buffers, use a per‑session temp dir like
|
||||||
|
`$TMPDIR/kte/` with a random UUID).
|
||||||
|
- Format: append‑only journal of editing operations with periodic
|
||||||
|
checkpoints.
|
||||||
|
- Crash safety: only append, fsync as per policy; checkpoint via
|
||||||
|
write‑to‑temp + fsync + atomic rename.
|
||||||
|
|
||||||
|
File format (v1)
|
||||||
|
----------------
|
||||||
|
Header (fixed 64 bytes):
|
||||||
|
|
||||||
|
- Magic: `KTE_SWP\0` (8 bytes)
|
||||||
|
- Version: 1 (u32)
|
||||||
|
- Flags: bitset (u32) — e.g., compression, checksums, endian.
|
||||||
|
- Created time (u64)
|
||||||
|
- Host info hash (u64) — optional, for telemetry/debug.
|
||||||
|
- File identity: hash of canonical path (u64) and original file
|
||||||
|
size+mtime (u64+u64) at start.
|
||||||
|
- Reserved/padding.
|
||||||
|
|
||||||
|
Records (stream after header):
|
||||||
|
|
||||||
|
- Each record: [type u8][len u24][payload][crc32 u32]
|
||||||
|
- Types:
|
||||||
|
- `CHKPT` — full snapshot checkpoint of entire buffer content and
|
||||||
|
minimal metadata (cursor pos, filetype). Payload optionally
|
||||||
|
compressed. Written occasionally to cap replay time.
|
||||||
|
- `INS` — insert at (row, col) text bytes (text may contain
|
||||||
|
newlines). Encoded with varints.
|
||||||
|
- `DEL` — delete length at (row, col). If spanning lines, semantics
|
||||||
|
defined as in Buffer::delete_text.
|
||||||
|
- `SPLIT`, `JOIN` — explicit structural ops (optional; can be
|
||||||
|
expressed via INS/DEL).
|
||||||
|
- `META` — update metadata (e.g., filetype, encoding hints).
|
||||||
|
|
||||||
|
Durability policy
|
||||||
|
-----------------
|
||||||
|
Configurable knobs (sane defaults in parentheses):
|
||||||
|
|
||||||
|
- Time‑based flush: group edits and flush every 150–300 ms (200 ms).
|
||||||
|
- Operation count flush: after N ops (200).
|
||||||
|
- Idle flush: on 500 ms idle lull, flush immediately.
|
||||||
|
- Checkpoint cadence: after M KB of journal (512–2048 KB) or T seconds (
|
||||||
|
30–120 s), whichever first.
|
||||||
|
- fsync policy:
|
||||||
|
- `always`: fsync every flush (safest, slowest).
|
||||||
|
- `grouped` (default): fsync at most every 1–2 s or on
|
||||||
|
idle/blur/quit.
|
||||||
|
- `never`: rely on OS flush (fastest, riskier).
|
||||||
|
- On POSIX, prefer `fdatasync` when available; fall back to `fsync`.
|
||||||
|
|
||||||
|
Performance & threading
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
- Background writer thread per editor instance (shared) with a bounded
|
||||||
|
MPSC queue of per‑buffer records.
|
||||||
|
- Each Buffer has a small in‑memory journal buffer; UI thread enqueues
|
||||||
|
ops (non‑blocking) and may coalesce adjacent inserts/deletes.
|
||||||
|
- Writer batch‑writes records to the swap file, computes CRCs, and
|
||||||
|
decides checkpoint boundaries.
|
||||||
|
- Backpressure: if the queue grows beyond a high watermark, signal the
|
||||||
|
UI to start coalescing more aggressively and slow enqueue (never block
|
||||||
|
hard editing path; at worst drop optional `META`).
|
||||||
|
|
||||||
|
Recovery flow
|
||||||
|
-------------
|
||||||
|
|
||||||
|
On opening a file:
|
||||||
|
|
||||||
|
1. Detect swap sidecar `.<basename>.kte.swp`.
|
||||||
|
2. Validate header, iterate records verifying CRCs.
|
||||||
|
3. Compare recorded original file identity against actual file; if
|
||||||
|
mismatch, warn user but allow recovery (content wins).
|
||||||
|
4. Reconstruct buffer: start from the last good `CHKPT` (if any), then
|
||||||
|
replay subsequent ops. If trailing partial record encountered (EOF
|
||||||
|
mid‑record), truncate at last good offset.
|
||||||
|
5. Present a choice: Recover (load recovered buffer; keep the swap file
|
||||||
|
until user saves) or Discard (delete swap file and open clean file).
|
||||||
|
|
||||||
|
Stability & corruption mitigation
|
||||||
|
---------------------------------
|
||||||
|
|
||||||
|
- Append‑only with per‑record CRC32 guards against torn writes.
|
||||||
|
- Atomic checkpoint rotation: write `.<basename>.kte.swp.tmp`, fsync,
|
||||||
|
then rename over old `.swp`.
|
||||||
|
- Size caps: rotate or compact when `.swp` exceeds a threshold (e.g.,
|
||||||
|
64–128 MB). Compaction creates a fresh file with a single checkpoint.
|
||||||
|
- Low‑disk‑space behavior: on write failures, surface a non‑modal
|
||||||
|
warning and temporarily fall back to in‑memory only; retry
|
||||||
|
opportunistically.
|
||||||
|
|
||||||
|
Security considerations
|
||||||
|
-----------------------
|
||||||
|
|
||||||
|
- Swap files mirror buffer content, which may be sensitive. Options:
|
||||||
|
- Configurable location (same dir vs. `$XDG_STATE_HOME/kte/swap`).
|
||||||
|
- Optional per‑file encryption (future work) using OS keychain.
|
||||||
|
- Ensure permissions are 0600.
|
||||||
|
|
||||||
|
Interoperability & UX
|
||||||
|
---------------------
|
||||||
|
|
||||||
|
- Use a distinctive extension `.kte.swp` to avoid conflicts with other
|
||||||
|
editors.
|
||||||
|
- Status bar indicator when swap is active; commands to purge/compact.
|
||||||
|
- On save: do not delete swap immediately; keep until the buffer is
|
||||||
|
clean and idle for a short grace period (allows undo of accidental
|
||||||
|
external changes).
|
||||||
|
|
||||||
|
Implementation plan (staged)
|
||||||
|
----------------------------
|
||||||
|
|
||||||
|
1. Minimal journal writer (append‑only INS/DEL) with grouped fsync;
|
||||||
|
single per‑editor writer thread.
|
||||||
|
2. Reader/recovery path with CRC validation and replay.
|
||||||
|
3. Checkpoints + atomic rotation; compaction path.
|
||||||
|
4. Config surface and UI prompts; telemetry counters.
|
||||||
|
5. Optional compression and advanced coalescing.
|
||||||
|
|
||||||
|
Defaults balancing performance and stability
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
- Grouped flush with fsync every ~1 s or on idle/quit.
|
||||||
|
- Checkpoint every 1 MB or 60 s.
|
||||||
|
- Bounded queue and batch writes to minimize syscalls.
|
||||||
|
- Immediate flush on critical events (buffer close, app quit, power
|
||||||
|
source change on laptops if detectable).
|
||||||
119
docs/syntax.md
119
docs/syntax.md
@@ -4,67 +4,118 @@ Syntax highlighting in kte
|
|||||||
Overview
|
Overview
|
||||||
--------
|
--------
|
||||||
|
|
||||||
kte provides lightweight syntax highlighting with a pluggable highlighter interface. The initial implementation targets C/C++ and focuses on speed and responsiveness.
|
kte provides lightweight syntax highlighting with a pluggable
|
||||||
|
highlighter interface. The initial implementation targets C/C++ and
|
||||||
|
focuses on speed and responsiveness.
|
||||||
|
|
||||||
Core types
|
Core types
|
||||||
----------
|
----------
|
||||||
|
|
||||||
- `TokenKind` — token categories (keywords, types, strings, comments, numbers, preprocessor, operators, punctuation, identifiers, whitespace, etc.).
|
- `TokenKind` — token categories (keywords, types, strings, comments,
|
||||||
- `HighlightSpan` — a half-open column range `[col_start, col_end)` with a `TokenKind`.
|
numbers, preprocessor, operators, punctuation, identifiers,
|
||||||
- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version` used to compute it.
|
whitespace, etc.).
|
||||||
|
- `HighlightSpan` — a half-open column range `[col_start, col_end)` with
|
||||||
|
a `TokenKind`.
|
||||||
|
- `LineHighlight` — a vector of `HighlightSpan` and the buffer `version`
|
||||||
|
used to compute it.
|
||||||
|
|
||||||
Engine and caching
|
Engine and caching
|
||||||
------------------
|
------------------
|
||||||
|
|
||||||
- `HighlighterEngine` maintains a per-line cache of `LineHighlight` keyed by row and buffer version.
|
- `HighlighterEngine` maintains a per-line cache of `LineHighlight`
|
||||||
- Cache invalidation occurs when the buffer version changes or when the buffer calls `InvalidateFrom(row)`, which clears cached lines and line states from `row` downward.
|
keyed by row and buffer version.
|
||||||
- The engine supports both stateless and stateful highlighters. For stateful highlighters, it memoizes a simple per-line state and computes lines sequentially when necessary.
|
- Cache invalidation occurs when the buffer version changes or when the
|
||||||
|
buffer calls `InvalidateFrom(row)`, which clears cached lines and line
|
||||||
|
states from `row` downward.
|
||||||
|
- The engine supports both stateless and stateful highlighters. For
|
||||||
|
stateful highlighters, it memoizes a simple per-line state and
|
||||||
|
computes lines sequentially when necessary.
|
||||||
|
|
||||||
Stateful highlighters
|
Stateful highlighters
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
- `LanguageHighlighter` is the base interface for stateless per-line tokenization.
|
- `LanguageHighlighter` is the base interface for stateless per-line
|
||||||
- `StatefulHighlighter` extends it with a `LineState` and the method `HighlightLineStateful(buf, row, prev_state, out)`.
|
tokenization.
|
||||||
- The engine detects `StatefulHighlighter` via dynamic_cast and feeds each line the previous line’s state, caching the resulting state per line.
|
- `StatefulHighlighter` extends it with a `LineState` and the method
|
||||||
|
`HighlightLineStateful(buf, row, prev_state, out)`.
|
||||||
|
- The engine detects `StatefulHighlighter` via dynamic_cast and feeds
|
||||||
|
each line the previous line’s state, caching the resulting state per
|
||||||
|
line.
|
||||||
|
|
||||||
C/C++ highlighter
|
C/C++ highlighter
|
||||||
-----------------
|
-----------------
|
||||||
|
|
||||||
- `CppHighlighter` implements `StatefulHighlighter`.
|
- `CppHighlighter` implements `StatefulHighlighter`.
|
||||||
- Stateless constructs: line comments `//`, strings `"..."`, chars `'...'`, numbers, identifiers (keywords/types), preprocessor at beginning of line after leading whitespace, operators/punctuation, and whitespace.
|
- Stateless constructs: line comments `//`, strings `"..."`, chars
|
||||||
|
`'...'`, numbers, identifiers (keywords/types), preprocessor at
|
||||||
|
beginning of line after leading whitespace, operators/punctuation, and
|
||||||
|
whitespace.
|
||||||
- Stateful constructs (v2):
|
- Stateful constructs (v2):
|
||||||
- Multi-line block comments `/* ... */` — the state records whether the next line continues a comment.
|
- Multi-line block comments `/* ... */` — the state records whether
|
||||||
- Raw strings `R"delim(... )delim"` — the state tracks whether we are inside a raw string and its delimiter `delim` until the closing sequence appears.
|
the next line continues a comment.
|
||||||
|
- Raw strings `R"delim(... )delim"` — the state tracks whether we
|
||||||
|
are inside a raw string and its delimiter `delim` until the
|
||||||
|
closing sequence appears.
|
||||||
|
|
||||||
Limitations and TODOs
|
Limitations and TODOs
|
||||||
---------------------
|
---------------------
|
||||||
|
|
||||||
- Raw string detection is intentionally simple and does not handle all corner cases of the C++ standard.
|
- Raw string detection is intentionally simple and does not handle all
|
||||||
- Preprocessor handling is line-based; continuation lines with `\\` are not yet tracked.
|
corner cases of the C++ standard.
|
||||||
- No semantic analysis; identifiers are classified via small keyword/type sets.
|
- Preprocessor handling is line-based; continuation lines with `\\` are
|
||||||
- Additional languages (JSON, Markdown, Shell, Python, Go, Rust, Lisp, …) are planned.
|
not yet tracked.
|
||||||
- Terminal color mapping is conservative to support 8/16-color terminals. Rich color-pair themes can be added later.
|
- No semantic analysis; identifiers are classified via small
|
||||||
|
keyword/type sets.
|
||||||
|
- Additional languages (JSON, Markdown, Shell, Python, Go, Rust,
|
||||||
|
Lisp, …) are planned.
|
||||||
|
- Terminal color mapping is conservative to support 8/16-color
|
||||||
|
terminals. Rich color-pair themes can be added later.
|
||||||
|
|
||||||
Renderer integration
|
Renderer integration
|
||||||
--------------------
|
--------------------
|
||||||
|
|
||||||
- Terminal and GUI renderers request line spans via `Highlighter()->GetLine(buf, row, buf.Version())`.
|
- Terminal and GUI renderers request line spans via
|
||||||
- Search highlight and cursor overlays take precedence over syntax colors.
|
`Highlighter()->GetLine(buf, row, buf.Version())`.
|
||||||
|
- Search highlight and cursor overlays take precedence over syntax
|
||||||
|
colors.
|
||||||
|
|
||||||
|
Renderer-side robustness
|
||||||
|
------------------------
|
||||||
|
|
||||||
|
- Renderers defensively sanitize `HighlightSpan` data before use to
|
||||||
|
ensure stability even if a highlighter misbehaves:
|
||||||
|
- Clamp `col_start/col_end` to the line length and ensure
|
||||||
|
`end >= start`.
|
||||||
|
- Drop empty/invalid spans and sort by start.
|
||||||
|
- Clip drawing to the horizontally visible region and the
|
||||||
|
tab-expanded line length.
|
||||||
|
- The highlighter engine returns `LineHighlight` by value to avoid
|
||||||
|
cross-thread lifetime issues; renderers operate on a local copy for
|
||||||
|
each frame.
|
||||||
|
|
||||||
Extensibility (Phase 4)
|
Extensibility (Phase 4)
|
||||||
-----------------------
|
-----------------------
|
||||||
|
|
||||||
- Public registration API: external code can register custom highlighters by filetype.
|
- Public registration API: external code can register custom
|
||||||
- Use `HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
|
highlighters by filetype.
|
||||||
- Registered factories are preferred over built-ins for the same filetype key.
|
- Use
|
||||||
- Filetype keys are normalized via `HighlighterRegistry::Normalize()`.
|
`HighlighterRegistry::Register("mylang", []{ return std::make_unique<MyHighlighter>(); });`
|
||||||
- Optional Tree-sitter adapter: disabled by default to keep dependencies minimal.
|
- Registered factories are preferred over built-ins for the same
|
||||||
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
|
filetype key.
|
||||||
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if needed.
|
- Filetype keys are normalized via
|
||||||
- Register a Tree-sitter-backed highlighter for a language (example assumes you link a grammar):
|
`HighlighterRegistry::Normalize()`.
|
||||||
```c++
|
- Optional Tree-sitter adapter: disabled by default to keep dependencies
|
||||||
extern "C" const TSLanguage* tree_sitter_c();
|
minimal.
|
||||||
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
|
- Enable with CMake option `-DKTE_ENABLE_TREESITTER=ON` and provide
|
||||||
```
|
`-DTREESITTER_INCLUDE_DIR=...` and `-DTREESITTER_LIBRARY=...` if
|
||||||
- Current adapter is a stub scaffold; it compiles and integrates cleanly when enabled, but
|
needed.
|
||||||
intentionally emits no spans until Tree-sitter node-to-token mapping is implemented.
|
- Register a Tree-sitter-backed highlighter for a language (example
|
||||||
|
assumes you link a grammar):
|
||||||
|
```c++
|
||||||
|
extern "C" const TSLanguage* tree_sitter_c();
|
||||||
|
kte::HighlighterRegistry::RegisterTreeSitter("c", &tree_sitter_c);
|
||||||
|
```
|
||||||
|
- Current adapter is a stub scaffold; it compiles and integrates
|
||||||
|
cleanly when enabled, but
|
||||||
|
intentionally emits no spans until Tree-sitter node-to-token
|
||||||
|
mapping is implemented.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
full = kge;
|
full = kge;
|
||||||
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
kte = (pkgsFor system).callPackage ./default.nix { graphical = false; };
|
||||||
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
kge = (pkgsFor system).callPackage ./default.nix { graphical = true; };
|
||||||
|
qt = (pkgsFor system).callPackage ./default.nix { graphical-qt = true; }
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
95
main.cc
95
main.cc
@@ -6,6 +6,10 @@
|
|||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <memory>
|
#include <memory>
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <random>
|
||||||
|
#include <thread>
|
||||||
#include <signal.h>
|
#include <signal.h>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <unistd.h>
|
#include <unistd.h>
|
||||||
@@ -17,7 +21,11 @@
|
|||||||
#include "TerminalFrontend.h"
|
#include "TerminalFrontend.h"
|
||||||
|
|
||||||
#if defined(KTE_BUILD_GUI)
|
#if defined(KTE_BUILD_GUI)
|
||||||
#include "GUIFrontend.h"
|
#if defined(KTE_USE_QT)
|
||||||
|
#include "QtFrontend.h"
|
||||||
|
#else
|
||||||
|
#include "ImGuiFrontend.h"
|
||||||
|
#endif
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
@@ -34,7 +42,71 @@ PrintUsage(const char *prog)
|
|||||||
<< " -g, --gui Use GUI frontend (if built)\n"
|
<< " -g, --gui Use GUI frontend (if built)\n"
|
||||||
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
<< " -t, --term Use terminal (ncurses) frontend [default]\n"
|
||||||
<< " -h, --help Show this help and exit\n"
|
<< " -h, --help Show this help and exit\n"
|
||||||
<< " -V, --version Show version and exit\n";
|
<< " -V, --version Show version and exit\n"
|
||||||
|
<< " --stress-highlighter[=SECONDS] Run a short highlighter stress harness (debug aid)\n";
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
static int
|
||||||
|
RunStressHighlighter(unsigned seconds)
|
||||||
|
{
|
||||||
|
// Build a synthetic buffer with code-like content
|
||||||
|
Buffer buf;
|
||||||
|
buf.SetFiletype("cpp");
|
||||||
|
buf.SetSyntaxEnabled(true);
|
||||||
|
buf.EnsureHighlighter();
|
||||||
|
// Seed with many lines
|
||||||
|
const int N = 1200;
|
||||||
|
for (int i = 0; i < N; ++i) {
|
||||||
|
std::string line = "int v" + std::to_string(i) + " = " + std::to_string(i) + "; // line\n";
|
||||||
|
buf.insert_row(i, line);
|
||||||
|
}
|
||||||
|
// Remove the extra last empty row if any artifacts
|
||||||
|
// Simulate a viewport of ~60 rows
|
||||||
|
const int viewport_rows = 60;
|
||||||
|
const auto start_ts = std::chrono::steady_clock::now();
|
||||||
|
std::mt19937 rng{1234567u};
|
||||||
|
std::uniform_int_distribution<int> row_d(0, N - 1);
|
||||||
|
std::uniform_int_distribution<int> op_d(0, 2);
|
||||||
|
std::uniform_int_distribution<int> sleep_d(0, 2);
|
||||||
|
|
||||||
|
// Loop performing edits and highlighter queries while background worker runs
|
||||||
|
while (std::chrono::duration_cast<std::chrono::seconds>(std::chrono::steady_clock::now() - start_ts).count() <
|
||||||
|
seconds) {
|
||||||
|
int fr = row_d(rng);
|
||||||
|
if (fr + viewport_rows >= N)
|
||||||
|
fr = std::max(0, N - viewport_rows - 1);
|
||||||
|
buf.SetOffsets(static_cast<std::size_t>(fr), 0);
|
||||||
|
if (buf.Highlighter()) {
|
||||||
|
buf.Highlighter()->PrefetchViewport(buf, fr, viewport_rows, buf.Version());
|
||||||
|
}
|
||||||
|
// Do a few direct GetLine calls over the viewport to shake the caches
|
||||||
|
if (buf.Highlighter()) {
|
||||||
|
for (int r = 0; r < viewport_rows; r += 7) {
|
||||||
|
(void) buf.Highlighter()->GetLine(buf, fr + r, buf.Version());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Random simple edit
|
||||||
|
int op = op_d(rng);
|
||||||
|
int r = row_d(rng);
|
||||||
|
if (op == 0) {
|
||||||
|
buf.insert_text(r, 0, "/*X*/");
|
||||||
|
buf.SetDirty(true);
|
||||||
|
} else if (op == 1) {
|
||||||
|
buf.delete_text(r, 0, 1);
|
||||||
|
buf.SetDirty(true);
|
||||||
|
} else {
|
||||||
|
// split and join occasionally
|
||||||
|
buf.split_line(r, 0);
|
||||||
|
buf.join_lines(std::min(r + 1, N - 1));
|
||||||
|
buf.SetDirty(true);
|
||||||
|
}
|
||||||
|
// tiny sleep to allow background thread to interleave
|
||||||
|
if (sleep_d(rng) == 0) {
|
||||||
|
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -54,11 +126,13 @@ main(int argc, const char *argv[])
|
|||||||
{"term", no_argument, nullptr, 't'},
|
{"term", no_argument, nullptr, 't'},
|
||||||
{"help", no_argument, nullptr, 'h'},
|
{"help", no_argument, nullptr, 'h'},
|
||||||
{"version", no_argument, nullptr, 'V'},
|
{"version", no_argument, nullptr, 'V'},
|
||||||
|
{"stress-highlighter", optional_argument, nullptr, 1000},
|
||||||
{nullptr, 0, nullptr, 0}
|
{nullptr, 0, nullptr, 0}
|
||||||
};
|
};
|
||||||
|
|
||||||
int opt;
|
int opt;
|
||||||
int long_index = 0;
|
int long_index = 0;
|
||||||
|
unsigned stress_seconds = 0;
|
||||||
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
while ((opt = getopt_long(argc, const_cast<char *const *>(argv), "gthV", long_opts, &long_index)) != -1) {
|
||||||
switch (opt) {
|
switch (opt) {
|
||||||
case 'g':
|
case 'g':
|
||||||
@@ -73,6 +147,17 @@ main(int argc, const char *argv[])
|
|||||||
case 'V':
|
case 'V':
|
||||||
show_version = true;
|
show_version = true;
|
||||||
break;
|
break;
|
||||||
|
case 1000: {
|
||||||
|
stress_seconds = 5; // default
|
||||||
|
if (optarg && *optarg) {
|
||||||
|
try {
|
||||||
|
unsigned v = static_cast<unsigned>(std::stoul(optarg));
|
||||||
|
if (v > 0 && v < 36000)
|
||||||
|
stress_seconds = v;
|
||||||
|
} catch (...) {}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
case '?':
|
case '?':
|
||||||
default:
|
default:
|
||||||
PrintUsage(argv[0]);
|
PrintUsage(argv[0]);
|
||||||
@@ -89,6 +174,10 @@ main(int argc, const char *argv[])
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (stress_seconds > 0) {
|
||||||
|
return RunStressHighlighter(stress_seconds);
|
||||||
|
}
|
||||||
|
|
||||||
// Determine frontend
|
// Determine frontend
|
||||||
#if !defined(KTE_BUILD_GUI)
|
#if !defined(KTE_BUILD_GUI)
|
||||||
if (req_gui) {
|
if (req_gui) {
|
||||||
|
|||||||
29
make-app-release
Executable file
29
make-app-release
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
mkdir -p cmake-build-release
|
||||||
|
cmake -S . -B cmake-build-release -DBUILD_GUI=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||||
|
|
||||||
|
cd cmake-build-release
|
||||||
|
make clean
|
||||||
|
rm -fr kge.app*
|
||||||
|
make
|
||||||
|
zip -r kge.app.zip kge.app
|
||||||
|
sha256sum kge.app.zip
|
||||||
|
open .
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
mkdir -p cmake-build-release-qt
|
||||||
|
cmake -S . -B cmake-build-release -DBUILD_GUI=ON -DCMAKE_BUILD_TYPE=Release -DENABLE_ASAN=OFF
|
||||||
|
|
||||||
|
cd cmake-build-release-qt
|
||||||
|
make clean
|
||||||
|
rm -fr kge.app* kge-qt.app*
|
||||||
|
make
|
||||||
|
mv kge.app kge-qt.app
|
||||||
|
zip -r kge-qt.app.zip kge-qt.app
|
||||||
|
sha256sum kge-qt.app.zip
|
||||||
|
open .
|
||||||
|
cd ..
|
||||||
26
make-release
Executable file
26
make-release
Executable file
@@ -0,0 +1,26 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
|
||||||
|
set -eu
|
||||||
|
set -o pipefail
|
||||||
|
|
||||||
|
KTE_VERSION=$(grep 'KTE_VERSION' CMakeLists.txt | grep -o '"[0-9.]*"' | tr -d '"')
|
||||||
|
KTE_VERSION="v${KTE_VERSION}"
|
||||||
|
|
||||||
|
if [ "${KTE_VERSION}" = "v" ]
|
||||||
|
then
|
||||||
|
echo "invalid version" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "kte version ${KTE_VERSION}"
|
||||||
|
TREE="$(git status --porcelain --untracked-files=no)"
|
||||||
|
if [ ! -z "${TREE}" ]
|
||||||
|
then
|
||||||
|
echo "tree is dirty" > /dev/stderr
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
git tag "${KTE_VERSION}"
|
||||||
|
git push && git push --tags
|
||||||
|
|
||||||
|
( ./make-app-release )
|
||||||
@@ -34,22 +34,24 @@ HighlighterEngine::SetHighlighter(std::unique_ptr<LanguageHighlighter> hl)
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
const LineHighlight &
|
LineHighlight
|
||||||
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const
|
||||||
{
|
{
|
||||||
std::unique_lock<std::mutex> lock(mtx_);
|
std::unique_lock<std::mutex> lock(mtx_);
|
||||||
auto it = cache_.find(row);
|
auto it = cache_.find(row);
|
||||||
if (it != cache_.end() && it->second.version == buf_version) {
|
if (it != cache_.end() && it->second.version == buf_version) {
|
||||||
return it->second;
|
return it->second; // return by value (copy)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Prepare destination slot to reuse its capacity and avoid allocations
|
// We'll compute into a local result to avoid exposing references to cache
|
||||||
LineHighlight &slot = cache_[row];
|
LineHighlight result;
|
||||||
slot.version = buf_version;
|
result.version = buf_version;
|
||||||
slot.spans.clear();
|
result.spans.clear();
|
||||||
|
|
||||||
if (!hl_) {
|
if (!hl_) {
|
||||||
return slot;
|
// Cache empty result and return it
|
||||||
|
cache_[row] = result;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Copy shared_ptr-like raw pointer for use outside critical sections
|
// Copy shared_ptr-like raw pointer for use outside critical sections
|
||||||
@@ -58,10 +60,12 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
|
|
||||||
if (!is_stateful) {
|
if (!is_stateful) {
|
||||||
// Stateless fast path: we can release the lock while computing to reduce contention
|
// Stateless fast path: we can release the lock while computing to reduce contention
|
||||||
auto &out = slot.spans;
|
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
hl_ptr->HighlightLine(buf, row, out);
|
hl_ptr->HighlightLine(buf, row, result.spans);
|
||||||
return cache_.at(row);
|
// Update cache and return
|
||||||
|
std::lock_guard<std::mutex> gl(mtx_);
|
||||||
|
cache_[row] = result;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
// Stateful path: we need to walk from a known previous state. Keep lock while consulting caches,
|
||||||
@@ -75,9 +79,13 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
int best = -1;
|
int best = -1;
|
||||||
for (const auto &kv: state_cache_) {
|
for (const auto &kv: state_cache_) {
|
||||||
int r = kv.first;
|
int r = kv.first;
|
||||||
|
// Only use cached state if it's for the current version and row still exists
|
||||||
if (r <= row - 1 && kv.second.version == buf_version) {
|
if (r <= row - 1 && kv.second.version == buf_version) {
|
||||||
if (r > best)
|
// Validate that the cached row index is still valid in the buffer
|
||||||
best = r;
|
if (r >= 0 && static_cast<std::size_t>(r) < buf.Rows().size()) {
|
||||||
|
if (r > best)
|
||||||
|
best = r;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (best >= 0) {
|
if (best >= 0) {
|
||||||
@@ -92,7 +100,7 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
StatefulHighlighter::LineState cur_state = prev_state;
|
StatefulHighlighter::LineState cur_state = prev_state;
|
||||||
for (int r = start_row + 1; r <= row; ++r) {
|
for (int r = start_row + 1; r <= row; ++r) {
|
||||||
std::vector<HighlightSpan> tmp;
|
std::vector<HighlightSpan> tmp;
|
||||||
std::vector<HighlightSpan> &out = (r == row) ? slot.spans : tmp;
|
std::vector<HighlightSpan> &out = (r == row) ? result.spans : tmp;
|
||||||
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
auto next_state = stateful->HighlightLineStateful(buf, r, cur_state, out);
|
||||||
// Update state cache for r
|
// Update state cache for r
|
||||||
std::lock_guard<std::mutex> gl(mtx_);
|
std::lock_guard<std::mutex> gl(mtx_);
|
||||||
@@ -103,9 +111,10 @@ HighlighterEngine::GetLine(const Buffer &buf, int row, std::uint64_t buf_version
|
|||||||
cur_state = next_state;
|
cur_state = next_state;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Return reference under lock to ensure slot's address stability in map
|
// Store in cache and return by value
|
||||||
lock.lock();
|
lock.lock();
|
||||||
return cache_.at(row);
|
cache_[row] = result;
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -160,11 +169,15 @@ HighlighterEngine::worker_loop() const
|
|||||||
// Copy locals then release lock while computing
|
// Copy locals then release lock while computing
|
||||||
lock.unlock();
|
lock.unlock();
|
||||||
if (req.buf) {
|
if (req.buf) {
|
||||||
int start = std::max(0, req.start_row);
|
int start = std::max(0, req.start_row);
|
||||||
int end = std::max(start, req.end_row);
|
int end = std::max(start, req.end_row);
|
||||||
|
int skip_f = std::min(req.skip_first, req.skip_last);
|
||||||
|
int skip_l = std::max(req.skip_first, req.skip_last);
|
||||||
for (int r = start; r <= end; ++r) {
|
for (int r = start; r <= end; ++r) {
|
||||||
// Re-check version staleness quickly by peeking cache version; not strictly necessary
|
// Avoid touching rows that the foreground just computed/drew.
|
||||||
// Compute line; GetLine is thread-safe
|
if (r >= skip_f && r <= skip_l)
|
||||||
|
continue;
|
||||||
|
// Compute line; GetLine is thread-safe and will refresh caches.
|
||||||
(void) this->GetLine(*req.buf, r, req.version);
|
(void) this->GetLine(*req.buf, r, req.version);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -197,11 +210,13 @@ HighlighterEngine::PrefetchViewport(const Buffer &buf, int first_row, int row_co
|
|||||||
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
int warm_end = std::min(max_rows - 1, end + warm_margin);
|
||||||
{
|
{
|
||||||
std::lock_guard<std::mutex> lock(mtx_);
|
std::lock_guard<std::mutex> lock(mtx_);
|
||||||
pending_.buf = &buf;
|
pending_.buf = &buf;
|
||||||
pending_.version = buf_version;
|
pending_.version = buf_version;
|
||||||
pending_.start_row = warm_start;
|
pending_.start_row = warm_start;
|
||||||
pending_.end_row = warm_end;
|
pending_.end_row = warm_end;
|
||||||
has_request_ = true;
|
pending_.skip_first = start;
|
||||||
|
pending_.skip_last = end;
|
||||||
|
has_request_ = true;
|
||||||
}
|
}
|
||||||
ensure_worker_started();
|
ensure_worker_started();
|
||||||
cv_.notify_one();
|
cv_.notify_one();
|
||||||
|
|||||||
@@ -25,8 +25,9 @@ public:
|
|||||||
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
void SetHighlighter(std::unique_ptr<LanguageHighlighter> hl);
|
||||||
|
|
||||||
// Retrieve highlights for a given line and buffer version.
|
// Retrieve highlights for a given line and buffer version.
|
||||||
|
// Returns a copy to avoid lifetime issues across threads/renderers.
|
||||||
// If cache is stale, recompute using the current highlighter.
|
// If cache is stale, recompute using the current highlighter.
|
||||||
const LineHighlight &GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
LineHighlight GetLine(const Buffer &buf, int row, std::uint64_t buf_version) const;
|
||||||
|
|
||||||
// Invalidate cached lines from row (inclusive)
|
// Invalidate cached lines from row (inclusive)
|
||||||
void InvalidateFrom(int row);
|
void InvalidateFrom(int row);
|
||||||
@@ -70,6 +71,10 @@ private:
|
|||||||
std::uint64_t version{0};
|
std::uint64_t version{0};
|
||||||
int start_row{0};
|
int start_row{0};
|
||||||
int end_row{0}; // inclusive
|
int end_row{0}; // inclusive
|
||||||
|
// Visible rows to skip touching in the background (inclusive range).
|
||||||
|
// These are computed synchronously by PrefetchViewport.
|
||||||
|
int skip_first{0};
|
||||||
|
int skip_last{-1};
|
||||||
};
|
};
|
||||||
|
|
||||||
mutable std::condition_variable cv_;
|
mutable std::condition_variable cv_;
|
||||||
|
|||||||
Reference in New Issue
Block a user