wraith/src-tauri/src/theme/mod.rs
Vantz Stockwell 633087633e test: full test coverage — 82 tests across all modules, zero warnings
Added tests for 3 uncovered modules:
- db: open, migrate, idempotent migration, FK/WAL pragmas, clone shares conn
- theme: seed_builtins (7 themes), idempotent seed, get_by_name, hex color
  validation, sort ordering, case sensitivity
- rdp/input: scancode lookup, extended key detection, value extraction,
  mouse flag composition, map coverage assertion

Existing test count: 52 (vault, connections, credentials, settings, host_keys)
New tests added: 30
Total: 82 tests, all passing, zero compiler warnings

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

465 lines
16 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)?,
})
}
// ── tests ─────────────────────────────────────────────────────────────────────
#[cfg(test)]
mod tests {
use super::*;
use crate::db::Database;
fn make_service() -> ThemeService {
let db = Database::open(std::path::Path::new(":memory:")).unwrap();
db.migrate().unwrap();
ThemeService::new(db)
}
#[test]
fn list_empty_before_seed() {
let svc = make_service();
assert!(svc.list().is_empty());
}
#[test]
fn seed_builtins_creates_seven_themes() {
let svc = make_service();
svc.seed_builtins();
let themes = svc.list();
assert_eq!(themes.len(), 7);
}
#[test]
fn seed_builtins_is_idempotent() {
let svc = make_service();
svc.seed_builtins();
svc.seed_builtins(); // second run must not duplicate
assert_eq!(svc.list().len(), 7);
}
#[test]
fn all_builtins_marked_as_builtin() {
let svc = make_service();
svc.seed_builtins();
for theme in svc.list() {
assert!(theme.is_builtin, "{} should be marked as builtin", theme.name);
}
}
#[test]
fn builtin_names_correct() {
let svc = make_service();
svc.seed_builtins();
let names: Vec<String> = svc.list().into_iter().map(|t| t.name).collect();
assert!(names.contains(&"Dracula".to_string()));
assert!(names.contains(&"Nord".to_string()));
assert!(names.contains(&"Monokai".to_string()));
assert!(names.contains(&"One Dark".to_string()));
assert!(names.contains(&"Solarized Dark".to_string()));
assert!(names.contains(&"Gruvbox Dark".to_string()));
assert!(names.contains(&"MobaXTerm Classic".to_string()));
}
#[test]
fn get_by_name_returns_theme() {
let svc = make_service();
svc.seed_builtins();
let theme = svc.get_by_name("Dracula");
assert!(theme.is_some());
let t = theme.unwrap();
assert_eq!(t.name, "Dracula");
assert_eq!(t.background, "#282a36");
assert_eq!(t.foreground, "#f8f8f2");
}
#[test]
fn get_by_name_missing_returns_none() {
let svc = make_service();
svc.seed_builtins();
assert!(svc.get_by_name("Nonexistent Theme").is_none());
}
#[test]
fn get_by_name_is_case_sensitive() {
let svc = make_service();
svc.seed_builtins();
assert!(svc.get_by_name("dracula").is_none()); // lowercase should not match
}
#[test]
fn all_themes_have_valid_hex_colors() {
let svc = make_service();
svc.seed_builtins();
for theme in svc.list() {
let colors = [
&theme.foreground, &theme.background, &theme.cursor,
&theme.black, &theme.red, &theme.green, &theme.yellow,
&theme.blue, &theme.magenta, &theme.cyan, &theme.white,
&theme.bright_black, &theme.bright_red, &theme.bright_green,
&theme.bright_yellow, &theme.bright_blue, &theme.bright_magenta,
&theme.bright_cyan, &theme.bright_white,
];
for color in colors {
assert!(
color.starts_with('#') && color.len() == 7,
"Theme '{}' has invalid color: '{}'",
theme.name,
color
);
}
}
}
#[test]
fn list_ordered_builtin_first_then_name() {
let svc = make_service();
svc.seed_builtins();
let themes = svc.list();
// All are builtin, so should be ordered by name (case-insensitive)
for w in themes.windows(2) {
assert!(
w[0].name.to_lowercase() <= w[1].name.to_lowercase(),
"'{}' should come before '{}'",
w[0].name,
w[1].name
);
}
}
}