feat: add settings panel to Terminal

This commit is contained in:
2026-08-29 18:24:07 +02:00
parent e7646bbbdb
commit 615ca7308a
6 changed files with 845 additions and 72 deletions
+1 -1
View File
@@ -12,4 +12,4 @@
#pragma once #pragma once
#define MONTAUK_BUILD_NUMBER 168 #define MONTAUK_BUILD_NUMBER 175
+98 -39
View File
@@ -60,32 +60,47 @@ struct TerminalState {
int csi_current_param; int csi_current_param;
}; };
// ==== Palette ====
//
// The palette is a mutable global rather than a set of constants because the
// terminal lets the user switch themes at runtime. Cells store resolved colors,
// so a theme switch also has to rewrite the cells that were painted with the
// outgoing palette -- see terminal_remap_palette below.
struct TermPalette {
Color bg;
Color fg;
Color cursor;
Color ansi[16];
};
inline constexpr TermPalette TERM_PALETTE_DEFAULT = {
colors::TERM_BG,
colors::TERM_FG,
colors::TERM_FG,
{
Color::from_hex(0x000000), Color::from_hex(0xCC0000),
Color::from_hex(0x4E9A06), Color::from_hex(0xC4A000),
Color::from_hex(0x3465A4), Color::from_hex(0x75507B),
Color::from_hex(0x06989A), Color::from_hex(0xD3D7CF),
Color::from_hex(0x555753), Color::from_hex(0xEF2929),
Color::from_hex(0x8AE234), Color::from_hex(0xFCE94F),
Color::from_hex(0x729FCF), Color::from_hex(0xAD7FA8),
Color::from_hex(0x34E2E2), Color::from_hex(0xEEEEEC),
}
};
inline TermPalette g_term_palette = TERM_PALETTE_DEFAULT;
// Standard ANSI color palette as ARGB pixels // Standard ANSI color palette as ARGB pixels
static inline Color term_ansi_color(int idx) { static inline Color term_ansi_color(int idx) {
switch (idx) { if (idx < 0 || idx > 15) return g_term_palette.fg;
case 0: return Color::from_hex(0x000000); return g_term_palette.ansi[idx];
case 1: return Color::from_hex(0xCC0000);
case 2: return Color::from_hex(0x4E9A06);
case 3: return Color::from_hex(0xC4A000);
case 4: return Color::from_hex(0x3465A4);
case 5: return Color::from_hex(0x75507B);
case 6: return Color::from_hex(0x06989A);
case 7: return Color::from_hex(0xD3D7CF);
case 8: return Color::from_hex(0x555753);
case 9: return Color::from_hex(0xEF2929);
case 10: return Color::from_hex(0x8AE234);
case 11: return Color::from_hex(0xFCE94F);
case 12: return Color::from_hex(0x729FCF);
case 13: return Color::from_hex(0xAD7FA8);
case 14: return Color::from_hex(0x34E2E2);
case 15: return Color::from_hex(0xEEEEEC);
default: return colors::TERM_FG;
}
} }
// ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale) // ANSI 256-color palette (0-15 = standard, 16-231 = RGB cube, 232-255 = grayscale)
static inline Color term_ansi_256_color(int idx) { static inline Color term_ansi_256_color(int idx) {
if (idx < 0) return colors::TERM_FG; if (idx < 0) return g_term_palette.fg;
if (idx <= 15) return term_ansi_color(idx); if (idx <= 15) return term_ansi_color(idx);
if (idx <= 231) { if (idx <= 231) {
// 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5) // 6x6x6 RGB cube: idx 16 = (0,0,0), idx 231 = (5,5,5)
@@ -119,6 +134,47 @@ static inline void terminal_invalidate_render_cache(TerminalState* t) {
t->render_cursor_visible = false; t->render_cursor_visible = false;
} }
// Translate one already-resolved cell color from an outgoing palette to the
// incoming one. Colors that are not palette entries -- 256-color cube and
// grayscale ramp values -- are left exactly as the program asked for them.
static inline Color term_remap_color(Color c, const TermPalette& from,
const TermPalette& to) {
if (term_color_equal(c, from.bg)) return to.bg;
if (term_color_equal(c, from.fg)) return to.fg;
for (int i = 0; i < 16; i++) {
if (term_color_equal(c, from.ansi[i])) return to.ansi[i];
}
return c;
}
// Repaint an existing terminal in a new palette. Cells carry resolved colors,
// so switching themes without this leaves all scrollback -- and the shell's
// colored prompt -- in the old theme's colors.
static inline void terminal_remap_palette(TerminalState* t,
const TermPalette& from,
const TermPalette& to) {
if (!t || !t->cells) return;
int total = (t->rows + t->max_scrollback) * t->cols;
for (int i = 0; i < total; i++) {
t->cells[i].fg = term_remap_color(t->cells[i].fg, from, to);
t->cells[i].bg = term_remap_color(t->cells[i].bg, from, to);
}
if (t->alt_cells) {
int screen = t->rows * t->cols;
for (int i = 0; i < screen; i++) {
t->alt_cells[i].fg = term_remap_color(t->alt_cells[i].fg, from, to);
t->alt_cells[i].bg = term_remap_color(t->alt_cells[i].bg, from, to);
}
}
t->current_fg = term_remap_color(t->current_fg, from, to);
t->current_bg = term_remap_color(t->current_bg, from, to);
terminal_invalidate_render_cache(t);
t->dirty = true;
}
static inline Color terminal_erase_bg(TerminalState* t) { static inline Color terminal_erase_bg(TerminalState* t) {
return t->current_bg; return t->current_bg;
} }
@@ -163,8 +219,8 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
t->scrollback_lines = 0; t->scrollback_lines = 0;
t->max_scrollback = max_sb; t->max_scrollback = max_sb;
t->view_offset = 0; t->view_offset = 0;
t->current_fg = colors::TERM_FG; t->current_fg = g_term_palette.fg;
t->current_bg = colors::TERM_BG; t->current_bg = g_term_palette.bg;
t->cursor_visible = false; t->cursor_visible = false;
t->alt_screen_active = false; t->alt_screen_active = false;
t->reverse_video = false; t->reverse_video = false;
@@ -202,10 +258,10 @@ static inline void terminal_init_cells(TerminalState* t, int cols, int rows, int
return; return;
} }
for (int i = 0; i < total_cells; i++) { for (int i = 0; i < total_cells; i++) {
t->cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; t->cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
} }
for (int i = 0; i < screen_cells; i++) { for (int i = 0; i < screen_cells; i++) {
t->alt_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; t->alt_cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
} }
} }
@@ -258,7 +314,7 @@ static inline void terminal_enter_alt_screen(TerminalState* t) {
TermCell* screen = term_screen_row(t, 0); TermCell* screen = term_screen_row(t, 0);
for (int i = 0; i < total; i++) { for (int i = 0; i < total; i++) {
t->alt_cells[i] = screen[i]; t->alt_cells[i] = screen[i];
screen[i] = {' ', colors::TERM_FG, colors::TERM_BG}; screen[i] = {' ', g_term_palette.fg, g_term_palette.bg};
} }
t->view_offset = 0; t->view_offset = 0;
t->cursor_x = 0; t->cursor_x = 0;
@@ -437,8 +493,8 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
} }
if (code == 0) { if (code == 0) {
t->current_fg = colors::TERM_FG; t->current_fg = g_term_palette.fg;
t->current_bg = colors::TERM_BG; t->current_bg = g_term_palette.bg;
t->reverse_video = false; t->reverse_video = false;
} else if (code == 1) { } else if (code == 1) {
// Bold: map to bright version of current color // Bold: map to bright version of current color
@@ -486,15 +542,15 @@ static inline void terminal_process_csi(TerminalState* t, char cmd) {
} else if (code >= 100 && code <= 107) { } else if (code >= 100 && code <= 107) {
t->current_bg = term_ansi_color(code - 100 + 8); t->current_bg = term_ansi_color(code - 100 + 8);
} else if (code == 39) { } else if (code == 39) {
t->current_fg = colors::TERM_FG; t->current_fg = g_term_palette.fg;
} else if (code == 49) { } else if (code == 49) {
t->current_bg = colors::TERM_BG; t->current_bg = g_term_palette.bg;
} }
} }
if (t->csi_param_count == 0) { if (t->csi_param_count == 0) {
// ESC[m with no params = reset // ESC[m with no params = reset
t->current_fg = colors::TERM_FG; t->current_fg = g_term_palette.fg;
t->current_bg = colors::TERM_BG; t->current_bg = g_term_palette.bg;
t->reverse_video = false; t->reverse_video = false;
} }
break; break;
@@ -545,8 +601,8 @@ static inline void terminal_feed(TerminalState* t, const char* data, int len) {
for (int j = 0; j < 8; j++) t->csi_params[j] = 0; for (int j = 0; j < 8; j++) t->csi_params[j] = 0;
} else if (ch == 'c') { } else if (ch == 'c') {
// Reset terminal // Reset terminal
t->current_fg = colors::TERM_FG; t->current_fg = g_term_palette.fg;
t->current_bg = colors::TERM_BG; t->current_bg = g_term_palette.bg;
t->cursor_x = 0; t->cursor_x = 0;
t->cursor_y = 0; t->cursor_y = 0;
t->parse_state = TerminalState::STATE_NORMAL; t->parse_state = TerminalState::STATE_NORMAL;
@@ -640,18 +696,21 @@ static inline void terminal_draw_cursor_pixels(const TermCell& cell,
uint32_t* pixels, int pw, int ph, uint32_t* pixels, int pw, int ph,
int px, int py, int cell_w, int cell_h, int px, int py, int cell_w, int cell_h,
bool use_ttf, GlyphCache* gc) { bool use_ttf, GlyphCache* gc) {
// The block cursor takes its color from the palette and punches the glyph
// out in the background color, so it stays legible on light themes too --
// a hardcoded white block disappears on a white background.
terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h, terminal_fill_pixel_rect(pixels, pw, ph, px, py, cell_w, cell_h,
colors::WHITE.to_pixel()); g_term_palette.cursor.to_pixel());
if (cell.ch <= 32 && cell.ch >= 0) return; if (cell.ch <= 32 && cell.ch >= 0) return;
if (use_ttf) { if (use_ttf) {
int baseline = py + gc->ascent; int baseline = py + gc->ascent;
fonts::mono->draw_char_to_buffer(pixels, pw, ph, fonts::mono->draw_char_to_buffer(pixels, pw, ph,
px, baseline, (unsigned char)cell.ch, colors::BLACK, gc); px, baseline, (unsigned char)cell.ch, g_term_palette.bg, gc);
} else { } else {
terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch, terminal_draw_bitmap_char(pixels, pw, ph, px, py, (unsigned char)cell.ch,
colors::BLACK.to_pixel()); g_term_palette.bg.to_pixel());
} }
} }
@@ -713,7 +772,7 @@ static inline void terminal_render(TerminalState* t, uint32_t* pixels, int pw, i
t->render_base_row != base_row || t->render_base_row != base_row ||
t->render_term_cols != t->cols || t->render_term_rows != t->rows; t->render_term_cols != t->cols || t->render_term_rows != t->rows;
uint32_t bg_px = colors::TERM_BG.to_pixel(); uint32_t bg_px = g_term_palette.bg.to_pixel();
if (full_redraw) { if (full_redraw) {
int row_bytes = pw * sizeof(uint32_t); int row_bytes = pw * sizeof(uint32_t);
for (int i = 0; i < pw; i++) pixels[i] = bg_px; for (int i = 0; i < pw; i++) pixels[i] = bg_px;
@@ -792,9 +851,9 @@ static inline void terminal_resize(TerminalState* t, int new_cols, int new_rows)
// Clear new buffers // Clear new buffers
for (int i = 0; i < new_total; i++) for (int i = 0; i < new_total; i++)
new_cells[i] = {' ', colors::TERM_FG, colors::TERM_BG}; new_cells[i] = {' ', g_term_palette.fg, g_term_palette.bg};
for (int i = 0; i < new_rows * new_cols; i++) for (int i = 0; i < new_rows * new_cols; i++)
new_alt[i] = {' ', colors::TERM_FG, colors::TERM_BG}; new_alt[i] = {' ', g_term_palette.fg, g_term_palette.bg};
// Copy content: scrollback + visible screen // Copy content: scrollback + visible screen
int old_content = t->scrollback_lines + t->rows; int old_content = t->scrollback_lines + t->rows;
+1 -1
View File
@@ -41,7 +41,7 @@ LDFLAGS := \
-Wl,--gc-sections \ -Wl,--gc-sections \
-T $(LINK_LD) -T $(LINK_LD)
SRCS := main.cpp stb_truetype_impl.cpp font_data.cpp SRCS := main.cpp settings.cpp stb_truetype_impl.cpp font_data.cpp
OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o)) OBJS := $(addprefix $(OBJDIR)/,$(SRCS:.cpp=.o))
DEPS := $(OBJS:.o=.d) DEPS := $(OBJS:.o=.d)
+173 -31
View File
@@ -1,7 +1,6 @@
/* /*
* main.cpp * main.cpp
* MontaukOS Terminal - standalone Window Server app * MontaukOS Terminal - standalone Window Server app
* Preserves the old desktop-integrated terminal layout and tab behavior.
* *
* Two run modes: * Two run modes:
* - Windowed (default): runs as a Window Server client inside the desktop. * - Windowed (default): runs as a Window Server client inside the desktop.
@@ -21,8 +20,11 @@
#include <gui/framebuffer.hpp> #include <gui/framebuffer.hpp>
#include <gui/standalone.hpp> #include <gui/standalone.hpp>
#include <gui/terminal.hpp> #include <gui/terminal.hpp>
#include <gui/mtk/theme.hpp>
#include <gui/truetype.hpp> #include <gui/truetype.hpp>
#include "settings.hpp"
extern "C" { extern "C" {
#include <stdio.h> #include <stdio.h>
} }
@@ -39,6 +41,8 @@ static constexpr int TERM_TAB_GAP = 4;
static constexpr int TERM_PLUS_W = 28; static constexpr int TERM_PLUS_W = 28;
static constexpr int TERM_PLUS_PAD = 8; static constexpr int TERM_PLUS_PAD = 8;
static constexpr int TERM_TAB_PAD = 8; static constexpr int TERM_TAB_PAD = 8;
static constexpr int TERM_COG_W = 28;
static constexpr int TERM_COG_GAP = 6;
struct TermTabs { struct TermTabs {
TerminalState* tabs[TERM_MAX_TABS]; TerminalState* tabs[TERM_MAX_TABS];
@@ -55,6 +59,9 @@ static bool g_force_redraw = true;
static int g_last_win_w = 0; static int g_last_win_w = 0;
static int g_last_win_h = 0; static int g_last_win_h = 0;
static bool g_show_clock = false; // console (full-screen) mode draws a live clock static bool g_show_clock = false; // console (full-screen) mode draws a live clock
// The settings panel is a Window Server window, so it only exists in windowed
// mode; the console session has no window server to open it in.
static bool g_show_cog = false;
static char g_clock_text[16] = {}; static char g_clock_text[16] = {};
static montauk::abi::ProcInfo g_kill_procs[256]; static montauk::abi::ProcInfo g_kill_procs[256];
static int g_kill_pids[256]; static int g_kill_pids[256];
@@ -180,13 +187,59 @@ static bool term_poll_tabs() {
return changed || g_force_redraw; return changed || g_force_redraw;
} }
// ==== Tab bar colors ====
//
// The tab bar is derived from the active terminal palette rather than
// hardcoded, so a light theme gets a light chrome with dark labels instead of
// the dark-theme strip the terminal originally shipped.
struct TabBarColors {
Color bar_bg;
Color tab_active_bg;
Color tab_inactive_bg;
Color label_active;
Color label_inactive;
Color close_active;
Color close_inactive;
Color furniture;
Color clock;
};
// Chrome shades are cut from the terminal background. A dark background can
// absorb a heavy darkening (the amounts below reproduce the original #1C1C1C
// bar and #262626 tabs on the default palette); a light one only tolerates a
// gentle one before the chrome reads as a different, darker window.
static Color term_shade(Color bg, uint8_t dark_amount, uint8_t light_amount) {
int lum = (bg.r * 30 + bg.g * 59 + bg.b * 11) / 100;
return mtk::darken(bg, lum > 140 ? light_amount : dark_amount);
}
static TabBarColors term_bar_colors() {
Color bg = g_term_palette.bg;
Color fg = g_term_palette.fg;
TabBarColors t;
t.bar_bg = term_shade(bg, 96, 30);
t.tab_active_bg = bg;
t.tab_inactive_bg = term_shade(bg, 40, 12);
t.label_active = fg;
t.label_inactive = mtk::mix(bg, fg, 130);
t.close_active = mtk::mix(bg, fg, 110);
t.close_inactive = mtk::mix(bg, fg, 50);
t.furniture = t.label_inactive;
t.clock = mtk::mix(bg, fg, 175);
return t;
}
// Right-hand tab-bar furniture. In console mode a live clock sits at the far // Right-hand tab-bar furniture. In console mode a live clock sits at the far
// right and the new-tab (+) button shifts left to make room; in windowed mode // right and the new-tab (+) button shifts left to make room; in windowed mode
// there is no clock and the + keeps its original far-right position. // there is no clock and the + keeps its original far-right position.
struct TabBarRight { struct TabBarRight {
int clock_x; int clock_x;
int plus_x; int plus_x;
int cog_x;
bool has_clock; bool has_clock;
bool has_cog;
}; };
static TabBarRight term_tabbar_right(int width) { static TabBarRight term_tabbar_right(int width) {
@@ -200,9 +253,65 @@ static TabBarRight term_tabbar_right(int width) {
right_limit = r.clock_x - 12; right_limit = r.clock_x - 12;
} }
r.plus_x = right_limit - TERM_PLUS_W; r.plus_x = right_limit - TERM_PLUS_W;
// The cog keeps its slot even when the tab limit hides the + button, so it
// does not jump under the pointer as tabs are opened and closed.
r.has_cog = g_show_cog;
r.cog_x = r.plus_x - TERM_COG_GAP - TERM_COG_W;
return r; return r;
} }
static Rect term_cog_rect(int width) {
TabBarRight right = term_tabbar_right(width);
return {right.cog_x, 7, TERM_COG_W, 22};
}
// Flat cog glyph, drawn rather than loaded -- the icon set ships no gear.
//
// The shape is analytic: a disc with the hub bored out, plus eight teeth formed
// by intersecting an outer ring with four symmetric bands (two axis-aligned,
// two diagonal). Coverage is sampled 3x3 per pixel and blended against the
// button fill, so the curves stay smooth at this size without an alpha buffer.
// Units are sixths of a pixel, which keeps pixel centers and subsample offsets
// exact -- this app does no floating point.
static void term_draw_cog(Canvas& c, const Rect& box, Color fg, Color bg) {
static constexpr int R_HUB = 15; // 2.5 px
static constexpr int R_BODY = 33; // 5.5 px
static constexpr int R_OUT = 48; // 8.0 px
static constexpr int TOOTH = 10; // 1.7 px half-width
static constexpr int TOOTH_DIAG = 14; // TOOTH * sqrt(2)
auto inked = [](int dx, int dy) -> bool {
int d2 = dx * dx + dy * dy;
if (d2 <= R_HUB * R_HUB) return false;
if (d2 <= R_BODY * R_BODY) return true;
if (d2 > R_OUT * R_OUT) return false;
int ax = dx < 0 ? -dx : dx;
int ay = dy < 0 ? -dy : dy;
int au = (dx + dy) < 0 ? -(dx + dy) : (dx + dy);
int av = (dx - dy) < 0 ? -(dx - dy) : (dx - dy);
return ax <= TOOTH || ay <= TOOTH ||
au <= TOOTH_DIAG || av <= TOOTH_DIAG;
};
for (int y = 0; y < box.h; y++) {
for (int x = 0; x < box.w; x++) {
int cov = 0;
for (int sy = -2; sy <= 2; sy += 2) {
for (int sx = -2; sx <= 2; sx += 2) {
if (inked(6 * x + 3 + sx - 3 * box.w,
6 * y + 3 + sy - 3 * box.h))
cov++;
}
}
if (cov == 0) continue;
Color px = (cov == 9) ? fg
: mtk::mix(bg, fg, (uint8_t)((cov * 255) / 9));
c.put_pixel(box.x + x, box.y + y, px);
}
}
}
// Refresh the cached clock string from the wall clock; returns true when the // Refresh the cached clock string from the wall clock; returns true when the
// displayed text changed (so the caller can request a repaint). // displayed text changed (so the caller can request a repaint).
static bool term_refresh_clock() { static bool term_refresh_clock() {
@@ -263,8 +372,8 @@ static bool term_render_into(uint32_t* pixels, int width, int height) {
if (!g_force_redraw && !ts->dirty) return false; if (!g_force_redraw && !ts->dirty) return false;
Canvas c(pixels, width, height); Canvas c(pixels, width, height);
Color bar_bg = Color::from_hex(0x1C1C1C); TabBarColors bar = term_bar_colors();
c.fill_rect(0, 0, width, TERM_TAB_BAR_H, bar_bg); c.fill_rect(0, 0, width, TERM_TAB_BAR_H, bar.bar_bg);
int fh = system_font_height(); int fh = system_font_height();
int tab_x = TERM_TAB_PAD; int tab_x = TERM_TAB_PAD;
@@ -277,20 +386,20 @@ static bool term_render_into(uint32_t* pixels, int width, int height) {
if (active) { if (active) {
int ty = 5; int ty = 5;
int th = TERM_TAB_BAR_H - ty; int th = TERM_TAB_BAR_H - ty;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 6, colors::TERM_BG); c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 6, bar.tab_active_bg);
c.fill_rect(tab_x, TERM_TAB_BAR_H - 6, TERM_TAB_W, 6, colors::TERM_BG); c.fill_rect(tab_x, TERM_TAB_BAR_H - 6, TERM_TAB_W, 6, bar.tab_active_bg);
int text_y = ty + (th - fh) / 2; int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0xE0E0E0)); c.text(tab_x + 12, text_y, label, bar.label_active);
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x707070)); c.text(tab_x + TERM_TAB_W - 20, text_y, "x", bar.close_active);
} else { } else {
int ty = 7; int ty = 7;
int th = 22; int th = 22;
c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 5, Color::from_hex(0x262626)); c.fill_rounded_rect(tab_x, ty, TERM_TAB_W, th, 5, bar.tab_inactive_bg);
int text_y = ty + (th - fh) / 2; int text_y = ty + (th - fh) / 2;
c.text(tab_x + 12, text_y, label, Color::from_hex(0x6E6E6E)); c.text(tab_x + 12, text_y, label, bar.label_inactive);
c.text(tab_x + TERM_TAB_W - 20, text_y, "x", Color::from_hex(0x444444)); c.text(tab_x + TERM_TAB_W - 20, text_y, "x", bar.close_inactive);
} }
tab_x += TERM_TAB_W + TERM_TAB_GAP; tab_x += TERM_TAB_W + TERM_TAB_GAP;
@@ -300,17 +409,23 @@ static bool term_render_into(uint32_t* pixels, int width, int height) {
if (right.has_clock) { if (right.has_clock) {
int text_y = (TERM_TAB_BAR_H - fh) / 2; int text_y = (TERM_TAB_BAR_H - fh) / 2;
c.text(right.clock_x, text_y, g_clock_text, Color::from_hex(0x9A9A9A)); c.text(right.clock_x, text_y, g_clock_text, bar.clock);
} }
if (g_tabs.tab_count < TERM_MAX_TABS) { if (g_tabs.tab_count < TERM_MAX_TABS) {
int plus_h = 22; int plus_h = 22;
int py = 7; int py = 7;
int px = right.plus_x; int px = right.plus_x;
c.fill_rounded_rect(px, py, TERM_PLUS_W, plus_h, 5, Color::from_hex(0x262626)); c.fill_rounded_rect(px, py, TERM_PLUS_W, plus_h, 5, bar.tab_inactive_bg);
int pw_text = text_width(fonts::system_font, "+", fonts::UI_SIZE); int pw_text = text_width(fonts::system_font, "+", fonts::UI_SIZE);
c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+", c.text(px + (TERM_PLUS_W - pw_text) / 2, py + (plus_h - fh) / 2, "+",
Color::from_hex(0x6E6E6E)); bar.furniture);
}
if (right.has_cog) {
Rect cog = term_cog_rect(width);
c.fill_rounded_rect(cog.x, cog.y, cog.w, cog.h, 5, bar.tab_inactive_bg);
term_draw_cog(c, cog, bar.furniture, bar.tab_inactive_bg);
} }
uint32_t* term_pixels = pixels + TERM_TAB_BAR_H * width; uint32_t* term_pixels = pixels + TERM_TAB_BAR_H * width;
@@ -376,6 +491,11 @@ static void term_handle_mouse_core(int mx, int my, int scroll, bool left_click,
tab_x += TERM_TAB_W + TERM_TAB_GAP; tab_x += TERM_TAB_W + TERM_TAB_GAP;
} }
if (g_show_cog && term_cog_rect(width).contains(mx, my)) {
termset::open();
return;
}
int px = term_tabbar_right(width).plus_x; int px = term_tabbar_right(width).plus_x;
if (g_tabs.tab_count < TERM_MAX_TABS && if (g_tabs.tab_count < TERM_MAX_TABS &&
mx >= px && mx < px + TERM_PLUS_W) { mx >= px && mx < px + TERM_PLUS_W) {
@@ -421,25 +541,15 @@ static void term_handle_key_core(const montauk::abi::KeyEvent& key,
return; return;
} }
// Ctrl+Plus/Equal: zoom in // Ctrl+Plus/Equal and Ctrl+Minus zoom. Both go through the settings panel so
// the keyboard shortcut and the panel agree on bounds and both persist.
if (key.ctrl && key.pressed && (key.ascii == '+' || key.ascii == '=')) { if (key.ctrl && key.pressed && (key.ascii == '+' || key.ascii == '=')) {
if (fonts::TERM_SIZE < 64) { termset::zoom_font(1);
fonts::TERM_SIZE += 2;
for (int i = 0; i < g_tabs.tab_count; i++)
g_tabs.tabs[i]->dirty = true;
term_request_redraw();
}
return; return;
} }
// Ctrl+Minus: zoom out
if (key.ctrl && key.pressed && key.ascii == '-') { if (key.ctrl && key.pressed && key.ascii == '-') {
if (fonts::TERM_SIZE > 8) { termset::zoom_font(-1);
fonts::TERM_SIZE -= 2;
for (int i = 0; i < g_tabs.tab_count; i++)
g_tabs.tabs[i]->dirty = true;
term_request_redraw();
}
return; return;
} }
@@ -450,6 +560,26 @@ static void term_handle_key(const montauk::abi::KeyEvent& key) {
term_handle_key_core(key, g_win.width, g_win.height); term_handle_key_core(key, g_win.width, g_win.height);
} }
// ==== Settings panel plumbing ====
// Cells carry resolved colors, so a theme switch has to rewrite every tab's
// grid -- including scrollback -- from the outgoing palette to the new one.
static void term_on_theme_changed(const TermPalette& from, const TermPalette& to) {
for (int i = 0; i < g_tabs.tab_count; i++)
terminal_remap_palette(g_tabs.tabs[i], from, to);
term_request_redraw();
}
// A font size change alters the cell metrics; term_render_into reflows the
// grid to the new cell count on the next frame.
static void term_on_font_changed() {
for (int i = 0; i < g_tabs.tab_count; i++) {
terminal_invalidate_render_cache(g_tabs.tabs[i]);
g_tabs.tabs[i]->dirty = true;
}
term_request_redraw();
}
static void term_cleanup() { static void term_cleanup() {
for (int i = 0; i < g_tabs.tab_count; i++) for (int i = 0; i < g_tabs.tab_count; i++)
term_free_tab(g_tabs.tabs[i]); term_free_tab(g_tabs.tabs[i]);
@@ -459,6 +589,8 @@ static void term_cleanup() {
// ==== Windowed mode (Window Server client) ==== // ==== Windowed mode (Window Server client) ====
static void run_windowed() { static void run_windowed() {
g_show_cog = true;
if (!g_win.create("Terminal", INIT_W, INIT_H)) if (!g_win.create("Terminal", INIT_W, INIT_H))
montauk::exit(1); montauk::exit(1);
@@ -468,8 +600,8 @@ static void run_windowed() {
// and the first TrueType render. Paint the dark background and present // and the first TrueType render. Paint the dark background and present
// immediately so the compositor sees terminal colors from frame one. // immediately so the compositor sees terminal colors from frame one.
{ {
uint32_t bar_px = Color::from_hex(0x1C1C1C).to_pixel(); uint32_t bar_px = term_bar_colors().bar_bg.to_pixel();
uint32_t bg_px = colors::TERM_BG.to_pixel(); uint32_t bg_px = g_term_palette.bg.to_pixel();
int bar_pixels = TERM_TAB_BAR_H * g_win.width; int bar_pixels = TERM_TAB_BAR_H * g_win.width;
if (bar_pixels > g_win.width * g_win.height) if (bar_pixels > g_win.width * g_win.height)
bar_pixels = g_win.width * g_win.height; bar_pixels = g_win.width * g_win.height;
@@ -508,6 +640,8 @@ static void run_windowed() {
bool quit = false; bool quit = false;
int r = 0; int r = 0;
termset::poll();
while ((r = g_win.poll(&ev)) > 0) { while ((r = g_win.poll(&ev)) > 0) {
redraw = true; redraw = true;
@@ -544,6 +678,7 @@ static void run_windowed() {
montauk::sleep_ms(16); montauk::sleep_ms(16);
} }
termset::close();
term_cleanup(); term_cleanup();
g_win.destroy(); g_win.destroy();
} }
@@ -577,8 +712,8 @@ static void run_console() {
// Paint the dark background immediately so the first frame is not a flash // Paint the dark background immediately so the first frame is not a flash
// of uninitialized memory while the shell spawns and the first render runs. // of uninitialized memory while the shell spawns and the first render runs.
{ {
uint32_t bar_px = Color::from_hex(0x1C1C1C).to_pixel(); uint32_t bar_px = term_bar_colors().bar_bg.to_pixel();
uint32_t bg_px = colors::TERM_BG.to_pixel(); uint32_t bg_px = g_term_palette.bg.to_pixel();
int bar_pixels = TERM_TAB_BAR_H * sw; int bar_pixels = TERM_TAB_BAR_H * sw;
int total = sw * sh; int total = sw * sh;
if (bar_pixels > total) bar_pixels = total; if (bar_pixels > total) bar_pixels = total;
@@ -685,6 +820,13 @@ extern "C" void _start() {
if (!fonts::init()) if (!fonts::init())
montauk::exit(1); montauk::exit(1);
// Must run before the first cell grid is sized: the saved font size decides
// the mono cell metrics, and so how many columns and rows fit. Console mode
// has no settings window, but it shares the saved appearance and the zoom
// shortcut, so it needs the callbacks too.
termset::init({term_on_theme_changed, term_on_font_changed});
termset::load_prefs();
char args[256] = {}; char args[256] = {};
int arglen = montauk::getargs(args, sizeof(args)); int arglen = montauk::getargs(args, sizeof(args));
+526
View File
@@ -0,0 +1,526 @@
/*
* settings.cpp
* MontaukOS Terminal - settings panel
*
* Copyright (c) 2026 Daniel Hammer
*/
#include "settings.hpp"
#include <montauk/config.h>
#include <montauk/heap.h>
#include <montauk/string.h>
#include <montauk/syscall.h>
#include <gui/gui.hpp>
#include <gui/canvas.hpp>
#include <gui/mtk.hpp>
#include <gui/standalone.hpp>
#include <gui/truetype.hpp>
extern "C" {
#include <stdio.h>
}
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
},
{
"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 = 416;
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}};
// ==== 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");
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() {
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", kThemes[0].id));
int size = (int)doc.get_int("appearance.font_size", fonts::TERM_SIZE);
doc.destroy();
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
+46
View File
@@ -0,0 +1,46 @@
/*
* settings.hpp
* MontaukOS Terminal - settings panel
* Copyright (c) 2026 Daniel Hammer
*/
#pragma once
#include <gui/terminal.hpp>
namespace termset {
// The panel owns the appearance preferences; the terminal owns the tabs. These
// callbacks let the panel hand a change back so every tab can be repainted:
// theme_changed also carries the outgoing palette, which the terminal needs to
// translate the colors already baked into its cells.
struct Callbacks {
void (*theme_changed)(const gui::TermPalette& from, const gui::TermPalette& to);
void (*font_changed)();
};
// Load the saved theme and font size and apply them to the globals. Call
// before the first tab is created -- there is nothing to repaint yet, so this
// deliberately does not fire the callbacks.
void load_prefs();
void init(const Callbacks& cb);
bool is_open();
// Open (or focus) the settings window.
void open();
void close();
// Pump the settings window's events. No-op when the panel is closed.
void poll();
void render();
// Step the terminal font size by `direction` zoom steps (negative shrinks).
// Shared with the terminal's Ctrl+Plus / Ctrl+Minus shortcut so both routes
// obey the same bounds and both persist.
void zoom_font(int direction);
} // namespace termset