/* * settings.cpp * MontaukOS Terminal - settings panel * * Copyright (c) 2026 Daniel Hammer */ #include "settings.hpp" #include #include #include #include #include #include #include #include #include extern "C" { #include } using namespace gui; namespace termset { // ==== Themes ==== struct ThemeEntry { const char* id; // stable key written to the config file const char* name; // shown in the panel TermPalette palette; }; // Palette order is the ANSI one: black, red, green, yellow, blue, magenta, // cyan, white, then the eight bright variants. static const ThemeEntry kThemes[] = { { "montauk-dark", "Montauk Dark", TERM_PALETTE_DEFAULT }, { "pure-black", "Pure Black", { Color::from_hex(0x000000), Color::from_hex(0xFFFFFF), Color::from_hex(0xFFFFFF), { Color::from_hex(0x000000), Color::from_hex(0xD00000), Color::from_hex(0x00C000), Color::from_hex(0xC0C000), Color::from_hex(0x4060E0), Color::from_hex(0xC000C0), Color::from_hex(0x00C0C0), Color::from_hex(0xE5E5E5), Color::from_hex(0x7F7F7F), Color::from_hex(0xFF4040), Color::from_hex(0x40FF40), Color::from_hex(0xFFFF40), Color::from_hex(0x6080FF), Color::from_hex(0xFF40FF), Color::from_hex(0x40FFFF), Color::from_hex(0xFFFFFF), } } }, { "solarized-dark", "Solarized Dark", { Color::from_hex(0x002B36), Color::from_hex(0x839496), Color::from_hex(0x93A1A1), { Color::from_hex(0x073642), Color::from_hex(0xDC322F), Color::from_hex(0x859900), Color::from_hex(0xB58900), Color::from_hex(0x268BD2), Color::from_hex(0xD33682), Color::from_hex(0x2AA198), Color::from_hex(0xEEE8D5), Color::from_hex(0x586E75), Color::from_hex(0xCB4B16), Color::from_hex(0x93A1A1), Color::from_hex(0x657B83), Color::from_hex(0x839496), Color::from_hex(0x6C71C4), Color::from_hex(0x93A1A1), Color::from_hex(0xFDF6E3), } } }, { "solarized-light", "Solarized Light", { Color::from_hex(0xFDF6E3), Color::from_hex(0x657B83), Color::from_hex(0x586E75), { Color::from_hex(0x073642), Color::from_hex(0xDC322F), Color::from_hex(0x859900), Color::from_hex(0xB58900), Color::from_hex(0x268BD2), Color::from_hex(0xD33682), Color::from_hex(0x2AA198), Color::from_hex(0xEEE8D5), Color::from_hex(0x586E75), Color::from_hex(0xCB4B16), Color::from_hex(0x93A1A1), Color::from_hex(0x657B83), Color::from_hex(0x839496), Color::from_hex(0x6C71C4), Color::from_hex(0x93A1A1), Color::from_hex(0xFDF6E3), } } }, { "gruvbox-dark", "Gruvbox Dark", { Color::from_hex(0x282828), Color::from_hex(0xEBDBB2), Color::from_hex(0xEBDBB2), { Color::from_hex(0x282828), Color::from_hex(0xCC241D), Color::from_hex(0x98971A), Color::from_hex(0xD79921), Color::from_hex(0x458588), Color::from_hex(0xB16286), Color::from_hex(0x689D6A), Color::from_hex(0xA89984), Color::from_hex(0x928374), Color::from_hex(0xFB4934), Color::from_hex(0xB8BB26), Color::from_hex(0xFABD2F), Color::from_hex(0x83A598), Color::from_hex(0xD3869B), Color::from_hex(0x8EC07C), Color::from_hex(0xEBDBB2), } } }, { "nord", "Nord", { Color::from_hex(0x2E3440), Color::from_hex(0xD8DEE9), Color::from_hex(0xD8DEE9), { Color::from_hex(0x3B4252), Color::from_hex(0xBF616A), Color::from_hex(0xA3BE8C), Color::from_hex(0xEBCB8B), Color::from_hex(0x81A1C1), Color::from_hex(0xB48EAD), Color::from_hex(0x88C0D0), Color::from_hex(0xE5E9F0), Color::from_hex(0x4C566A), Color::from_hex(0xD08770), Color::from_hex(0xB9D4A0), Color::from_hex(0xF0D399), Color::from_hex(0x8FA8CE), Color::from_hex(0xC3A0BB), Color::from_hex(0x8FBCBB), Color::from_hex(0xECEFF4), } } }, { "dracula", "Dracula", { Color::from_hex(0x282A36), Color::from_hex(0xF8F8F2), Color::from_hex(0xF8F8F2), { Color::from_hex(0x21222C), Color::from_hex(0xFF5555), Color::from_hex(0x50FA7B), Color::from_hex(0xF1FA8C), Color::from_hex(0xBD93F9), Color::from_hex(0xFF79C6), Color::from_hex(0x8BE9FD), Color::from_hex(0xF8F8F2), Color::from_hex(0x6272A4), Color::from_hex(0xFF6E6E), Color::from_hex(0x69FF94), Color::from_hex(0xFFFFA5), Color::from_hex(0xD6ACFF), Color::from_hex(0xFF92DF), Color::from_hex(0xA4FFFF), Color::from_hex(0xFFFFFF), } } }, { "paper-light", "Paper Light", { Color::from_hex(0xFFFFFF), Color::from_hex(0x33333A), Color::from_hex(0x33333A), { Color::from_hex(0x2E3436), Color::from_hex(0xC01C28), Color::from_hex(0x26A269), Color::from_hex(0xA2734C), Color::from_hex(0x12488B), Color::from_hex(0xA347BA), Color::from_hex(0x2AA1B3), Color::from_hex(0x8B8E8F), Color::from_hex(0x5E5C64), Color::from_hex(0xF66151), Color::from_hex(0x33D17A), Color::from_hex(0xE9AD0C), Color::from_hex(0x2A7BDE), Color::from_hex(0xC061CB), Color::from_hex(0x33C7DE), Color::from_hex(0x3D3846), } } }, }; static constexpr int kThemeCount = (int)(sizeof(kThemes) / sizeof(kThemes[0])); static constexpr int FONT_SIZE_MIN = 8; static constexpr int FONT_SIZE_DEFAULT = 18; static constexpr int FONT_SIZE_MAX = 64; static constexpr int FONT_SIZE_STEP = 2; // ==== Panel state ==== static constexpr int PANEL_W = 380; static constexpr int PANEL_H = 452; static constexpr int PAD = 16; static constexpr int CARD_PAD = 4; static constexpr int ROW_H = 34; static constexpr int ROW_GAP = 2; static constexpr int STEP_BTN_W = 32; static constexpr int SWATCH = 12; static constexpr int SWATCH_GAP = 4; static constexpr int SWATCH_N = 6; struct Panel { int win_id; uint32_t* pixels; int width; int height; bool open; int mouse_x; int mouse_y; int theme_index; Callbacks cb; }; static Panel g_panel = {-1, nullptr, PANEL_W, PANEL_H, false, -1, -1, 0, {nullptr, nullptr}}; // The console session takes over the whole framebuffer with no desktop around // it, so it defaults to the plain black palette rather than the windowed // terminal's lighter grey. static constexpr const char* kConsoleDefaultTheme = "pure-black"; static bool g_console_session = false; // ==== Preferences ==== static void current_user(char* out, int cap) { if (montauk::getuser(out, cap) <= 0 || !out[0]) montauk::strcpy(out, "default"); } static int theme_index_by_id(const char* id) { for (int i = 0; i < kThemeCount; i++) { if (montauk::streq(kThemes[i].id, id)) return i; } return -1; } static int clamp_font_size(int size) { if (size < FONT_SIZE_MIN) return FONT_SIZE_MIN; if (size > FONT_SIZE_MAX) return FONT_SIZE_MAX; return size; } static void save_prefs() { char user[64]; current_user(user, sizeof(user)); montauk::toml::Doc doc = montauk::config::load_user(user, "terminal"); // The console session has no panel, so its theme is a default rather than a // choice -- writing it back would let a zoom in the console silently retheme // every windowed terminal. Leaving the key untouched round-trips whatever // the user actually picked. if (!g_console_session) montauk::config::set_string(&doc, "appearance.theme", kThemes[g_panel.theme_index].id); montauk::config::set_int(&doc, "appearance.font_size", fonts::TERM_SIZE); montauk::config::save_user(user, "terminal", &doc); doc.destroy(); } void load_prefs(bool console_session) { g_console_session = console_session; char user[64]; current_user(user, sizeof(user)); montauk::toml::Doc doc = montauk::config::load_user(user, "terminal"); int idx = theme_index_by_id(doc.get_string("appearance.theme", "")); int size = (int)doc.get_int("appearance.font_size", fonts::TERM_SIZE); doc.destroy(); // A saved theme is an explicit choice and wins in either mode; with none // saved, the full-screen console starts on Pure Black and the windowed // terminal on the default palette. if (idx < 0) { idx = console_session ? theme_index_by_id(kConsoleDefaultTheme) : 0; if (idx < 0) idx = 0; } g_panel.theme_index = idx; g_term_palette = kThemes[idx].palette; fonts::TERM_SIZE = clamp_font_size(size); } // ==== Applying changes ==== static void select_theme(int idx) { if (idx < 0 || idx >= kThemeCount || idx == g_panel.theme_index) return; TermPalette from = g_term_palette; g_panel.theme_index = idx; g_term_palette = kThemes[idx].palette; if (g_panel.cb.theme_changed) g_panel.cb.theme_changed(from, g_term_palette); save_prefs(); } static void step_font_size(int delta) { int size = clamp_font_size(fonts::TERM_SIZE + delta); if (size == fonts::TERM_SIZE) return; fonts::TERM_SIZE = size; if (g_panel.cb.font_changed) g_panel.cb.font_changed(); save_prefs(); render(); } void zoom_font(int direction) { step_font_size(direction * FONT_SIZE_STEP); } // ==== Layout ==== // // Sections are a muted label over a bordered card, matching the rest of the // system settings apps. The window titlebar already names the panel, so there // is deliberately no heading inside it. struct Layout { Rect theme_card; Rect theme_rows[kThemeCount]; Rect font_minus; Rect font_plus; Rect font_value; Rect reset_btn; Rect close_btn; int theme_label_y; int font_label_y; }; static Layout compute_layout() { Layout lo = {}; int fh = system_font_height(); int content_w = g_panel.width - PAD * 2; int y = PAD; lo.theme_label_y = y; y += fh + 6; lo.theme_card = {PAD, y, content_w, kThemeCount * ROW_H + (kThemeCount - 1) * ROW_GAP + CARD_PAD * 2}; int row_y = y + CARD_PAD; for (int i = 0; i < kThemeCount; i++) { lo.theme_rows[i] = {lo.theme_card.x + CARD_PAD, row_y, content_w - CARD_PAD * 2, ROW_H}; row_y += ROW_H + ROW_GAP; } y += lo.theme_card.h + 20; // Font size is a single setting, so it reads as a plain row -- label left, // stepper right-aligned to the same content edge as the card above -- // rather than a one-row card. int ctrl_h = 28; lo.font_label_y = y + (ctrl_h - fh) / 2; lo.font_plus = {PAD + content_w - STEP_BTN_W, y, STEP_BTN_W, ctrl_h}; lo.font_value = {lo.font_plus.x - 6 - 56, y, 56, ctrl_h}; lo.font_minus = {lo.font_value.x - 6 - STEP_BTN_W, y, STEP_BTN_W, ctrl_h}; int btn_h = 30; int btn_y = g_panel.height - PAD - btn_h; lo.close_btn = {g_panel.width - PAD - 90, btn_y, 90, btn_h}; lo.reset_btn = {PAD, btn_y, 140, btn_h}; return lo; } // ==== Rendering ==== // Background, four representative ANSI colors and the foreground, drawn as a // mini strip so each theme is identifiable without applying it. Every swatch // gets a hairline border -- without it a white background swatch vanishes into // the card on the light themes. static void draw_theme_preview(Canvas& c, const TermPalette& p, int x, int y, const mtk::Theme& theme) { Color strip[SWATCH_N] = { p.bg, p.ansi[1], p.ansi[2], p.ansi[4], p.ansi[5], p.fg }; for (int i = 0; i < SWATCH_N; i++) { int sx = x + i * (SWATCH + SWATCH_GAP); mtk::draw_rounded_frame(c, {sx, y, SWATCH, SWATCH}, 3, strip[i], mtk::mix(theme.border, strip[i], 80)); } } void render() { if (!g_panel.open || g_panel.win_id < 0 || !g_panel.pixels) return; Canvas c(g_panel.pixels, g_panel.width, g_panel.height); mtk::Theme theme = mtk::make_theme(); c.fill(theme.window_bg); Layout lo = compute_layout(); int fh = system_font_height(); c.text(PAD, lo.theme_label_y, "Theme", theme.text_subtle); mtk::draw_rounded_frame(c, lo.theme_card, 6, theme.surface_alt, theme.border); int preview_w = SWATCH_N * (SWATCH + SWATCH_GAP) - SWATCH_GAP; for (int i = 0; i < kThemeCount; i++) { const Rect& row = lo.theme_rows[i]; bool selected = (i == g_panel.theme_index); bool hovered = row.contains(g_panel.mouse_x, g_panel.mouse_y); if (selected) mtk::draw_list_row(c, row, true, false, theme); else if (hovered) c.fill_rounded_rect(row.x, row.y, row.w, row.h, theme.radius_md, theme.surface_hover); c.text(row.x + 10, row.y + (row.h - fh) / 2, kThemes[i].name, selected ? theme.text_inverse : theme.text); draw_theme_preview(c, kThemes[i].palette, row.x + row.w - 10 - preview_w, row.y + (row.h - SWATCH) / 2, theme); } c.text(PAD, lo.font_label_y, "Font size", theme.text); char value[16]; snprintf(value, sizeof(value), "%d px", fonts::TERM_SIZE); int vw = text_width(value); c.text(lo.font_value.x + (lo.font_value.w - vw) / 2, lo.font_value.y + (lo.font_value.h - fh) / 2, value, theme.text); bool can_shrink = fonts::TERM_SIZE > FONT_SIZE_MIN; bool can_grow = fonts::TERM_SIZE < FONT_SIZE_MAX; mtk::draw_button(c, lo.font_minus, "-", mtk::BUTTON_SECONDARY, mtk::widget_state(false, can_shrink && lo.font_minus.contains( g_panel.mouse_x, g_panel.mouse_y), can_shrink), theme); mtk::draw_button(c, lo.font_plus, "+", mtk::BUTTON_SECONDARY, mtk::widget_state(false, can_grow && lo.font_plus.contains( g_panel.mouse_x, g_panel.mouse_y), can_grow), theme); mtk::draw_button(c, lo.reset_btn, "Reset defaults", mtk::BUTTON_SECONDARY, mtk::widget_state(false, lo.reset_btn.contains(g_panel.mouse_x, g_panel.mouse_y)), theme); mtk::draw_button(c, lo.close_btn, "Close", mtk::BUTTON_PRIMARY, mtk::widget_state(false, lo.close_btn.contains(g_panel.mouse_x, g_panel.mouse_y)), theme); montauk::win_present(g_panel.win_id); } // ==== Input ==== static void reset_defaults() { select_theme(0); int target = FONT_SIZE_DEFAULT; if (target != fonts::TERM_SIZE) step_font_size(target - fonts::TERM_SIZE); } static void handle_mouse(const montauk::abi::WinEvent& ev) { g_panel.mouse_x = ev.mouse.x; g_panel.mouse_y = ev.mouse.y; bool pressed = (ev.mouse.buttons & 1) && !(ev.mouse.prev_buttons & 1); if (!pressed) return; Layout lo = compute_layout(); for (int i = 0; i < kThemeCount; i++) { if (lo.theme_rows[i].contains(g_panel.mouse_x, g_panel.mouse_y)) { select_theme(i); return; } } if (lo.font_minus.contains(g_panel.mouse_x, g_panel.mouse_y)) { step_font_size(-FONT_SIZE_STEP); } else if (lo.font_plus.contains(g_panel.mouse_x, g_panel.mouse_y)) { step_font_size(FONT_SIZE_STEP); } else if (lo.reset_btn.contains(g_panel.mouse_x, g_panel.mouse_y)) { reset_defaults(); } else if (lo.close_btn.contains(g_panel.mouse_x, g_panel.mouse_y)) { close(); } } static void handle_key(const montauk::abi::KeyEvent& key) { if (!key.pressed) return; if (key.scancode == 0x01) { // Escape close(); return; } if (key.ascii == '+' || key.ascii == '=') { step_font_size(FONT_SIZE_STEP); } else if (key.ascii == '-') { step_font_size(-FONT_SIZE_STEP); } else if (key.scancode == 0x48) { // Up select_theme(g_panel.theme_index - 1); } else if (key.scancode == 0x50) { // Down select_theme(g_panel.theme_index + 1); } } // ==== Lifecycle ==== void init(const Callbacks& cb) { g_panel.cb = cb; } bool is_open() { return g_panel.open; } void open() { if (g_panel.open) { render(); return; } montauk::abi::WinCreateResult wres; if (montauk::win_create("Terminal Settings", PANEL_W, PANEL_H, &wres) < 0 || wres.id < 0) return; g_panel.win_id = wres.id; g_panel.pixels = (uint32_t*)(uintptr_t)wres.pixelVa; g_panel.width = PANEL_W; g_panel.height = PANEL_H; g_panel.mouse_x = -1; g_panel.mouse_y = -1; g_panel.open = true; render(); } void close() { if (!g_panel.open) return; if (g_panel.win_id >= 0) montauk::win_destroy(g_panel.win_id); g_panel.win_id = -1; g_panel.pixels = nullptr; g_panel.open = false; } void poll() { if (!g_panel.open || g_panel.win_id < 0) return; bool redraw = false; montauk::abi::WinEvent ev; int r; while ((r = montauk::win_poll(g_panel.win_id, &ev)) > 0) { if (ev.type == 3) { close(); return; } if (ev.type == 0) { handle_key(ev.key); } else if (ev.type == 1) { handle_mouse(ev); } else if (ev.type == 2) { g_panel.width = ev.resize.w; g_panel.height = ev.resize.h; g_panel.pixels = (uint32_t*)(uintptr_t)montauk::win_resize( g_panel.win_id, ev.resize.w, ev.resize.h); } redraw = true; if (!g_panel.open) return; } if (r < 0) { close(); return; } if (redraw) render(); } } // namespace termset