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}