wraith/src-tauri/src/theme/mod.rs
Vantz Stockwell 0cd4cc0f64 feat: Phase 5 complete — themes, editor, shortcuts, workspace, settings
Theme service: 7 built-in themes seeded from Rust, ThemePicker
loads from backend. Workspace service: save/load snapshots, crash
recovery detection. SettingsModal: full port with Tauri invoke.
CommandPalette, HostKeyDialog, ConnectionEditDialog all ported.

CodeMirror editor: inline above terminal, reads/writes via SFTP.
Full keyboard shortcuts: Ctrl+K/W/Tab/Shift+Tab/1-9/B/F.
Terminal search bar via Ctrl+F (SearchAddon).
Tab badges: protocol dots, ROOT warning, environment pills.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-17 16:36:06 -04:00

340 lines
12 KiB
Rust

use rusqlite::params;
use serde::{Deserialize, Serialize};
use crate::db::Database;
// ── domain types ─────────────────────────────────────────────────────────────
#[derive(Debug, Serialize, Deserialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct Theme {
pub id: i64,
pub name: String,
pub foreground: String,
pub background: String,
pub cursor: String,
pub black: String,
pub red: String,
pub green: String,
pub yellow: String,
pub blue: String,
pub magenta: String,
pub cyan: String,
pub white: String,
pub bright_black: String,
pub bright_red: String,
pub bright_green: String,
pub bright_yellow: String,
pub bright_blue: String,
pub bright_magenta: String,
pub bright_cyan: String,
pub bright_white: String,
pub is_builtin: bool,
}
// ── helper struct for seed data ───────────────────────────────────────────────
struct BuiltinTheme {
name: &'static str,
foreground: &'static str,
background: &'static str,
cursor: &'static str,
black: &'static str,
red: &'static str,
green: &'static str,
yellow: &'static str,
blue: &'static str,
magenta: &'static str,
cyan: &'static str,
white: &'static str,
bright_black: &'static str,
bright_red: &'static str,
bright_green: &'static str,
bright_yellow: &'static str,
bright_blue: &'static str,
bright_magenta: &'static str,
bright_cyan: &'static str,
bright_white: &'static str,
}
// ── service ───────────────────────────────────────────────────────────────────
pub struct ThemeService {
db: Database,
}
impl ThemeService {
pub fn new(db: Database) -> Self {
Self { db }
}
/// Insert the 7 built-in themes if they don't exist yet.
///
/// Uses INSERT OR IGNORE so re-running on an existing database is safe.
pub fn seed_builtins(&self) {
let themes: &[BuiltinTheme] = &[
BuiltinTheme {
name: "Dracula",
foreground: "#f8f8f2",
background: "#282a36",
cursor: "#f8f8f2",
black: "#21222c",
red: "#ff5555",
green: "#50fa7b",
yellow: "#f1fa8c",
blue: "#bd93f9",
magenta: "#ff79c6",
cyan: "#8be9fd",
white: "#f8f8f2",
bright_black: "#6272a4",
bright_red: "#ff6e6e",
bright_green: "#69ff94",
bright_yellow: "#ffffa5",
bright_blue: "#d6acff",
bright_magenta: "#ff92df",
bright_cyan: "#a4ffff",
bright_white: "#ffffff",
},
BuiltinTheme {
name: "Nord",
foreground: "#d8dee9",
background: "#2e3440",
cursor: "#d8dee9",
black: "#3b4252",
red: "#bf616a",
green: "#a3be8c",
yellow: "#ebcb8b",
blue: "#81a1c1",
magenta: "#b48ead",
cyan: "#88c0d0",
white: "#e5e9f0",
bright_black: "#4c566a",
bright_red: "#bf616a",
bright_green: "#a3be8c",
bright_yellow: "#ebcb8b",
bright_blue: "#81a1c1",
bright_magenta: "#b48ead",
bright_cyan: "#8fbcbb",
bright_white: "#eceff4",
},
BuiltinTheme {
name: "Monokai",
foreground: "#f8f8f2",
background: "#272822",
cursor: "#f8f8f0",
black: "#272822",
red: "#f92672",
green: "#a6e22e",
yellow: "#f4bf75",
blue: "#66d9ef",
magenta: "#ae81ff",
cyan: "#a1efe4",
white: "#f8f8f2",
bright_black: "#75715e",
bright_red: "#f92672",
bright_green: "#a6e22e",
bright_yellow: "#f4bf75",
bright_blue: "#66d9ef",
bright_magenta: "#ae81ff",
bright_cyan: "#a1efe4",
bright_white: "#f9f8f5",
},
BuiltinTheme {
name: "One Dark",
foreground: "#abb2bf",
background: "#282c34",
cursor: "#528bff",
black: "#282c34",
red: "#e06c75",
green: "#98c379",
yellow: "#e5c07b",
blue: "#61afef",
magenta: "#c678dd",
cyan: "#56b6c2",
white: "#abb2bf",
bright_black: "#545862",
bright_red: "#e06c75",
bright_green: "#98c379",
bright_yellow: "#e5c07b",
bright_blue: "#61afef",
bright_magenta: "#c678dd",
bright_cyan: "#56b6c2",
bright_white: "#c8ccd4",
},
BuiltinTheme {
name: "Solarized Dark",
foreground: "#839496",
background: "#002b36",
cursor: "#839496",
black: "#073642",
red: "#dc322f",
green: "#859900",
yellow: "#b58900",
blue: "#268bd2",
magenta: "#d33682",
cyan: "#2aa198",
white: "#eee8d5",
bright_black: "#002b36",
bright_red: "#cb4b16",
bright_green: "#586e75",
bright_yellow: "#657b83",
bright_blue: "#839496",
bright_magenta: "#6c71c4",
bright_cyan: "#93a1a1",
bright_white: "#fdf6e3",
},
BuiltinTheme {
name: "Gruvbox Dark",
foreground: "#ebdbb2",
background: "#282828",
cursor: "#ebdbb2",
black: "#282828",
red: "#cc241d",
green: "#98971a",
yellow: "#d79921",
blue: "#458588",
magenta: "#b16286",
cyan: "#689d6a",
white: "#a89984",
bright_black: "#928374",
bright_red: "#fb4934",
bright_green: "#b8bb26",
bright_yellow: "#fabd2f",
bright_blue: "#83a598",
bright_magenta: "#d3869b",
bright_cyan: "#8ec07c",
bright_white: "#ebdbb2",
},
BuiltinTheme {
name: "MobaXTerm Classic",
foreground: "#ececec",
background: "#242424",
cursor: "#b4b4c0",
black: "#000000",
red: "#aa4244",
green: "#7e8d53",
yellow: "#e4b46d",
blue: "#6e9aba",
magenta: "#9e5085",
cyan: "#80d5cf",
white: "#cccccc",
bright_black: "#808080",
bright_red: "#cc7b7d",
bright_green: "#a5b17c",
bright_yellow: "#ecc995",
bright_blue: "#96b6cd",
bright_magenta: "#c083ac",
bright_cyan: "#a9e2de",
bright_white: "#cccccc",
},
];
let conn = self.db.conn();
for t in themes {
if let Err(e) = conn.execute(
"INSERT OR IGNORE INTO themes (
name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow,
bright_blue, bright_magenta, bright_cyan, bright_white,
is_builtin
) VALUES (
?1, ?2, ?3, ?4,
?5, ?6, ?7, ?8, ?9, ?10, ?11, ?12,
?13, ?14, ?15, ?16,
?17, ?18, ?19, ?20,
1
)",
params![
t.name, t.foreground, t.background, t.cursor,
t.black, t.red, t.green, t.yellow,
t.blue, t.magenta, t.cyan, t.white,
t.bright_black, t.bright_red, t.bright_green, t.bright_yellow,
t.bright_blue, t.bright_magenta, t.bright_cyan, t.bright_white,
],
) {
eprintln!("theme::seed_builtins: failed to seed '{}': {}", t.name, e);
}
}
}
/// Return all themes ordered by built-in first, then name.
pub fn list(&self) -> Vec<Theme> {
let conn = self.db.conn();
let mut stmt = match conn.prepare(
"SELECT id, name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow,
bright_blue, bright_magenta, bright_cyan, bright_white,
is_builtin
FROM themes
ORDER BY is_builtin DESC, name COLLATE NOCASE ASC",
) {
Ok(s) => s,
Err(e) => {
eprintln!("theme::list: failed to prepare query: {}", e);
return vec![];
}
};
match stmt.query_map([], map_theme_row) {
Ok(rows) => rows
.filter_map(|r| {
r.map_err(|e| eprintln!("theme::list: row error: {}", e))
.ok()
})
.collect(),
Err(e) => {
eprintln!("theme::list: query failed: {}", e);
vec![]
}
}
}
/// Look up a theme by exact name (case-sensitive).
pub fn get_by_name(&self, name: &str) -> Option<Theme> {
let conn = self.db.conn();
conn.query_row(
"SELECT id, name, foreground, background, cursor,
black, red, green, yellow, blue, magenta, cyan, white,
bright_black, bright_red, bright_green, bright_yellow,
bright_blue, bright_magenta, bright_cyan, bright_white,
is_builtin
FROM themes
WHERE name = ?1",
params![name],
map_theme_row,
)
.ok()
}
}
// ── private helpers ───────────────────────────────────────────────────────────
fn map_theme_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<Theme> {
Ok(Theme {
id: row.get(0)?,
name: row.get(1)?,
foreground: row.get(2)?,
background: row.get(3)?,
cursor: row.get(4)?,
black: row.get(5)?,
red: row.get(6)?,
green: row.get(7)?,
yellow: row.get(8)?,
blue: row.get(9)?,
magenta: row.get(10)?,
cyan: row.get(11)?,
white: row.get(12)?,
bright_black: row.get(13)?,
bright_red: row.get(14)?,
bright_green: row.get(15)?,
bright_yellow: row.get(16)?,
bright_blue: row.get(17)?,
bright_magenta: row.get(18)?,
bright_cyan: row.get(19)?,
bright_white: row.get(20)?,
is_builtin: row.get(21)?,
})
}