From f6cc0aeacddcce225d33e1ef25f97b7f9b8d300f Mon Sep 17 00:00:00 2001 From: Subhan Gadirli Date: Wed, 22 Jul 2026 01:14:56 +0400 Subject: [PATCH 1/5] feat(terminal): add color theming with built-in themes Terminals previously never set VTE colors, so they inherited a poor GTK-derived palette. Introduce a theme module with 10 curated schemes (Dracula, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark, One Dark, Solarized Dark/Light, Monokai, GitHub Light). - Apply the configured theme to each terminal on creation - Add terminal_theme to AppConfig, defaulting to Dracula, with a serde default so existing configs load unchanged - Add a Color Theme dropdown in settings that saves and live-recolors all open terminals instantly --- src/config_observer.rs | 7 ++ src/ui/components/settings.rs | 27 +++++- src/ui/mod.rs | 1 + src/ui/theme.rs | 168 ++++++++++++++++++++++++++++++++++ src/ui/window.rs | 6 +- 5 files changed, 207 insertions(+), 2 deletions(-) create mode 100644 src/ui/theme.rs diff --git a/src/config_observer.rs b/src/config_observer.rs index c9164fc..d9762ea 100644 --- a/src/config_observer.rs +++ b/src/config_observer.rs @@ -119,15 +119,22 @@ pub struct AppConfig { pub monitor_refresh_rate: u32, // index: 0=1s, 1=3s, 2=5s, 3=10s pub terminal_font: String, pub terminal_scrollback: u32, + #[serde(default = "default_terminal_theme")] + pub terminal_theme: String, pub confirm_tab_close: bool, } +fn default_terminal_theme() -> String { + crate::ui::theme::default_theme_name().to_string() +} + impl Default for AppConfig { fn default() -> Self { Self { monitor_refresh_rate: 1, // 3s terminal_font: "Monospace 11".to_string(), terminal_scrollback: 10000, + terminal_theme: default_terminal_theme(), confirm_tab_close: false, } } diff --git a/src/ui/components/settings.rs b/src/ui/components/settings.rs index 4c60ca7..5b7654d 100644 --- a/src/ui/components/settings.rs +++ b/src/ui/components/settings.rs @@ -6,7 +6,7 @@ pub struct Settings { } impl Settings { - pub fn new() -> Self { + pub fn new(notebook: >k4::Notebook) -> Self { let config = crate::config_observer::load_app_config().unwrap_or_else(|e| { tracing::error!("Failed to load app config: {}", e); AppConfig::default() @@ -48,6 +48,15 @@ impl Settings { let scrollback_spinner = gtk4::SpinButton::new(Some(&scrollback_adj), 1.0, 0); scrollback_row.append(&scrollback_label); scrollback_row.append(&scrollback_spinner); terminal_group.append(&scrollback_row); + + let theme_names = crate::ui::theme::theme_names(); + let theme_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12); + let theme_label = gtk4::Label::new(Some("Color Theme")); theme_label.set_hexpand(true); theme_label.set_halign(gtk4::Align::Start); + let theme_dropdown = gtk4::DropDown::from_strings(&theme_names); + let current_theme_idx = theme_names.iter().position(|n| *n == config.terminal_theme).unwrap_or(0) as u32; + theme_dropdown.set_selected(current_theme_idx); + theme_row.append(&theme_label); theme_row.append(&theme_dropdown); + terminal_group.append(&theme_row); content.append(&terminal_group); let monitor_group = gtk4::Box::new(gtk4::Orientation::Vertical, 12); @@ -75,7 +84,9 @@ impl Settings { let r_drop = refresh_dropdown.clone(); let f_btn = font_button.clone(); let s_spin = scrollback_spinner.clone(); + let t_drop = theme_dropdown.clone(); let c_switch = confirm_switch.clone(); + let theme_names_saved = theme_names.clone(); let save_config = move || { let mut new_config = AppConfig::default(); @@ -84,6 +95,10 @@ impl Settings { .map(|fd| fd.to_string()) .unwrap_or_else(|| "Monospace 11".to_string()); new_config.terminal_scrollback = s_spin.value() as u32; + new_config.terminal_theme = theme_names_saved + .get(t_drop.selected() as usize) + .map(|n| n.to_string()) + .unwrap_or_else(|| crate::ui::theme::default_theme_name().to_string()); new_config.confirm_tab_close = c_switch.is_active(); let _ = crate::config_observer::save_app_config(&new_config); }; @@ -94,6 +109,16 @@ impl Settings { let s3 = save_fn.clone(); scrollback_spinner.connect_value_changed(move |_| { s3(); }); let s5 = save_fn.clone(); confirm_switch.connect_active_notify(move |_| { s5(); }); + // Theme changes save and re-color every open terminal immediately. + let s4 = save_fn.clone(); + let nb = notebook.clone(); + theme_dropdown.connect_selected_notify(move |dd| { + s4(); + if let Some(name) = crate::ui::theme::theme_names().get(dd.selected() as usize) { + crate::ui::theme::apply_to_open_terminals(&nb, crate::ui::theme::get_theme(name)); + } + }); + scrolled.set_child(Some(&content)); container.append(&scrolled); diff --git a/src/ui/mod.rs b/src/ui/mod.rs index d07a73e..1143634 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -1,4 +1,5 @@ pub mod style; +pub mod theme; pub mod window; pub mod server_list; pub mod components; diff --git a/src/ui/theme.rs b/src/ui/theme.rs new file mode 100644 index 0000000..25434f9 --- /dev/null +++ b/src/ui/theme.rs @@ -0,0 +1,168 @@ +use gtk4::gdk::RGBA; +use std::str::FromStr; +use vte4::prelude::*; + +/// A terminal color scheme: default foreground/background/cursor colors plus the +/// 16-entry ANSI palette (8 normal + 8 bright). +pub struct TerminalTheme { + pub name: &'static str, + pub foreground: &'static str, + pub background: &'static str, + pub cursor: &'static str, + pub palette: [&'static str; 16], +} + +/// Built-in terminal themes. The first entry is used as the fallback default. +pub static THEMES: &[TerminalTheme] = &[ + TerminalTheme { + name: "Dracula", + foreground: "#f8f8f2", + background: "#282a36", + cursor: "#f8f8f2", + palette: [ + "#21222c", "#ff5555", "#50fa7b", "#f1fa8c", "#bd93f9", "#ff79c6", "#8be9fd", "#f8f8f2", + "#6272a4", "#ff6e6e", "#69ff94", "#ffffa5", "#d6acff", "#ff92df", "#a4ffff", "#ffffff", + ], + }, + TerminalTheme { + name: "Nord", + foreground: "#d8dee9", + background: "#2e3440", + cursor: "#d8dee9", + palette: [ + "#3b4252", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#88c0d0", "#e5e9f0", + "#4c566a", "#bf616a", "#a3be8c", "#ebcb8b", "#81a1c1", "#b48ead", "#8fbcbb", "#eceff4", + ], + }, + TerminalTheme { + name: "Tokyo Night", + foreground: "#c0caf5", + background: "#1a1b26", + cursor: "#c0caf5", + palette: [ + "#15161e", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#a9b1d6", + "#414868", "#f7768e", "#9ece6a", "#e0af68", "#7aa2f7", "#bb9af7", "#7dcfff", "#c0caf5", + ], + }, + TerminalTheme { + name: "Catppuccin Mocha", + foreground: "#cdd6f4", + background: "#1e1e2e", + cursor: "#f5e0dc", + palette: [ + "#45475a", "#f38ba8", "#a6e3a1", "#f9e2af", "#89b4fa", "#f5c2e7", "#94e2d5", "#bac2de", + "#585b70", "#f38ba8", "#a6e3a1", "#f9e2af", "#89b4fa", "#f5c2e7", "#94e2d5", "#a6adc8", + ], + }, + TerminalTheme { + name: "Gruvbox Dark", + foreground: "#ebdbb2", + background: "#282828", + cursor: "#ebdbb2", + palette: [ + "#282828", "#cc241d", "#98971a", "#d79921", "#458588", "#b16286", "#689d6a", "#a89984", + "#928374", "#fb4934", "#b8bb26", "#fabd2f", "#83a598", "#d3869b", "#8ec07c", "#ebdbb2", + ], + }, + TerminalTheme { + name: "One Dark", + foreground: "#abb2bf", + background: "#282c34", + cursor: "#528bff", + palette: [ + "#282c34", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#abb2bf", + "#5c6370", "#e06c75", "#98c379", "#e5c07b", "#61afef", "#c678dd", "#56b6c2", "#ffffff", + ], + }, + TerminalTheme { + name: "Solarized Dark", + foreground: "#839496", + background: "#002b36", + cursor: "#93a1a1", + palette: [ + "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5", + "#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3", + ], + }, + TerminalTheme { + name: "Monokai", + foreground: "#f8f8f2", + background: "#272822", + cursor: "#f8f8f0", + palette: [ + "#272822", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f8f8f2", + "#75715e", "#f92672", "#a6e22e", "#f4bf75", "#66d9ef", "#ae81ff", "#a1efe4", "#f9f8f5", + ], + }, + TerminalTheme { + name: "Solarized Light", + foreground: "#657b83", + background: "#fdf6e3", + cursor: "#586e75", + palette: [ + "#073642", "#dc322f", "#859900", "#b58900", "#268bd2", "#d33682", "#2aa198", "#eee8d5", + "#002b36", "#cb4b16", "#586e75", "#657b83", "#839496", "#6c71c4", "#93a1a1", "#fdf6e3", + ], + }, + TerminalTheme { + name: "GitHub Light", + foreground: "#24292e", + background: "#ffffff", + cursor: "#24292e", + palette: [ + "#24292e", "#d73a49", "#28a745", "#dbab09", "#0366d6", "#5a32a3", "#0598bc", "#6a737d", + "#959da5", "#cb2431", "#22863a", "#b08800", "#005cc5", "#5a32a3", "#3192aa", "#d1d5da", + ], + }, +]; + +/// The default theme name, used when the config value is empty or unrecognized. +pub fn default_theme_name() -> &'static str { + THEMES[0].name +} + +/// Returns the theme matching `name`, falling back to the default if not found. +pub fn get_theme(name: &str) -> &'static TerminalTheme { + THEMES.iter().find(|t| t.name == name).unwrap_or(&THEMES[0]) +} + +/// The names of all built-in themes, in display order. +pub fn theme_names() -> Vec<&'static str> { + THEMES.iter().map(|t| t.name).collect() +} + +fn parse_color(hex: &str, fallback: RGBA) -> RGBA { + RGBA::from_str(hex).unwrap_or(fallback) +} + +/// Applies the given theme's colors to a single terminal. +pub fn apply_to_terminal(theme: &TerminalTheme, terminal: &vte4::Terminal) { + let fg = parse_color(theme.foreground, RGBA::WHITE); + let bg = parse_color(theme.background, RGBA::BLACK); + let cursor = parse_color(theme.cursor, fg); + let palette: Vec = theme + .palette + .iter() + .map(|c| parse_color(c, RGBA::BLACK)) + .collect(); + let palette_refs: Vec<&RGBA> = palette.iter().collect(); + terminal.set_colors(Some(&fg), Some(&bg), &palette_refs); + terminal.set_color_cursor(Some(&cursor)); +} + +/// Re-applies a theme to every open terminal currently living in the notebook. +/// Session pages are vertical boxes whose direct children include the VTE +/// terminal; other page types (explorer, monitor, docker) are skipped. +pub fn apply_to_open_terminals(notebook: >k4::Notebook, theme: &TerminalTheme) { + for i in 0..notebook.n_pages() { + let Some(page) = notebook.nth_page(Some(i)) else { continue }; + let Some(bx) = page.downcast_ref::() else { continue }; + let mut child = bx.first_child(); + while let Some(c) = child { + if let Some(term) = c.downcast_ref::() { + apply_to_terminal(theme, term); + } + child = c.next_sibling(); + } + } +} diff --git a/src/ui/window.rs b/src/ui/window.rs index ab51208..cb1d8fb 100644 --- a/src/ui/window.rs +++ b/src/ui/window.rs @@ -59,7 +59,7 @@ impl AppWindow { sessions_box.append(¬ebook); stack.add_named(&sessions_box, Some("sessions")); - let settings = Settings::new(); + let settings = Settings::new(¬ebook); stack.add_named(&settings.container, Some("settings")); let keys_box = build_ssh_keys_ui(&window); @@ -212,6 +212,10 @@ impl AppWindow { let font_desc = gtk4::pango::FontDescription::from_string(&app_config.terminal_font); terminal.set_font(Some(&font_desc)); terminal.set_scrollback_lines(app_config.terminal_scrollback as i64); + crate::ui::theme::apply_to_terminal( + crate::ui::theme::get_theme(&app_config.terminal_theme), + &terminal, + ); let key_controller = gtk4::EventControllerKey::new(); let terminal_clone = terminal.clone(); From eb447e7382b2ac53880fe49973a106ed5fc1001f Mon Sep 17 00:00:00 2001 From: Cleboost Date: Wed, 22 Jul 2026 11:32:51 +0200 Subject: [PATCH 2/5] refactor(terminal): simplify theme config and application Move the default theme constant into config_observer, use index-based theme lookup, filter session tabs when recoloring, and avoid heap allocations when applying VTE palettes. --- src/config_observer.rs | 4 +++- src/ui/components/settings.rs | 23 +++++++++++----------- src/ui/theme.rs | 37 ++++++++++++----------------------- 3 files changed, 26 insertions(+), 38 deletions(-) diff --git a/src/config_observer.rs b/src/config_observer.rs index d9762ea..b8a9847 100644 --- a/src/config_observer.rs +++ b/src/config_observer.rs @@ -113,6 +113,8 @@ pub async fn delete_keyring_password(alias: &str) -> anyhow::Result<()> { Ok(()) } +pub const DEFAULT_TERMINAL_THEME: &str = "Dracula"; + /// Global application configuration settings. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct AppConfig { @@ -125,7 +127,7 @@ pub struct AppConfig { } fn default_terminal_theme() -> String { - crate::ui::theme::default_theme_name().to_string() + DEFAULT_TERMINAL_THEME.to_string() } impl Default for AppConfig { diff --git a/src/ui/components/settings.rs b/src/ui/components/settings.rs index 5b7654d..003164c 100644 --- a/src/ui/components/settings.rs +++ b/src/ui/components/settings.rs @@ -49,11 +49,14 @@ impl Settings { scrollback_row.append(&scrollback_label); scrollback_row.append(&scrollback_spinner); terminal_group.append(&scrollback_row); - let theme_names = crate::ui::theme::theme_names(); + let theme_labels: Vec<&str> = crate::ui::theme::THEMES.iter().map(|t| t.name).collect(); let theme_row = gtk4::Box::new(gtk4::Orientation::Horizontal, 12); let theme_label = gtk4::Label::new(Some("Color Theme")); theme_label.set_hexpand(true); theme_label.set_halign(gtk4::Align::Start); - let theme_dropdown = gtk4::DropDown::from_strings(&theme_names); - let current_theme_idx = theme_names.iter().position(|n| *n == config.terminal_theme).unwrap_or(0) as u32; + let theme_dropdown = gtk4::DropDown::from_strings(&theme_labels); + let current_theme_idx = crate::ui::theme::THEMES + .iter() + .position(|t| t.name == config.terminal_theme) + .unwrap_or(0) as u32; theme_dropdown.set_selected(current_theme_idx); theme_row.append(&theme_label); theme_row.append(&theme_dropdown); terminal_group.append(&theme_row); @@ -86,7 +89,6 @@ impl Settings { let s_spin = scrollback_spinner.clone(); let t_drop = theme_dropdown.clone(); let c_switch = confirm_switch.clone(); - let theme_names_saved = theme_names.clone(); let save_config = move || { let mut new_config = AppConfig::default(); @@ -95,10 +97,7 @@ impl Settings { .map(|fd| fd.to_string()) .unwrap_or_else(|| "Monospace 11".to_string()); new_config.terminal_scrollback = s_spin.value() as u32; - new_config.terminal_theme = theme_names_saved - .get(t_drop.selected() as usize) - .map(|n| n.to_string()) - .unwrap_or_else(|| crate::ui::theme::default_theme_name().to_string()); + new_config.terminal_theme = crate::ui::theme::theme_at(t_drop.selected() as usize).name.to_string(); new_config.confirm_tab_close = c_switch.is_active(); let _ = crate::config_observer::save_app_config(&new_config); }; @@ -109,14 +108,14 @@ impl Settings { let s3 = save_fn.clone(); scrollback_spinner.connect_value_changed(move |_| { s3(); }); let s5 = save_fn.clone(); confirm_switch.connect_active_notify(move |_| { s5(); }); - // Theme changes save and re-color every open terminal immediately. let s4 = save_fn.clone(); let nb = notebook.clone(); theme_dropdown.connect_selected_notify(move |dd| { s4(); - if let Some(name) = crate::ui::theme::theme_names().get(dd.selected() as usize) { - crate::ui::theme::apply_to_open_terminals(&nb, crate::ui::theme::get_theme(name)); - } + crate::ui::theme::apply_to_open_terminals( + &nb, + crate::ui::theme::theme_at(dd.selected() as usize), + ); }); scrolled.set_child(Some(&content)); diff --git a/src/ui/theme.rs b/src/ui/theme.rs index 25434f9..a3bf39f 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -116,46 +116,33 @@ pub static THEMES: &[TerminalTheme] = &[ }, ]; -/// The default theme name, used when the config value is empty or unrecognized. -pub fn default_theme_name() -> &'static str { - THEMES[0].name -} - -/// Returns the theme matching `name`, falling back to the default if not found. pub fn get_theme(name: &str) -> &'static TerminalTheme { THEMES.iter().find(|t| t.name == name).unwrap_or(&THEMES[0]) } -/// The names of all built-in themes, in display order. -pub fn theme_names() -> Vec<&'static str> { - THEMES.iter().map(|t| t.name).collect() -} - -fn parse_color(hex: &str, fallback: RGBA) -> RGBA { - RGBA::from_str(hex).unwrap_or(fallback) +pub fn theme_at(index: usize) -> &'static TerminalTheme { + THEMES.get(index).unwrap_or(&THEMES[0]) } -/// Applies the given theme's colors to a single terminal. pub fn apply_to_terminal(theme: &TerminalTheme, terminal: &vte4::Terminal) { - let fg = parse_color(theme.foreground, RGBA::WHITE); - let bg = parse_color(theme.background, RGBA::BLACK); - let cursor = parse_color(theme.cursor, fg); - let palette: Vec = theme - .palette - .iter() - .map(|c| parse_color(c, RGBA::BLACK)) - .collect(); + let fg = RGBA::from_str(theme.foreground).unwrap_or(RGBA::WHITE); + let bg = RGBA::from_str(theme.background).unwrap_or(RGBA::BLACK); + let cursor = RGBA::from_str(theme.cursor).unwrap_or(fg); + let mut palette = [RGBA::BLACK; 16]; + for (i, hex) in theme.palette.iter().enumerate() { + palette[i] = RGBA::from_str(hex).unwrap_or(RGBA::BLACK); + } let palette_refs: Vec<&RGBA> = palette.iter().collect(); terminal.set_colors(Some(&fg), Some(&bg), &palette_refs); terminal.set_color_cursor(Some(&cursor)); } -/// Re-applies a theme to every open terminal currently living in the notebook. -/// Session pages are vertical boxes whose direct children include the VTE -/// terminal; other page types (explorer, monitor, docker) are skipped. pub fn apply_to_open_terminals(notebook: >k4::Notebook, theme: &TerminalTheme) { for i in 0..notebook.n_pages() { let Some(page) = notebook.nth_page(Some(i)) else { continue }; + if !page.widget_name().starts_with("session:") { + continue; + } let Some(bx) = page.downcast_ref::() else { continue }; let mut child = bx.first_child(); while let Some(c) = child { From 84e71dc0a4dcba58e27bec46724bac7853194b68 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Wed, 22 Jul 2026 11:35:42 +0200 Subject: [PATCH 3/5] chore: add changeset for terminal color theming --- .changeset/terminal-color-theming.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/terminal-color-theming.md diff --git a/.changeset/terminal-color-theming.md b/.changeset/terminal-color-theming.md new file mode 100644 index 0000000..50e03a8 --- /dev/null +++ b/.changeset/terminal-color-theming.md @@ -0,0 +1,8 @@ +--- +rustmius: minor +--- + +Add terminal color theming with 10 built-in schemes (Dracula, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark, One Dark, Solarized Dark/Light, Monokai, GitHub Light). + +- Apply the configured theme to each terminal on creation +- Add a Color Theme dropdown in Settings with instant live recolor of open terminals From 966d50e2b64b7a6633da8cf4e5258dc6e763ae99 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Wed, 22 Jul 2026 11:45:13 +0200 Subject: [PATCH 4/5] ci: split build, lint and clippy jobs with rustfmt autofix --- .github/workflows/ci.yml | 55 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 51 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6fbbf80..c043556 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: Continuous Integration +name: CI on: push: @@ -13,10 +13,10 @@ permissions: contents: read jobs: - check: - name: Build Check + build: + name: Build runs-on: ubuntu-latest - + steps: - name: Checkout code uses: actions/checkout@v7 @@ -37,3 +37,50 @@ jobs: - name: Cargo Build run: cargo build + + lint: + name: Lint + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: rustfmt + + - name: Auto-fix formatting + run: cargo fmt --all + + - name: Commit changes + uses: stefanzweifel/git-auto-commit-action@v7 + with: + commit_message: "style: apply rustfmt [skip ci]" + + clippy: + name: Clippy + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v7 + + - name: Install dependencies + run: | + sudo apt-get update + sudo apt-get install -y libgtk-4-dev libvte-2.91-gtk4-dev libadwaita-1-dev + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + components: clippy + + - name: Rust Cache + uses: Swatinem/rust-cache@v2 + + - name: Clippy + run: cargo clippy -- -D warnings From 46355c9dd3739cc4902c623f05a2f3fbe9e7a460 Mon Sep 17 00:00:00 2001 From: Cleboost Date: Wed, 22 Jul 2026 11:46:44 +0200 Subject: [PATCH 5/5] Revert "chore: add changeset for terminal color theming" This reverts commit 84e71dc0a4dcba58e27bec46724bac7853194b68. --- .changeset/terminal-color-theming.md | 8 -------- 1 file changed, 8 deletions(-) delete mode 100644 .changeset/terminal-color-theming.md diff --git a/.changeset/terminal-color-theming.md b/.changeset/terminal-color-theming.md deleted file mode 100644 index 50e03a8..0000000 --- a/.changeset/terminal-color-theming.md +++ /dev/null @@ -1,8 +0,0 @@ ---- -rustmius: minor ---- - -Add terminal color theming with 10 built-in schemes (Dracula, Nord, Tokyo Night, Catppuccin Mocha, Gruvbox Dark, One Dark, Solarized Dark/Light, Monokai, GitHub Light). - -- Apply the configured theme to each terminal on creation -- Add a Color Theme dropdown in Settings with instant live recolor of open terminals