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>
340 lines
12 KiB
Rust
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)?,
|
|
})
|
|
}
|