pacsea/theme/store.rs
1//! Theme store with live-reload capability.
2//!
3//! This module provides the global theme cache and functions to access and reload
4//! the theme. It uses the unified resolution logic from `resolve.rs` to determine
5//! which theme source to use.
6
7use std::fs;
8use std::sync::{OnceLock, RwLock};
9
10use super::config::THEME_SKELETON_CONTENT;
11use super::paths::config_dir;
12use super::resolve::{ThemeSource, resolve_theme};
13use super::types::Theme;
14
15/// Global theme store with live-reload capability.
16static THEME_STORE: OnceLock<RwLock<Theme>> = OnceLock::new();
17
18/// What: Load theme using the unified resolution logic.
19///
20/// Inputs:
21/// - None.
22///
23/// Output:
24/// - Returns a fully-populated `Theme`.
25///
26/// Details:
27/// - Uses `resolve_theme()` to determine the best theme source.
28/// - When using codebase default and no theme.conf exists, writes the skeleton.
29/// - Never exits the process; always returns a valid theme.
30fn load_initial_theme() -> Theme {
31 let resolved = resolve_theme();
32
33 // If we're using the default theme and no theme.conf exists, create one
34 // so the user has a file to edit
35 if resolved.source == ThemeSource::Default {
36 ensure_theme_file_exists();
37 }
38
39 resolved.theme
40}
41
42/// What: Ensures theme.conf exists at the canonical config path, creating it from skeleton if missing or empty.
43///
44/// Inputs:
45/// - None.
46///
47/// Output:
48/// - No return value; logs success or failure.
49///
50/// Details:
51/// - Always targets `config_dir()/theme.conf` for creation (never the legacy pacsea.conf).
52/// - Reading continues to use `resolve_theme_config_path()` elsewhere so legacy paths are still used when present.
53/// - Creates parent dirs and writes skeleton only when theme.conf is missing or empty.
54/// - Logs success only when both `create_dir_all` and write succeed.
55/// - Logs errors when I/O fails (e.g. permissions, read-only FS) for easier diagnosis.
56fn ensure_theme_file_exists() {
57 let path = config_dir().join("theme.conf");
58
59 // Only create if file doesn't exist or is empty
60 let should_create = fs::metadata(&path).map_or(true, |meta| meta.len() == 0);
61
62 if should_create {
63 if let Some(dir) = path.parent()
64 && let Err(e) = fs::create_dir_all(dir)
65 {
66 tracing::error!(
67 path = %dir.display(),
68 error = %e,
69 "Failed to create theme config directory"
70 );
71 return;
72 }
73 match fs::write(&path, THEME_SKELETON_CONTENT) {
74 Ok(()) => {
75 tracing::info!(path = %path.display(), "Created theme.conf skeleton");
76 }
77 Err(e) => {
78 tracing::error!(
79 path = %path.display(),
80 error = %e,
81 "Failed to write theme.conf skeleton"
82 );
83 }
84 }
85 }
86}
87
88/// What: Access the application's theme palette, loading or caching as needed.
89///
90/// Inputs:
91/// - None.
92///
93/// Output:
94/// - A copy of the currently loaded `Theme`.
95///
96/// # Panics
97/// - Panics if the theme store `RwLock` is poisoned
98///
99/// Details:
100/// - Lazily initializes a global `RwLock<Theme>` using `load_initial_theme`.
101/// - Subsequent calls reuse the cached theme until `reload_theme` updates it.
102pub fn theme() -> Theme {
103 let lock = THEME_STORE.get_or_init(|| RwLock::new(load_initial_theme()));
104 *lock.read().expect("theme store poisoned")
105}
106
107/// What: Reload the theme configuration on demand.
108///
109/// Inputs:
110/// - None (uses the unified resolution logic).
111///
112/// Output:
113/// - `Ok(())` when the theme is reloaded successfully.
114/// - `Err(String)` with a human-readable reason when reloading fails.
115///
116/// # Errors
117/// - Returns `Err` if the theme store lock cannot be acquired
118///
119/// Details:
120/// - Re-runs the full resolution logic (reads settings, theme.conf, queries terminal).
121/// - Keeps the in-memory cache up to date so the UI can refresh without restarting Pacsea.
122/// - With the new resolution logic, this never fails due to missing/invalid theme.conf
123/// as it falls back to terminal theme or codebase default.
124pub fn reload_theme() -> std::result::Result<(), String> {
125 // Re-run resolution to pick up any settings changes
126 let resolved = resolve_theme();
127
128 let lock = THEME_STORE.get_or_init(|| RwLock::new(load_initial_theme()));
129 lock.write().map_or_else(
130 |_| Err("Failed to acquire theme store for writing".to_string()),
131 |mut guard| {
132 *guard = resolved.theme;
133 tracing::info!(source = ?resolved.source, "Theme reloaded");
134 Ok(())
135 },
136 )
137}