use ratatui::style::{Modifier, Style};
use ratatui::text::{Line, Span};
use crate::state::AppState;
use crate::state::modal::DoasPersistChoice;
use crate::theme::theme;
pub const DOAS_PERSIST_INSTRUCTION_VIEWPORT_LINES: usize = 10;
fn current_username_for_snippet() -> String {
let from_env = std::env::var("USER")
.or_else(|_| std::env::var("LOGNAME"))
.ok()
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
if let Some(name) = from_env {
return name;
}
let from_id = std::process::Command::new("id")
.args(["-un"])
.output()
.ok()
.filter(|out| out.status.success())
.and_then(|out| String::from_utf8(out.stdout).ok())
.map(|s| s.trim().to_string())
.filter(|s| !s.is_empty());
from_id.unwrap_or_else(|| "youruser".to_string())
}
#[must_use]
pub fn doas_conf_has_persist_rule(contents: &str) -> bool {
contents.lines().any(|line| {
let trimmed = line.trim();
if trimmed.is_empty() || trimmed.starts_with('#') {
return false;
}
let without_inline_comment = trimmed
.split_once('#')
.map_or(trimmed, |(rule, _)| rule.trim());
let tokens: Vec<&str> = without_inline_comment.split_whitespace().collect();
tokens.contains(&"permit") && tokens.contains(&"persist")
})
}
#[must_use]
pub fn pacsea_doas_persist_configured() -> bool {
std::fs::read_to_string("/etc/doas.conf")
.ok()
.is_some_and(|contents| doas_conf_has_persist_rule(&contents))
}
#[must_use]
pub fn doas_persist_snippet(choice: DoasPersistChoice, username: &str) -> String {
match choice {
DoasPersistChoice::WheelScoped => "permit persist :wheel as root".to_string(),
DoasPersistChoice::UserScoped => format!("permit persist {username} as root"),
DoasPersistChoice::Skip => "# Skip setup".to_string(),
}
}
#[must_use]
pub fn validation_commands() -> Vec<&'static str> {
vec!["doas -C /etc/doas.conf", "doas -n true"]
}
#[must_use]
#[allow(clippy::vec_init_then_push)]
pub fn doas_persist_instruction_lines(
app: &AppState,
choice: DoasPersistChoice,
) -> Vec<Line<'static>> {
let th = theme();
let user = current_username_for_snippet();
let snippet = doas_persist_snippet(choice, &user);
let mut lines: Vec<Line<'static>> = Vec::new();
lines.push(Line::from(Span::styled(
crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_heading"),
Style::default().fg(th.mauve).add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_note"),
Style::default().fg(th.text),
)));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
crate::i18n::t(app, "app.modals.doas_persist_setup.label_snippet"),
Style::default()
.fg(th.overlay1)
.add_modifier(Modifier::BOLD),
)));
lines.push(Line::from(Span::styled(
snippet,
Style::default().fg(th.lavender),
)));
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
crate::i18n::t(app, "app.modals.doas_persist_setup.label_checks"),
Style::default()
.fg(th.overlay1)
.add_modifier(Modifier::BOLD),
)));
for cmd in validation_commands() {
lines.push(Line::from(Span::styled(
format!(" {cmd}"),
Style::default().fg(th.subtext1),
)));
}
lines.push(Line::from(""));
lines.push(Line::from(Span::styled(
crate::i18n::t(app, "app.modals.doas_persist_setup.instructions_footer"),
Style::default().fg(th.overlay1),
)));
lines
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn snippet_generation_uses_expected_forms() {
assert_eq!(
doas_persist_snippet(DoasPersistChoice::WheelScoped, "alice"),
"permit persist :wheel as root"
);
assert_eq!(
doas_persist_snippet(DoasPersistChoice::UserScoped, "alice"),
"permit persist alice as root"
);
}
#[test]
fn validation_commands_include_required_checks() {
let cmds = validation_commands();
assert!(cmds.iter().any(|c| c.contains("doas -C /etc/doas.conf")));
assert!(cmds.iter().any(|c| c.contains("doas -n true")));
}
#[test]
fn doas_conf_parser_detects_active_persist_rule() {
let conf = r"
# comment
permit persist :wheel as root
";
assert!(doas_conf_has_persist_rule(conf));
}
#[test]
fn doas_conf_parser_ignores_commented_persist_rule() {
let conf = r"
# permit persist :wheel as root
permit :wheel as root
";
assert!(!doas_conf_has_persist_rule(conf));
}
}