From 3f343ff97e5873f66ba1f295641cae1c7b8fe11a Mon Sep 17 00:00:00 2001 From: hachem Date: Tue, 24 Mar 2026 05:10:09 +0100 Subject: feat: theme system --- assets/themes/bark.toml | 29 +++++ assets/themes/catppuccin.toml | 25 +++++ assets/themes/dracula.toml | 25 +++++ assets/themes/gruvbox.toml | 25 +++++ assets/themes/midnight.toml | 25 +++++ assets/themes/nord.toml | 25 +++++ assets/themes/paper.toml | 25 +++++ assets/themes/rosepine.toml | 25 +++++ assets/themes/tokyonight.toml | 25 +++++ src/ui/theme.cpp | 254 ++++++++++++++++++++++++++++++++++++++++++ src/ui/theme.h | 48 ++++++++ 11 files changed, 531 insertions(+) create mode 100644 assets/themes/bark.toml create mode 100644 assets/themes/catppuccin.toml create mode 100644 assets/themes/dracula.toml create mode 100644 assets/themes/gruvbox.toml create mode 100644 assets/themes/midnight.toml create mode 100644 assets/themes/nord.toml create mode 100644 assets/themes/paper.toml create mode 100644 assets/themes/rosepine.toml create mode 100644 assets/themes/tokyonight.toml create mode 100644 src/ui/theme.cpp create mode 100644 src/ui/theme.h diff --git a/assets/themes/bark.toml b/assets/themes/bark.toml new file mode 100644 index 0000000..791acdf --- /dev/null +++ b/assets/themes/bark.toml @@ -0,0 +1,29 @@ +# Bark - https://github.com/if-not-nil/bark +# * by :lung/if-not-nil +# * creator of revo: https://revo.lung.fyi + +[theme] +name = "Bark" + +[palette] +text = "#D0D0D0" +text_dim = "#808080" +bg = "#101010" +bg_low = "#0A0A0A" +bg_high = "#202020" +surface = "#2B2B2B" +border = "#383838" +accent = "#7CAFC2" + +[style] +rounding = 6 +window_border = 1 + +[syntax] +keyword = "#BA8BAF" +type = "#F7CA88" +string = "#A1B56C" +number = "#DC9656" +comment = "#585858" +preprocessor = "#7CAFC2" +current_line = "#202020" diff --git a/assets/themes/catppuccin.toml b/assets/themes/catppuccin.toml new file mode 100644 index 0000000..0929f39 --- /dev/null +++ b/assets/themes/catppuccin.toml @@ -0,0 +1,25 @@ +[theme] +name = "Catppuccin Mocha" + +[palette] +text = "#CDD6F4" +text_dim = "#7F849C" +bg = "#1E1E2E" +bg_low = "#181825" +bg_high = "#313244" +surface = "#45475A" +border = "#585B70" +accent = "#89B4FA" + +[style] +rounding = 7 +window_border = 1 + +[syntax] +keyword = "#CBA6F7" +type = "#F9E2AF" +string = "#A6E3A1" +number = "#FAB387" +comment = "#6C7086" +preprocessor = "#F5C2E7" +current_line = "#313244" diff --git a/assets/themes/dracula.toml b/assets/themes/dracula.toml new file mode 100644 index 0000000..24a7e35 --- /dev/null +++ b/assets/themes/dracula.toml @@ -0,0 +1,25 @@ +[theme] +name = "Dracula" + +[palette] +text = "#F8F8F2" +text_dim = "#6272A4" +bg = "#282A36" +bg_low = "#21222C" +bg_high = "#343746" +surface = "#44475A" +border = "#44475A" +accent = "#BD93F9" + +[style] +rounding = 7 +window_border = 1 + +[syntax] +keyword = "#FF79C6" +type = "#8BE9FD" +string = "#F1FA8C" +number = "#BD93F9" +comment = "#6272A4" +preprocessor = "#FFB86C" +current_line = "#44475A" diff --git a/assets/themes/gruvbox.toml b/assets/themes/gruvbox.toml new file mode 100644 index 0000000..9ddb60d --- /dev/null +++ b/assets/themes/gruvbox.toml @@ -0,0 +1,25 @@ +[theme] +name = "Gruvbox" + +[palette] +text = "#EBDBB2" +text_dim = "#A89984" +bg = "#282828" +bg_low = "#1D2021" +bg_high = "#3C3836" +surface = "#504945" +border = "#665C54" +accent = "#FE8019" + +[style] +rounding = 5 +window_border = 1 + +[syntax] +keyword = "#FB4934" +type = "#FABD2F" +string = "#B8BB26" +number = "#D3869B" +comment = "#928374" +preprocessor = "#8EC07C" +current_line = "#3C3836" diff --git a/assets/themes/midnight.toml b/assets/themes/midnight.toml new file mode 100644 index 0000000..f5e1c7e --- /dev/null +++ b/assets/themes/midnight.toml @@ -0,0 +1,25 @@ +[theme] +name = "Midnight" + +[palette] +text = "#ECEEF3" +text_dim = "#838B9E" +bg = "#15161C" +bg_low = "#101116" +bg_high = "#1E2028" +surface = "#262933" +border = "#363A47" +accent = "#4DB6C4" + +[style] +rounding = 6 +window_border = 1 + +[syntax] +keyword = "#569CD6" +type = "#4EC9B0" +string = "#CE9178" +number = "#B5CEA8" +comment = "#6A9955" +preprocessor = "#C586C0" +current_line = "#3A482E" diff --git a/assets/themes/nord.toml b/assets/themes/nord.toml new file mode 100644 index 0000000..073f8af --- /dev/null +++ b/assets/themes/nord.toml @@ -0,0 +1,25 @@ +[theme] +name = "Nord" + +[palette] +text = "#D8DEE9" +text_dim = "#6C7A8D" +bg = "#2E3440" +bg_low = "#272C36" +bg_high = "#3B4252" +surface = "#434C5E" +border = "#4C566A" +accent = "#88C0D0" + +[style] +rounding = 6 +window_border = 1 + +[syntax] +keyword = "#81A1C1" +type = "#8FBCBB" +string = "#A3BE8C" +number = "#B48EAD" +comment = "#616E88" +preprocessor = "#5E81AC" +current_line = "#3B4252" diff --git a/assets/themes/paper.toml b/assets/themes/paper.toml new file mode 100644 index 0000000..c3151ca --- /dev/null +++ b/assets/themes/paper.toml @@ -0,0 +1,25 @@ +[theme] +name = "Paper" + +[palette] +text = "#21242B" +text_dim = "#8A909C" +bg = "#F4F5F7" +bg_low = "#E7EAEF" +bg_high = "#FFFFFF" +surface = "#E4E7ED" +border = "#D2D6DE" +accent = "#0E8FA8" + +[style] +rounding = 6 +window_border = 1 + +[syntax] +keyword = "#0B5FBF" +type = "#0E7C86" +string = "#B4581E" +number = "#7A5AB0" +comment = "#5E8759" +preprocessor = "#A03E8F" +current_line = "#E1E8F2" diff --git a/assets/themes/rosepine.toml b/assets/themes/rosepine.toml new file mode 100644 index 0000000..4f0bd4e --- /dev/null +++ b/assets/themes/rosepine.toml @@ -0,0 +1,25 @@ +[theme] +name = "Rosé Pine" + +[palette] +text = "#E0DEF4" +text_dim = "#908CAA" +bg = "#191724" +bg_low = "#14121C" +bg_high = "#1F1D2E" +surface = "#26233A" +border = "#403D52" +accent = "#C4A7E7" + +[style] +rounding = 7 +window_border = 1 + +[syntax] +keyword = "#31748F" +type = "#9CCFD8" +string = "#F6C177" +number = "#EBBCBA" +comment = "#6E6A86" +preprocessor = "#EB6F92" +current_line = "#26233A" diff --git a/assets/themes/tokyonight.toml b/assets/themes/tokyonight.toml new file mode 100644 index 0000000..778ce1c --- /dev/null +++ b/assets/themes/tokyonight.toml @@ -0,0 +1,25 @@ +[theme] +name = "Tokyo Night" + +[palette] +text = "#C0CAF5" +text_dim = "#737AA2" +bg = "#1A1B26" +bg_low = "#16161E" +bg_high = "#24283B" +surface = "#2F3549" +border = "#3B4261" +accent = "#7AA2F7" + +[style] +rounding = 6 +window_border = 1 + +[syntax] +keyword = "#BB9AF7" +type = "#2AC3DE" +string = "#9ECE6A" +number = "#FF9E64" +comment = "#565F89" +preprocessor = "#7DCFFF" +current_line = "#292E42" diff --git a/src/ui/theme.cpp b/src/ui/theme.cpp new file mode 100644 index 0000000..699b4d4 --- /dev/null +++ b/src/ui/theme.cpp @@ -0,0 +1,254 @@ +#include "ui/theme.h" + +#include "core/log.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace Hsdbg +{ + namespace + { + auto trim(std::string_view text) -> std::string_view + { + while (!text.empty() && std::isspace(static_cast(text.front()))) + text.remove_prefix(1); + while (!text.empty() && std::isspace(static_cast(text.back()))) + text.remove_suffix(1); + + return text; + } + + auto strip_comment(std::string_view line) -> std::string_view + { + bool in_quote = false; + for (size_t i = 0; i < line.size(); ++i) + { + if (line[i] == '"') + in_quote = !in_quote; + else if (line[i] == '#' && !in_quote) + return line.substr(0, i); + } + return line; + } + + auto unquote(std::string_view value) -> std::string_view + { + if (value.size() >= 2 && value.front() == '"' && value.back() == '"') + return value.substr(1, value.size() - 2); + return value; + } + + auto hex_pair(std::string_view text) -> float + { + int value = 0; + std::from_chars(text.data(), text.data() + text.size(), value, 16); + return static_cast(value) / 255.0f; + } + + auto parse_color(std::string_view value, ImVec4 fallback) -> ImVec4 + { + value = trim(unquote(trim(value))); + if (value.empty()) + return fallback; + + if (value.front() == '#') + { + value.remove_prefix(1); + ImVec4 out = fallback; + out.w = 1.0f; + + if (value.size() == 3) + { + const auto nibble = [&](size_t i) { + const char pair[2] = { value[i], value[i] }; + return hex_pair(std::string_view(pair, 2)); + }; + out.x = nibble(0); + out.y = nibble(1); + out.z = nibble(2); + return out; + } + + if (value.size() == 6 || value.size() == 8) + { + out.x = hex_pair(value.substr(0, 2)); + out.y = hex_pair(value.substr(2, 2)); + out.z = hex_pair(value.substr(4, 2)); + if (value.size() == 8) + out.w = hex_pair(value.substr(6, 2)); + return out; + } + + return fallback; + } + + if (value.front() == '[') + { + value.remove_prefix(1); + if (!value.empty() && value.back() == ']') + value.remove_suffix(1); + + std::array parts = { fallback.x, fallback.y, fallback.z, 1.0f }; + size_t count = 0; + while (!value.empty() && count < parts.size()) + { + const size_t comma = value.find(','); + const std::string_view token = trim(value.substr(0, comma)); + float parsed = parts[count]; + std::from_chars(token.data(), token.data() + token.size(), parsed); + parts[count++] = parsed; + if (comma == std::string_view::npos) + break; + value.remove_prefix(comma + 1); + } + + if (count >= 3) + return ImVec4(parts[0], parts[1], parts[2], parts[3]); + } + + return fallback; + } + + auto parse_float(std::string_view value, float fallback) -> float + { + value = trim(unquote(trim(value))); + float parsed = fallback; + std::from_chars(value.data(), value.data() + value.size(), parsed); + return parsed; + } + + using Section = std::unordered_map; + using Document = std::unordered_map; + + auto parse_document(const std::filesystem::path& path, Document& out) -> bool + { + std::ifstream file(path); + if (!file.is_open()) + return false; + + std::string current; + std::string line; + while (std::getline(file, line)) + { + std::string_view view = trim(strip_comment(line)); + if (view.empty()) + continue; + + if (view.front() == '[') + { + const size_t close = view.find(']'); + if (close != std::string_view::npos) + current = std::string(trim(view.substr(1, close - 1))); + continue; + } + + const size_t equals = view.find('='); + if (equals == std::string_view::npos) + continue; + + const std::string key(trim(view.substr(0, equals))); + const std::string value(trim(view.substr(equals + 1))); + out[current][key] = value; + } + + return true; + } + + auto lookup(const Document& doc, std::string_view section, std::string_view key) + -> const std::string* + { + const auto s = doc.find(std::string(section)); + if (s == doc.end()) + return nullptr; + const auto k = s->second.find(std::string(key)); + return k == s->second.end() ? nullptr : &k->second; + } + } + + auto default_theme() -> Theme + { + return Theme{}; + } + + auto load_theme(const std::filesystem::path& path) -> Theme + { + Theme theme = default_theme(); + + Document doc; + if (!parse_document(path, doc)) + { + Log::warn("theme: could not read '{}'", path.string()); + return theme; + } + + if (const std::string* name = lookup(doc, "theme", "name")) + theme.name = std::string(unquote(trim(*name))); + + const auto color = [&](const char* section, const char* key, ImVec4& field) { + if (const std::string* value = lookup(doc, section, key)) + field = parse_color(*value, field); + }; + + color("palette", "text", theme.text); + color("palette", "text_dim", theme.text_dim); + color("palette", "bg", theme.bg); + color("palette", "bg_low", theme.bg_low); + color("palette", "bg_high", theme.bg_high); + color("palette", "surface", theme.surface); + color("palette", "border", theme.border); + color("palette", "accent", theme.accent); + + if (const std::string* value = lookup(doc, "style", "rounding")) + theme.rounding = parse_float(*value, theme.rounding); + if (const std::string* value = lookup(doc, "style", "window_border")) + theme.window_border = parse_float(*value, theme.window_border); + + color("syntax", "keyword", theme.syntax_keyword); + color("syntax", "type", theme.syntax_type); + color("syntax", "string", theme.syntax_string); + color("syntax", "number", theme.syntax_number); + color("syntax", "comment", theme.syntax_comment); + color("syntax", "preprocessor", theme.syntax_preprocessor); + color("syntax", "current_line", theme.current_line); + + return theme; + } + + auto list_themes(const std::filesystem::path& dir) -> std::vector + { + std::vector entries; + + std::error_code error; + if (!std::filesystem::is_directory(dir, error)) + return entries; + + for (const auto& item : std::filesystem::directory_iterator(dir, error)) + { + if (!item.is_regular_file() || item.path().extension() != ".toml") + continue; + + Document doc; + std::string name = item.path().stem().string(); + if (parse_document(item.path(), doc)) + { + if (const std::string* value = lookup(doc, "theme", "name")) + name = std::string(unquote(trim(*value))); + } + + entries.push_back(ThemeEntry{ item.path().stem().string(), std::move(name), item.path() }); + } + + std::sort(entries.begin(), entries.end(), + [](const ThemeEntry& a, const ThemeEntry& b) { return a.name < b.name; }); + + return entries; + } +} diff --git a/src/ui/theme.h b/src/ui/theme.h new file mode 100644 index 0000000..da17c18 --- /dev/null +++ b/src/ui/theme.h @@ -0,0 +1,48 @@ +#pragma once + +#include + +#include +#include +#include + +namespace Hsdbg +{ + struct Theme + { + std::string name = "Midnight"; + + ImVec4 text = ImVec4(0.925f, 0.933f, 0.953f, 1.00f); + ImVec4 text_dim = ImVec4(0.514f, 0.545f, 0.620f, 1.00f); + ImVec4 bg = ImVec4(0.082f, 0.086f, 0.110f, 1.00f); + ImVec4 bg_low = ImVec4(0.063f, 0.067f, 0.086f, 1.00f); + ImVec4 bg_high = ImVec4(0.118f, 0.125f, 0.157f, 1.00f); + ImVec4 surface = ImVec4(0.149f, 0.161f, 0.200f, 1.00f); + ImVec4 border = ImVec4(0.212f, 0.227f, 0.278f, 1.00f); + + ImVec4 accent = ImVec4(0.302f, 0.714f, 0.769f, 1.00f); + float rounding = 6.0f; + float window_border = 1.0f; + + ImVec4 syntax_keyword = ImVec4(0.337f, 0.612f, 0.839f, 1.00f); + ImVec4 syntax_type = ImVec4(0.306f, 0.788f, 0.690f, 1.00f); + ImVec4 syntax_string = ImVec4(0.808f, 0.569f, 0.471f, 1.00f); + ImVec4 syntax_number = ImVec4(0.710f, 0.808f, 0.659f, 1.00f); + ImVec4 syntax_comment = ImVec4(0.416f, 0.600f, 0.333f, 1.00f); + ImVec4 syntax_preprocessor = ImVec4(0.773f, 0.525f, 0.753f, 1.00f); + ImVec4 current_line = ImVec4(0.227f, 0.282f, 0.180f, 1.00f); + }; + + struct ThemeEntry + { + std::string id; + std::string name; + std::filesystem::path path; + }; + + auto default_theme() -> Theme; + + auto list_themes(const std::filesystem::path& dir) -> std::vector; + + auto load_theme(const std::filesystem::path& path) -> Theme; +} -- cgit v1.3