Rust · 6764 bytes Raw Blame History
1 //! garbg integration for wallpaper resolution
2 //!
3 //! Reads garbg configuration and playlist state to determine which wallpaper
4 //! to display, creating a seamless transition from greeter to desktop.
5
6 use nix::unistd::User;
7 use serde::Deserialize;
8 use std::path::PathBuf;
9
10 /// garbg configuration file structure
11 #[derive(Debug, Deserialize, Default)]
12 pub struct GarbgConfig {
13 #[serde(default)]
14 pub general: GeneralConfig,
15 #[serde(default)]
16 pub default: DefaultConfig,
17 }
18
19 #[derive(Debug, Deserialize, Default)]
20 pub struct GeneralConfig {
21 #[serde(default = "default_mode")]
22 pub mode: String,
23 }
24
25 #[derive(Debug, Deserialize, Default)]
26 pub struct DefaultConfig {
27 #[serde(default)]
28 pub source: String,
29 }
30
31 fn default_mode() -> String {
32 "fill".to_string()
33 }
34
35 impl GarbgConfig {
36 /// Load garbg config from user or system location
37 pub fn load(username: Option<&str>) -> Option<Self> {
38 // Try user config first (if we know the username)
39 if let Some(user) = username {
40 if let Some(home) = get_user_home(user) {
41 let user_config = home.join(".config/garbg/config.toml");
42 if let Ok(content) = std::fs::read_to_string(&user_config) {
43 if let Ok(config) = toml::from_str(&content) {
44 tracing::debug!("Loaded garbg config from {:?}", user_config);
45 return Some(config);
46 }
47 }
48 }
49 }
50
51 // Try system-wide default
52 let system_config = PathBuf::from("/etc/garbg/config.toml");
53 if let Ok(content) = std::fs::read_to_string(&system_config) {
54 if let Ok(config) = toml::from_str(&content) {
55 tracing::debug!("Loaded garbg config from {:?}", system_config);
56 return Some(config);
57 }
58 }
59
60 None
61 }
62
63 /// Get the default wallpaper path from config
64 pub fn default_wallpaper(&self) -> Option<String> {
65 if self.default.source.is_empty() {
66 None
67 } else {
68 Some(expand_path(&self.default.source))
69 }
70 }
71 }
72
73 /// garbg playlist state (runtime state file)
74 #[derive(Debug, Deserialize)]
75 pub struct PlaylistState {
76 pub source: String,
77 pub images: Vec<String>,
78 pub current_index: usize,
79 #[serde(default)]
80 pub shuffled: bool,
81 }
82
83 impl PlaylistState {
84 /// Load current playlist state from runtime directory
85 pub fn load(username: Option<&str>) -> Option<Self> {
86 // Try XDG_RUNTIME_DIR first
87 if let Ok(runtime_dir) = std::env::var("XDG_RUNTIME_DIR") {
88 let state_file = PathBuf::from(&runtime_dir).join("garbg-state.json");
89 if let Ok(content) = std::fs::read_to_string(&state_file) {
90 if let Ok(state) = serde_json::from_str(&content) {
91 tracing::debug!("Loaded playlist state from {:?}", state_file);
92 return Some(state);
93 }
94 }
95 }
96
97 // Try user-specific runtime directory
98 if let Some(user) = username {
99 if let Some(uid) = get_user_uid(user) {
100 let user_runtime = format!("/run/user/{}/garbg-state.json", uid);
101 if let Ok(content) = std::fs::read_to_string(&user_runtime) {
102 if let Ok(state) = serde_json::from_str(&content) {
103 tracing::debug!("Loaded playlist state from {}", user_runtime);
104 return Some(state);
105 }
106 }
107 }
108 }
109
110 None
111 }
112
113 /// Get the current wallpaper path
114 pub fn current_wallpaper(&self) -> Option<&str> {
115 self.images.get(self.current_index).map(|s| s.as_str())
116 }
117 }
118
119 /// Resolves which wallpaper to use for the greeter
120 pub struct WallpaperResolver {
121 fallback_path: String,
122 }
123
124 impl WallpaperResolver {
125 pub fn new(fallback: &str) -> Self {
126 Self {
127 fallback_path: fallback.to_string(),
128 }
129 }
130
131 /// Resolve wallpaper path, trying multiple sources in priority order
132 pub fn resolve(&self, username: Option<&str>) -> String {
133 // Priority 1: Current playlist state (what user was last seeing)
134 if let Some(state) = PlaylistState::load(username) {
135 if let Some(current) = state.current_wallpaper() {
136 let expanded = expand_path(current);
137 if std::path::Path::new(&expanded).exists() {
138 tracing::info!("Using wallpaper from playlist: {}", expanded);
139 return expanded;
140 }
141 }
142 }
143
144 // Priority 2: garbg config default
145 if let Some(config) = GarbgConfig::load(username) {
146 if let Some(default) = config.default_wallpaper() {
147 let path = std::path::Path::new(&default);
148 if path.is_dir() {
149 // If it's a directory, pick first image
150 if let Some(first) = first_image_in_dir(&default) {
151 tracing::info!("Using first image from garbg source dir: {}", first);
152 return first;
153 }
154 } else if path.exists() {
155 tracing::info!("Using wallpaper from garbg config: {}", default);
156 return default;
157 }
158 }
159 }
160
161 // Priority 3: Fallback
162 tracing::info!("Using fallback wallpaper: {}", self.fallback_path);
163 self.fallback_path.clone()
164 }
165 }
166
167 /// Get user's home directory
168 fn get_user_home(username: &str) -> Option<PathBuf> {
169 User::from_name(username)
170 .ok()?
171 .map(|u| PathBuf::from(u.dir.to_string_lossy().to_string()))
172 }
173
174 /// Get user's UID
175 fn get_user_uid(username: &str) -> Option<u32> {
176 User::from_name(username).ok()?.map(|u| u.uid.as_raw())
177 }
178
179 /// Expand ~ and environment variables in path
180 fn expand_path(path: &str) -> String {
181 shellexpand::full(path)
182 .map(|s| s.to_string())
183 .unwrap_or_else(|_| path.to_string())
184 }
185
186 /// Get first image file in a directory (sorted alphabetically)
187 fn first_image_in_dir(dir: &str) -> Option<String> {
188 const EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "webp", "bmp"];
189
190 let mut entries: Vec<_> = std::fs::read_dir(dir)
191 .ok()?
192 .filter_map(|e| e.ok())
193 .filter(|e| {
194 let path = e.path();
195 if let Some(ext) = path.extension() {
196 EXTENSIONS.contains(&ext.to_string_lossy().to_lowercase().as_str())
197 } else {
198 false
199 }
200 })
201 .collect();
202
203 entries.sort_by_key(|e| e.path());
204 entries
205 .first()
206 .map(|e| e.path().to_string_lossy().to_string())
207 }
208