Skip to main content

prt_core/core/
session.rs

1//! Session management — encapsulates the refresh/diff/retain/sort cycle.
2//!
3//! [`Session`] is the single point of truth for scan state. The TUI
4//! delegates all data operations to it, keeping the UI layer thin.
5
6use crate::core::scanner;
7use crate::i18n;
8use crate::model::{EntryStatus, SortState, TrackedEntry, GONE_RETENTION};
9use std::time::Instant;
10
11/// Shared scan session state used by the TUI app.
12///
13/// Encapsulates the full refresh cycle:
14/// `scan → diff → retain(gone) → sort`
15///
16/// Stores sudo password (if elevated) so subsequent refreshes
17/// can re-authenticate without user interaction.
18pub struct Session {
19    pub entries: Vec<TrackedEntry>,
20    pub sort: SortState,
21    pub is_elevated: bool,
22    pub is_root: bool,
23    sudo_password: Option<String>,
24}
25
26impl Default for Session {
27    fn default() -> Self {
28        Self::new()
29    }
30}
31
32impl Session {
33    pub fn new() -> Self {
34        Self {
35            entries: Vec::new(),
36            sort: SortState::default(),
37            is_elevated: false,
38            is_root: scanner::is_root(),
39            sudo_password: None,
40        }
41    }
42
43    /// Run a scan cycle: scan -> diff -> retain -> sort.
44    pub fn refresh(&mut self) -> Result<(), String> {
45        let scan_result = if let Some(password) = &self.sudo_password {
46            scanner::scan_with_sudo(password)
47        } else {
48            scanner::scan()
49        };
50
51        match scan_result {
52            Ok(new_entries) => {
53                let now = Instant::now();
54                self.entries = scanner::diff_entries(&self.entries, new_entries, now);
55                self.entries.retain(|e| {
56                    e.status != EntryStatus::Gone || now.duration_since(e.seen_at) < GONE_RETENTION
57                });
58                scanner::sort_entries(&mut self.entries, &self.sort);
59                Ok(())
60            }
61            Err(e) => {
62                let s = i18n::strings();
63                Err(s.fmt_scan_error(&e.to_string()))
64            }
65        }
66    }
67
68    pub fn filtered_indices(&self, query: &str) -> Vec<usize> {
69        scanner::filter_indices(&self.entries, query)
70    }
71
72    /// Attempt sudo elevation with password. Returns status message.
73    pub fn try_sudo(&mut self, password: &str) -> String {
74        let s = i18n::strings();
75        match scanner::scan_with_sudo(password) {
76            Ok(new_entries) => {
77                self.sudo_password = Some(password.to_string());
78                self.is_elevated = true;
79                self.is_root = true;
80                let now = Instant::now();
81                self.entries = scanner::diff_entries(&self.entries, new_entries, now);
82                scanner::sort_entries(&mut self.entries, &self.sort);
83                s.sudo_elevated.to_string()
84            }
85            Err(e) => {
86                self.sudo_password = None;
87                self.is_elevated = false;
88                let msg = e.to_string();
89                if msg.contains("incorrect password") || msg.contains("Sorry") {
90                    s.sudo_wrong_password.to_string()
91                } else {
92                    s.fmt_sudo_error(&msg)
93                }
94            }
95        }
96    }
97}