1use crate::util::paths;
9use std::fs::{File, OpenOptions};
10use std::io::{Read, Seek, Write};
11use std::os::unix::io::AsRawFd;
12use std::path::PathBuf;
13
14#[derive(Debug, Default)]
16pub struct MruState {
17 pub current: Option<String>,
19 pub previous: Option<String>,
21}
22
23fn mru_path() -> PathBuf {
27 match paths::mru_file() {
28 Ok(path) => path,
29 Err(e) => {
30 tracing::warn!(
31 "Failed to get secure MRU path: {}. MRU tracking disabled.",
32 e
33 );
34 PathBuf::from("/nonexistent/open-sesame-mru")
35 }
36 }
37}
38
39fn lock_file_exclusive(file: &File) -> bool {
41 let fd = file.as_raw_fd();
42 unsafe { libc::flock(fd, libc::LOCK_EX) == 0 }
43}
44
45fn lock_file_shared(file: &File) -> bool {
47 let fd = file.as_raw_fd();
48 unsafe { libc::flock(fd, libc::LOCK_SH) == 0 }
49}
50
51fn parse_mru_contents(contents: &str) -> MruState {
53 let lines: Vec<&str> = contents.lines().collect();
54 let previous = lines
55 .first()
56 .map(|s| s.trim().to_string())
57 .filter(|s| !s.is_empty());
58 let current = lines
59 .get(1)
60 .map(|s| s.trim().to_string())
61 .filter(|s| !s.is_empty());
62
63 MruState { current, previous }
64}
65
66pub fn save_activated_window(origin_window_id: Option<&str>, new_window_id: &str) {
75 let path = mru_path();
76
77 if origin_window_id == Some(new_window_id) {
79 tracing::debug!("MRU: activating same window as origin, not updating");
80 return;
81 }
82
83 let file = match OpenOptions::new()
85 .read(true)
86 .write(true)
87 .create(true)
88 .truncate(false)
89 .open(&path)
90 {
91 Ok(f) => f,
92 Err(e) => {
93 tracing::warn!("Failed to open MRU file: {}", e);
94 return;
95 }
96 };
97
98 if !lock_file_exclusive(&file) {
100 tracing::warn!("Failed to lock MRU file for writing");
101 return;
102 }
103
104 let previous = origin_window_id.unwrap_or("");
106 let new_state = format!("{}\n{}", previous, new_window_id);
107
108 let mut file = file;
109 if let Err(e) = file.seek(std::io::SeekFrom::Start(0)) {
110 tracing::warn!("Failed to seek MRU file: {}", e);
111 return;
112 }
113
114 if let Err(e) = file.set_len(0) {
115 tracing::warn!("Failed to truncate MRU file: {}", e);
116 return;
117 }
118
119 if let Err(e) = file.write_all(new_state.as_bytes()) {
120 tracing::warn!("Failed to write MRU state: {}", e);
121 return;
122 }
123
124 tracing::info!(
125 "MRU: saved state - previous={:?}, current={}",
126 origin_window_id,
127 new_window_id
128 );
129 }
131
132pub fn load_mru_state() -> MruState {
134 let path = mru_path();
135
136 let file = match File::open(&path) {
137 Ok(f) => f,
138 Err(_) => {
139 tracing::debug!("MRU: no state file found");
140 return MruState::default();
141 }
142 };
143
144 if !lock_file_shared(&file) {
146 tracing::warn!("Failed to lock MRU file for reading");
147 return MruState::default();
148 }
149
150 let mut contents = String::new();
151 let mut file = file;
152 if file.read_to_string(&mut contents).is_err() {
153 return MruState::default();
154 }
155
156 let state = parse_mru_contents(&contents);
157
158 tracing::debug!(
159 "MRU: loaded state - previous={:?}, current={:?}",
160 state.previous,
161 state.current
162 );
163
164 state
165 }
167
168pub fn get_previous_window() -> Option<String> {
170 let state = load_mru_state();
171 state.previous
172}
173
174pub fn get_current_window() -> Option<String> {
176 let state = load_mru_state();
177 state.current
178}
179
180pub fn reorder_for_mru<T, F>(windows: &mut Vec<T>, get_id: F)
184where
185 F: Fn(&T) -> &str,
186{
187 let state = load_mru_state();
188
189 if let Some(current_id) = &state.current {
190 if let Some(pos) = windows.iter().position(|w| get_id(w) == current_id) {
191 if pos < windows.len() - 1 {
192 let window = windows.remove(pos);
193 windows.push(window);
194 tracing::info!("MRU: moved current window from index {} to end", pos);
195 } else {
196 tracing::debug!("MRU: current window already at end");
197 }
198 } else {
199 tracing::debug!("MRU: current window not found in list");
200 }
201 } else {
202 tracing::debug!("MRU: no current window recorded");
203 }
204}
205
206#[cfg(test)]
207mod tests {
208 use super::*;
209
210 #[test]
211 fn test_parse_mru_contents_empty() {
212 let state = parse_mru_contents("");
213 assert!(state.previous.is_none());
214 assert!(state.current.is_none());
215 }
216
217 #[test]
218 fn test_parse_mru_contents_single_line() {
219 let state = parse_mru_contents("window-id-prev");
220 assert_eq!(state.previous, Some("window-id-prev".to_string()));
221 assert!(state.current.is_none());
222 }
223
224 #[test]
225 fn test_parse_mru_contents_two_lines() {
226 let state = parse_mru_contents("window-prev\nwindow-current");
227 assert_eq!(state.previous, Some("window-prev".to_string()));
228 assert_eq!(state.current, Some("window-current".to_string()));
229 }
230
231 #[test]
232 fn test_parse_mru_contents_with_whitespace() {
233 let state = parse_mru_contents(" window-prev \n window-current ");
234 assert_eq!(state.previous, Some("window-prev".to_string()));
235 assert_eq!(state.current, Some("window-current".to_string()));
236 }
237
238 #[test]
239 fn test_parse_mru_contents_empty_lines() {
240 let state = parse_mru_contents("\n");
241 assert!(state.previous.is_none());
242 assert!(state.current.is_none());
243 }
244
245 #[test]
246 fn test_mru_state_default() {
247 let state = MruState::default();
248 assert!(state.current.is_none());
249 assert!(state.previous.is_none());
250 }
251
252 #[test]
253 fn test_reorder_for_mru_basic() {
254 #[derive(Debug, Clone, PartialEq)]
258 struct MockWindow {
259 id: String,
260 name: String,
261 }
262
263 let mut windows = vec![
264 MockWindow {
265 id: "a".to_string(),
266 name: "Window A".to_string(),
267 },
268 MockWindow {
269 id: "b".to_string(),
270 name: "Window B".to_string(),
271 },
272 MockWindow {
273 id: "c".to_string(),
274 name: "Window C".to_string(),
275 },
276 ];
277
278 let current_id = "a";
281 if let Some(pos) = windows.iter().position(|w| w.id == current_id)
282 && pos < windows.len() - 1
283 {
284 let window = windows.remove(pos);
285 windows.push(window);
286 }
287
288 assert_eq!(windows[0].id, "b");
289 assert_eq!(windows[1].id, "c");
290 assert_eq!(windows[2].id, "a"); }
292
293 #[test]
294 fn test_reorder_already_at_end() {
295 #[derive(Debug, Clone, PartialEq)]
296 struct MockWindow {
297 id: String,
298 }
299
300 let mut windows = vec![
301 MockWindow {
302 id: "a".to_string(),
303 },
304 MockWindow {
305 id: "b".to_string(),
306 },
307 MockWindow {
308 id: "c".to_string(),
309 },
310 ];
311
312 let current_id = "c";
314 let original = windows.clone();
315 if let Some(pos) = windows.iter().position(|w| w.id == current_id)
316 && pos < windows.len() - 1
317 {
318 let window = windows.remove(pos);
319 windows.push(window);
320 }
321
322 assert_eq!(windows, original);
323 }
324
325 #[test]
326 fn test_reorder_not_found() {
327 #[derive(Debug, Clone, PartialEq)]
328 struct MockWindow {
329 id: String,
330 }
331
332 let mut windows = vec![
333 MockWindow {
334 id: "a".to_string(),
335 },
336 MockWindow {
337 id: "b".to_string(),
338 },
339 ];
340
341 let current_id = "nonexistent";
343 let original = windows.clone();
344 if let Some(pos) = windows.iter().position(|w| w.id == current_id)
345 && pos < windows.len() - 1
346 {
347 let window = windows.remove(pos);
348 windows.push(window);
349 }
350
351 assert_eq!(windows, original);
352 }
353}