pacsea/logic/
sudo_timestamp_setup.rs1use ratatui::style::{Modifier, Style};
4use ratatui::text::{Line, Span};
5
6use crate::state::AppState;
7use crate::state::modal::SudoTimestampChoice;
8use crate::theme::theme;
9
10pub const SUDO_TIMESTAMP_INSTRUCTION_VIEWPORT_LINES: usize = 9;
12
13#[must_use]
25#[allow(clippy::vec_init_then_push)] pub fn sudo_timestamp_instruction_lines(
27 app: &AppState,
28 choice: SudoTimestampChoice,
29) -> Vec<Line<'static>> {
30 let th = theme();
31 let user = std::env::var("USER").unwrap_or_else(|_| "youruser".to_string());
32 let heading_key = match choice {
33 SudoTimestampChoice::TenMinutes => {
34 "app.modals.sudo_timestamp_setup.instructions_heading_ten"
35 }
36 SudoTimestampChoice::ThirtyMinutes => {
37 "app.modals.sudo_timestamp_setup.instructions_heading_thirty"
38 }
39 SudoTimestampChoice::Infinity => {
40 "app.modals.sudo_timestamp_setup.instructions_heading_infinity"
41 }
42 };
43 let mut lines: Vec<Line<'static>> = Vec::new();
44 lines.push(Line::from(Span::styled(
45 crate::i18n::t(app, heading_key),
46 Style::default().fg(th.mauve).add_modifier(Modifier::BOLD),
47 )));
48 lines.push(Line::from(""));
49 lines.push(Line::from(Span::styled(
50 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.instructions_note_tui"),
51 Style::default().fg(th.text),
52 )));
53 lines.push(Line::from(""));
54 lines.push(Line::from(Span::styled(
55 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.label_dropin_contents"),
56 Style::default()
57 .fg(th.overlay1)
58 .add_modifier(Modifier::BOLD),
59 )));
60 for l in sudoers_defaults_line(choice).lines() {
61 lines.push(Line::from(Span::styled(
62 l.to_string(),
63 Style::default().fg(th.lavender),
64 )));
65 }
66 lines.push(Line::from(""));
67 lines.push(Line::from(Span::styled(
68 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.label_user_scoped"),
69 Style::default()
70 .fg(th.overlay1)
71 .add_modifier(Modifier::BOLD),
72 )));
73 for l in sudoers_user_scoped_line(&user, choice).lines() {
74 lines.push(Line::from(Span::styled(
75 l.to_string(),
76 Style::default().fg(th.lavender),
77 )));
78 }
79 lines.push(Line::from(""));
80 lines.push(Line::from(Span::styled(
81 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.label_manual"),
82 Style::default()
83 .fg(th.overlay1)
84 .add_modifier(Modifier::BOLD),
85 )));
86 lines.push(Line::from(Span::styled(
87 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.manual_step_1"),
88 Style::default().fg(th.subtext1),
89 )));
90 lines.push(Line::from(Span::styled(
91 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.manual_step_2"),
92 Style::default().fg(th.subtext1),
93 )));
94 lines.push(Line::from(Span::styled(
95 crate::i18n::t(app, "app.modals.sudo_timestamp_setup.manual_step_3"),
96 Style::default().fg(th.subtext1),
97 )));
98 lines
99}
100
101pub const SUDOERS_DROP_IN_PATH: &str = "/etc/sudoers.d/99-pacsea-timestamp";
103
104const DROP_IN_MARKER: &str = "Pacsea optional:";
106
107#[must_use]
118pub const fn timestamp_timeout_value(choice: SudoTimestampChoice) -> i32 {
119 match choice {
120 SudoTimestampChoice::TenMinutes => 10,
121 SudoTimestampChoice::ThirtyMinutes => 30,
122 SudoTimestampChoice::Infinity => -1,
123 }
124}
125
126#[must_use]
137pub fn sudoers_defaults_line(choice: SudoTimestampChoice) -> String {
138 let v = timestamp_timeout_value(choice);
139 format!(
140 "# {DROP_IN_MARKER} extend sudo credential cache for long installs/updates\nDefaults timestamp_timeout={v}"
141 )
142}
143
144#[must_use]
156pub fn sudoers_user_scoped_line(username: &str, choice: SudoTimestampChoice) -> String {
157 let v = timestamp_timeout_value(choice);
158 format!(
159 "# {DROP_IN_MARKER} extend sudo credential cache for this user only\nDefaults:{username} timestamp_timeout={v}"
160 )
161}
162
163#[must_use]
174pub fn pacsea_sudo_timestamp_drop_in_present() -> bool {
175 std::fs::read_to_string(SUDOERS_DROP_IN_PATH)
176 .ok()
177 .is_some_and(|s| s.contains(DROP_IN_MARKER) && s.contains("timestamp_timeout"))
178}
179
180#[must_use]
192pub fn apply_drop_in_shell_script(choice: SudoTimestampChoice) -> String {
193 let block = sudoers_defaults_line(choice);
194 let path = SUDOERS_DROP_IN_PATH;
195 format!(
196 r#"set -euo pipefail
197DROP_IN="{path}"
198TMP="$(mktemp)"
199trap 'rm -f "$TMP"' EXIT
200cat <<'EOF' >"$TMP"
201{block}
202EOF
203sudo visudo -cf "$TMP"
204sudo install -m 0440 "$TMP" "$DROP_IN"
205echo "Installed $DROP_IN"
206read -r -p "Press Enter to close... " _
207"#
208 )
209}
210
211#[cfg(test)]
212mod tests {
213 use super::*;
214
215 #[test]
216 fn timestamp_timeout_value_maps_choices() {
217 assert_eq!(timestamp_timeout_value(SudoTimestampChoice::TenMinutes), 10);
218 assert_eq!(
219 timestamp_timeout_value(SudoTimestampChoice::ThirtyMinutes),
220 30
221 );
222 assert_eq!(timestamp_timeout_value(SudoTimestampChoice::Infinity), -1);
223 }
224
225 #[test]
226 fn sudoers_defaults_line_contains_marker_and_defaults() {
227 let s = sudoers_defaults_line(SudoTimestampChoice::ThirtyMinutes);
228 assert!(s.contains("Defaults timestamp_timeout=30"));
229 assert!(s.contains(DROP_IN_MARKER));
230 }
231
232 #[test]
233 fn sudoers_user_scoped_line_contains_username() {
234 let s = sudoers_user_scoped_line("alice", SudoTimestampChoice::TenMinutes);
235 assert!(s.contains("Defaults:alice timestamp_timeout=10"));
236 }
237
238 #[test]
239 fn apply_script_runs_visudo_and_install() {
240 let script = apply_drop_in_shell_script(SudoTimestampChoice::Infinity);
241 assert!(script.contains("visudo -cf"));
242 assert!(script.contains("install -m 0440"));
243 assert!(script.contains(SUDOERS_DROP_IN_PATH));
244 }
245}