diff --git a/CMakeLists.txt b/CMakeLists.txt index d4f0883..c7afe2f 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -4,7 +4,7 @@ project(kte) include(GNUInstallDirs) set(CMAKE_CXX_STANDARD 20) -set(KTE_VERSION "1.9.0") +set(KTE_VERSION "1.9.1") # Default to terminal-only build to avoid SDL/OpenGL dependency by default. # Enable with -DBUILD_GUI=ON when SDL2/OpenGL/Freetype are available. diff --git a/Command.cc b/Command.cc index f6e9be6..bc2dd1d 100644 --- a/Command.cc +++ b/Command.cc @@ -1357,6 +1357,10 @@ cmd_background_set(const CommandContext &ctx) std::transform(mode.begin(), mode.end(), mode.begin(), [](unsigned char c) { return (char) std::tolower(c); }); + if (mode.empty()) { + ctx.editor.SetStatus(std::string("Background: ") + kte::BackgroundModeName()); + return true; + } if (mode != "light" && mode != "dark") { ctx.editor.SetStatus("background: expected 'light' or 'dark'"); return true; diff --git a/Command.h b/Command.h index 3d000a5..778a1e9 100644 --- a/Command.h +++ b/Command.h @@ -113,6 +113,10 @@ enum class CommandId { CenterOnCursor, // center the viewport on the current cursor line (C-k k) // GUI: open a new editor window sharing the same buffer list NewWindow, + // GUI: font size controls + FontZoomIn, + FontZoomOut, + FontZoomReset, }; diff --git a/GUITheme.h b/GUITheme.h index e1ad0b8..19ecfb4 100644 --- a/GUITheme.h +++ b/GUITheme.h @@ -312,7 +312,7 @@ namespace kte { enum class BackgroundMode { Light, Dark }; // Global background mode; default to Dark to match prior defaults -static inline auto gBackgroundMode = BackgroundMode::Dark; +inline auto gBackgroundMode = BackgroundMode::Dark; // Basic theme identifier (kept minimal; some ids are aliases) enum class ThemeId { @@ -331,11 +331,12 @@ enum class ThemeId { WeylandYutani = 11, Orbital = 12, Tufte = 13, + Leuchtturm = 14, }; // Current theme tracking -static inline auto gCurrentTheme = ThemeId::Nord; -static inline std::size_t gCurrentThemeIndex = 6; // Nord index +inline auto gCurrentTheme = ThemeId::Nord; +inline std::size_t gCurrentThemeIndex = 7; // Nord index // Forward declarations for helpers used below static size_t ThemeIndexFromId(ThemeId id); @@ -373,6 +374,7 @@ BackgroundModeName() #include "themes/Everforest.h" #include "themes/KanagawaPaper.h" #include "themes/LCARS.h" +#include "themes/Leuchtturm.h" #include "themes/OldBook.h" #include "themes/Amber.h" #include "themes/WeylandYutani.h" @@ -411,6 +413,28 @@ struct LCARSTheme final : Theme { } }; +struct LeuchtturmTheme final : Theme { + [[nodiscard]] const char *Name() const override + { + return "leuchtturm"; + } + + + void Apply() const override + { + if (gBackgroundMode == BackgroundMode::Dark) + ApplyLeuchtturmDarkTheme(); + else + ApplyLeuchtturmLightTheme(); + } + + + ThemeId Id() override + { + return ThemeId::Leuchtturm; + } +}; + struct EverforestTheme final : Theme { [[nodiscard]] const char *Name() const override { @@ -681,13 +705,14 @@ ThemeRegistry() static std::vector > reg; if (reg.empty()) { // Alphabetical by canonical name: - // amber, eink, everforest, gruvbox, kanagawa-paper, lcars, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn + // amber, eink, everforest, gruvbox, kanagawa-paper, lcars, leuchtturm, nord, old-book, orbital, plan9, solarized, tufte, weyland-yutani, zenburn reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); + reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); reg.emplace_back(std::make_unique()); @@ -870,22 +895,24 @@ ThemeIndexFromId(const ThemeId id) return 4; case ThemeId::LCARS: return 5; - case ThemeId::Nord: + case ThemeId::Leuchtturm: return 6; - case ThemeId::OldBook: + case ThemeId::Nord: return 7; - case ThemeId::Orbital: + case ThemeId::OldBook: return 8; - case ThemeId::Plan9: + case ThemeId::Orbital: return 9; - case ThemeId::Solarized: + case ThemeId::Plan9: return 10; - case ThemeId::Tufte: + case ThemeId::Solarized: return 11; - case ThemeId::WeylandYutani: + case ThemeId::Tufte: return 12; - case ThemeId::Zenburn: + case ThemeId::WeylandYutani: return 13; + case ThemeId::Zenburn: + return 14; } return 0; } @@ -909,32 +936,144 @@ ThemeIdFromIndex(const size_t idx) case 5: return ThemeId::LCARS; case 6: - return ThemeId::Nord; + return ThemeId::Leuchtturm; case 7: - return ThemeId::OldBook; + return ThemeId::Nord; case 8: - return ThemeId::Orbital; + return ThemeId::OldBook; case 9: - return ThemeId::Plan9; + return ThemeId::Orbital; case 10: - return ThemeId::Solarized; + return ThemeId::Plan9; case 11: - return ThemeId::Tufte; + return ThemeId::Solarized; case 12: - return ThemeId::WeylandYutani; + return ThemeId::Tufte; case 13: + return ThemeId::WeylandYutani; + case 14: return ThemeId::Zenburn; } } // --- Syntax palette (v1): map TokenKind to ink color per current theme/background --- + +// Tufte palette: high-contrast, restrained color. Body text is true black on +// cream; only keywords and links get subtle color to avoid a "christmas tree." +static ImVec4 +SyntaxInkTufte(const TokenKind k, const bool dark) +{ + const ImVec4 ink = dark ? RGBA(0xEAE6DE) : RGBA(0x111111); // body text + const ImVec4 dim = dark ? RGBA(0x8A8680) : RGBA(0x555555); // comments + const ImVec4 red = dark ? RGBA(0xD06060) : RGBA(0x8B0000); // keywords/preproc + const ImVec4 navy = dark ? RGBA(0x7098C0) : RGBA(0x1A3A5C); // functions/links + const ImVec4 grn = dark ? RGBA(0x8AAA6E) : RGBA(0x2E5E2E); // strings + switch (k) { + case TokenKind::Keyword: + case TokenKind::Preproc: + return red; + case TokenKind::String: + case TokenKind::Char: + return grn; + case TokenKind::Comment: + return dim; + case TokenKind::Function: + return navy; + case TokenKind::Number: + case TokenKind::Constant: + return dark ? RGBA(0xC8A85A) : RGBA(0x6B4C00); + case TokenKind::Type: + return dark ? RGBA(0xBBAA90) : RGBA(0x333333); + case TokenKind::Error: + return dark ? RGBA(0xD06060) : RGBA(0xCC0000); + default: + return ink; + } +} + + +// Leuchtturm palette: blue-black fountain pen ink with brass and bronze accents. +// Body text is ink-colored; accents drawn from the pen metals. +static ImVec4 +SyntaxInkLeuchtturm(const TokenKind k, const bool dark) +{ + const ImVec4 ink = dark ? RGBA(0xE5DDD0) : RGBA(0x040720); // fountain pen ink + const ImVec4 dim = dark ? RGBA(0x7A7060) : RGBA(0x6A6558); // comments + const ImVec4 brass = dark ? RGBA(0xB8A060) : RGBA(0x504518); // patinated brass + const ImVec4 bronze= dark ? RGBA(0xC08050) : RGBA(0x5C3010); // dark bronze + const ImVec4 navy = dark ? RGBA(0x8898B0) : RGBA(0x1C2E4A); // deep navy + switch (k) { + case TokenKind::Keyword: + case TokenKind::Preproc: + return brass; + case TokenKind::String: + case TokenKind::Char: + return bronze; + case TokenKind::Comment: + return dim; + case TokenKind::Function: + return navy; + case TokenKind::Number: + case TokenKind::Constant: + return dark ? RGBA(0xA89060) : RGBA(0x483C10); + case TokenKind::Type: + return dark ? RGBA(0xC0B898) : RGBA(0x222238); + case TokenKind::Error: + return dark ? RGBA(0xD06060) : RGBA(0xA02020); + default: + return ink; + } +} + + +// Everforest: warm forest palette on dark green-gray (bg 0x2B3339). +// Default comment color (0x616E88) is too dim; boost it and tune others. +static ImVec4 +SyntaxInkEverforest(const TokenKind k) +{ + switch (k) { + case TokenKind::Keyword: + return RGBA(0xE67E80); // everforest red + case TokenKind::Type: + return RGBA(0xD699B6); // everforest purple + case TokenKind::String: + case TokenKind::Char: + return RGBA(0xA7C080); // everforest green + case TokenKind::Comment: + return RGBA(0x859289); // boosted from 0x616E88 for contrast + case TokenKind::Number: + case TokenKind::Constant: + return RGBA(0xD8A657); // everforest yellow/orange + case TokenKind::Preproc: + return RGBA(0xE69875); // everforest orange + case TokenKind::Function: + return RGBA(0x83C092); // everforest aqua + case TokenKind::Operator: + case TokenKind::Punctuation: + return RGBA(0xD3C6AA); // everforest fg + case TokenKind::Error: + return RGBA(0xE67E80); + default: + return RGBA(0xD3C6AA); // everforest fg + } +} + + [[maybe_unused]] static ImVec4 SyntaxInk(const TokenKind k) { - // Basic palettes for dark/light backgrounds; tuned for Nord-ish defaults const bool dark = (GetBackgroundMode() == BackgroundMode::Dark); - // Base text + + // Per-theme syntax palettes + if (gCurrentTheme == ThemeId::Tufte) + return SyntaxInkTufte(k, dark); + if (gCurrentTheme == ThemeId::Leuchtturm) + return SyntaxInkLeuchtturm(k, dark); + if (gCurrentTheme == ThemeId::Everforest) + return SyntaxInkEverforest(k); + + // Default palettes tuned for Nord-ish themes const ImVec4 def = dark ? RGBA(0xD8DEE9) : RGBA(0x2E3440); switch (k) { case TokenKind::Keyword: diff --git a/ImGuiFrontend.cc b/ImGuiFrontend.cc index 0abed79..940c7e6 100644 --- a/ImGuiFrontend.cc +++ b/ImGuiFrontend.cc @@ -527,6 +527,21 @@ GUIFrontend::Step(Editor &ed, bool &running) if (mi.id == CommandId::NewWindow) { // Open a new window; handled after this loop wed.SetNewWindowRequested(true); + } else if (mi.id == CommandId::FontZoomIn || + mi.id == CommandId::FontZoomOut || + mi.id == CommandId::FontZoomReset) { + auto &fr = kte::Fonts::FontRegistry::Instance(); + float cur = fr.CurrentFontSize(); + if (cur <= 0.0f) cur = config_.font_size; + float next = cur; + if (mi.id == CommandId::FontZoomIn) + next = std::min(cur + 2.0f, 72.0f); + else if (mi.id == CommandId::FontZoomOut) + next = std::max(cur - 2.0f, 8.0f); + else + next = config_.font_size; // reset to config default + if (next != cur) + fr.RequestLoadFont(fr.CurrentFontName(), next); } else { const std::string before = wed.KillRingHead(); Execute(wed, mi.id, mi.arg, mi.count); diff --git a/ImGuiInputHandler.cc b/ImGuiInputHandler.cc index 2eabf31..436fa29 100644 --- a/ImGuiInputHandler.cc +++ b/ImGuiInputHandler.cc @@ -349,6 +349,26 @@ ImGuiInputHandler::ProcessSDLEvent(const SDL_Event &e) } } + // Font zoom: Cmd+=/Cmd+-/Cmd+0 (macOS) or Ctrl+=/Ctrl+-/Ctrl+0 + if ((mods & (KMOD_CTRL | KMOD_GUI)) && !(mods & KMOD_SHIFT)) { + bool is_zoom = true; + CommandId zoom_cmd = CommandId::FontZoomIn; + if (key == SDLK_EQUALS || key == SDLK_PLUS) + zoom_cmd = CommandId::FontZoomIn; + else if (key == SDLK_MINUS) + zoom_cmd = CommandId::FontZoomOut; + else if (key == SDLK_0) + zoom_cmd = CommandId::FontZoomReset; + else + is_zoom = false; + if (is_zoom) { + std::lock_guard lk(mu_); + q_.push(MappedInput{true, zoom_cmd, std::string(), 0}); + suppress_text_input_once_ = true; + return true; + } + } + // Handle Paste: Ctrl+V (Windows/Linux) or Cmd+V (macOS) // Note: SDL defines letter keycodes in lowercase only (e.g., SDLK_v). Shift does not change keycode. if ((mods & (KMOD_CTRL | KMOD_GUI)) && (key == SDLK_v)) { diff --git a/themes/Leuchtturm.h b/themes/Leuchtturm.h new file mode 100644 index 0000000..2eb8880 --- /dev/null +++ b/themes/Leuchtturm.h @@ -0,0 +1,204 @@ +// themes/Leuchtturm.h — Fountain pen on cream paper, brass and leather (header-only) +// Inspired by Kaweco Brass/Bronze Sport pens on Leuchtturm1917 notebook paper. +// Light: warm cream paper with blue-black fountain pen ink. +// Dark: leather case and patinated metal. +#pragma once +#include "ThemeHelpers.h" + +static inline void +ApplyLeuchtturmLightTheme() +{ + // Notebook paper and fountain pen ink + const ImVec4 paper = RGBA(0xF2ECDF); // Leuchtturm cream paper + const ImVec4 bg1 = RGBA(0xE8E2D5); // slightly darker cream + const ImVec4 bg2 = RGBA(0xDDD7CA); // UI elements + const ImVec4 bg3 = RGBA(0xD1CBBD); // hover/active + const ImVec4 ink = RGBA(0x040720); // blue-black fountain pen ink + const ImVec4 dim = RGBA(0x7A756A); // faded text (like printed headers) + const ImVec4 border = RGBA(0xCCC6B4); // faint ruled lines + + // Metal accents from the pens + const ImVec4 brass = RGBA(0x6B5E2A); // dark patinated brass + const ImVec4 brown = RGBA(0x5C3D28); // leather/bronze + + ImGuiStyle &style = ImGui::GetStyle(); + style.WindowPadding = ImVec2(8.0f, 8.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.CellPadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(6.0f, 6.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); + style.ScrollbarSize = 12.0f; + style.GrabMinSize = 10.0f; + style.WindowRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.GrabRounding = 0.0f; + style.TabRounding = 0.0f; + style.WindowBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + + ImVec4 *colors = style.Colors; + colors[ImGuiCol_Text] = ink; + colors[ImGuiCol_TextDisabled] = dim; + colors[ImGuiCol_WindowBg] = paper; + colors[ImGuiCol_ChildBg] = paper; + colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f); + colors[ImGuiCol_Border] = border; + colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f); + + colors[ImGuiCol_FrameBg] = bg2; + colors[ImGuiCol_FrameBgHovered] = bg3; + colors[ImGuiCol_FrameBgActive] = bg1; + + colors[ImGuiCol_TitleBg] = bg1; + colors[ImGuiCol_TitleBgActive] = bg2; + colors[ImGuiCol_TitleBgCollapsed] = bg1; + + colors[ImGuiCol_MenuBarBg] = bg1; + colors[ImGuiCol_ScrollbarBg] = paper; + colors[ImGuiCol_ScrollbarGrab] = bg3; + colors[ImGuiCol_ScrollbarGrabHovered] = bg2; + colors[ImGuiCol_ScrollbarGrabActive] = border; + + colors[ImGuiCol_CheckMark] = ink; + colors[ImGuiCol_SliderGrab] = ink; + colors[ImGuiCol_SliderGrabActive] = brass; + + colors[ImGuiCol_Button] = bg2; + colors[ImGuiCol_ButtonHovered] = bg3; + colors[ImGuiCol_ButtonActive] = bg1; + + colors[ImGuiCol_Header] = bg2; + colors[ImGuiCol_HeaderHovered] = bg3; + colors[ImGuiCol_HeaderActive] = bg3; + + colors[ImGuiCol_Separator] = border; + colors[ImGuiCol_SeparatorHovered] = bg3; + colors[ImGuiCol_SeparatorActive] = brass; + + colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f); + colors[ImGuiCol_ResizeGripActive] = brass; + + colors[ImGuiCol_Tab] = bg2; + colors[ImGuiCol_TabHovered] = bg1; + colors[ImGuiCol_TabActive] = bg3; + colors[ImGuiCol_TabUnfocused] = bg2; + colors[ImGuiCol_TabUnfocusedActive] = bg3; + + colors[ImGuiCol_TableHeaderBg] = bg2; + colors[ImGuiCol_TableBorderStrong] = border; + colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f); + colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f); + + colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.18f); + colors[ImGuiCol_DragDropTarget] = brass; + colors[ImGuiCol_NavHighlight] = brass; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.15f); + colors[ImGuiCol_PlotLines] = brown; + colors[ImGuiCol_PlotLinesHovered] = brass; + colors[ImGuiCol_PlotHistogram] = brown; + colors[ImGuiCol_PlotHistogramHovered] = brass; +} + + +// Dark variant — leather pen case with warm metal and cream accents +static inline void +ApplyLeuchtturmDarkTheme() +{ + const ImVec4 bg0 = RGBA(0x1C1610); // dark leather + const ImVec4 bg1 = RGBA(0x251E16); // slightly lighter + const ImVec4 bg2 = RGBA(0x30281E); // UI elements + const ImVec4 bg3 = RGBA(0x3E3428); // hover/active + const ImVec4 ink = RGBA(0xE5DDD0); // warm cream text + const ImVec4 dim = RGBA(0x978E7C); // secondary text + const ImVec4 border = RGBA(0x4A3E30); // subtle borders + + const ImVec4 brass = RGBA(0xB8A060); // polished brass + const ImVec4 brown = RGBA(0x8B6848); // bronze pen + + ImGuiStyle &style = ImGui::GetStyle(); + style.WindowPadding = ImVec2(8.0f, 8.0f); + style.FramePadding = ImVec2(6.0f, 4.0f); + style.CellPadding = ImVec2(6.0f, 4.0f); + style.ItemSpacing = ImVec2(6.0f, 6.0f); + style.ItemInnerSpacing = ImVec2(6.0f, 4.0f); + style.ScrollbarSize = 12.0f; + style.GrabMinSize = 10.0f; + style.WindowRounding = 0.0f; + style.FrameRounding = 0.0f; + style.PopupRounding = 0.0f; + style.GrabRounding = 0.0f; + style.TabRounding = 0.0f; + style.WindowBorderSize = 1.0f; + style.FrameBorderSize = 0.0f; + + ImVec4 *colors = style.Colors; + colors[ImGuiCol_Text] = ink; + colors[ImGuiCol_TextDisabled] = dim; + colors[ImGuiCol_WindowBg] = bg0; + colors[ImGuiCol_ChildBg] = bg0; + colors[ImGuiCol_PopupBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.98f); + colors[ImGuiCol_Border] = border; + colors[ImGuiCol_BorderShadow] = RGBA(0x000000, 0.0f); + + colors[ImGuiCol_FrameBg] = bg2; + colors[ImGuiCol_FrameBgHovered] = bg3; + colors[ImGuiCol_FrameBgActive] = bg1; + + colors[ImGuiCol_TitleBg] = bg1; + colors[ImGuiCol_TitleBgActive] = bg2; + colors[ImGuiCol_TitleBgCollapsed] = bg1; + + colors[ImGuiCol_MenuBarBg] = bg1; + colors[ImGuiCol_ScrollbarBg] = bg0; + colors[ImGuiCol_ScrollbarGrab] = bg3; + colors[ImGuiCol_ScrollbarGrabHovered] = border; + colors[ImGuiCol_ScrollbarGrabActive] = dim; + + colors[ImGuiCol_CheckMark] = brass; + colors[ImGuiCol_SliderGrab] = brass; + colors[ImGuiCol_SliderGrabActive] = brown; + + colors[ImGuiCol_Button] = bg2; + colors[ImGuiCol_ButtonHovered] = bg3; + colors[ImGuiCol_ButtonActive] = bg1; + + colors[ImGuiCol_Header] = bg2; + colors[ImGuiCol_HeaderHovered] = bg3; + colors[ImGuiCol_HeaderActive] = bg3; + + colors[ImGuiCol_Separator] = border; + colors[ImGuiCol_SeparatorHovered] = bg3; + colors[ImGuiCol_SeparatorActive] = brass; + + colors[ImGuiCol_ResizeGrip] = ImVec4(ink.x, ink.y, ink.z, 0.10f); + colors[ImGuiCol_ResizeGripHovered] = ImVec4(brass.x, brass.y, brass.z, 0.50f); + colors[ImGuiCol_ResizeGripActive] = brass; + + colors[ImGuiCol_Tab] = bg2; + colors[ImGuiCol_TabHovered] = bg1; + colors[ImGuiCol_TabActive] = bg3; + colors[ImGuiCol_TabUnfocused] = bg2; + colors[ImGuiCol_TabUnfocusedActive] = bg3; + + colors[ImGuiCol_TableHeaderBg] = bg2; + colors[ImGuiCol_TableBorderStrong] = border; + colors[ImGuiCol_TableBorderLight] = ImVec4(border.x, border.y, border.z, 0.5f); + colors[ImGuiCol_TableRowBg] = ImVec4(bg1.x, bg1.y, bg1.z, 0.0f); + colors[ImGuiCol_TableRowBgAlt] = ImVec4(bg1.x, bg1.y, bg1.z, 0.30f); + + colors[ImGuiCol_TextSelectedBg] = ImVec4(brass.x, brass.y, brass.z, 0.22f); + colors[ImGuiCol_DragDropTarget] = brass; + colors[ImGuiCol_NavHighlight] = brass; + colors[ImGuiCol_NavWindowingHighlight] = ImVec4(ink.x, ink.y, ink.z, 0.70f); + colors[ImGuiCol_NavWindowingDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f); + colors[ImGuiCol_ModalWindowDimBg] = ImVec4(0.0f, 0.0f, 0.0f, 0.35f); + colors[ImGuiCol_PlotLines] = brass; + colors[ImGuiCol_PlotLinesHovered] = brown; + colors[ImGuiCol_PlotHistogram] = brass; + colors[ImGuiCol_PlotHistogramHovered] = brown; +}