Skip to main content

rpi_cli/
extras.rs

1//! First-launch setup banner — thin host wrapper over the rpi-tui components.
2//!
3//! Kept out of the library crate so rpi-tui stays cli-free (project constraint).
4
5use std::sync::Arc;
6
7use rpi_tui::Container;
8
9use crate::config;
10
11/// Path of the "first-time setup done" sentinel, under the agent dir
12/// (`~/.rpi/agent/.setup_done`).
13pub fn setup_done_path() -> Option<std::path::PathBuf> {
14    config::agent_dir().ok().map(|d| d.join(".setup_done"))
15}
16
17/// Whether first-time setup has already been completed (sentinel present).
18pub fn setup_done() -> bool {
19    setup_done_path().map(|p| p.exists()).unwrap_or(false)
20}
21
22/// Mark first-time setup complete (write the sentinel). Best-effort.
23pub fn mark_setup_done() -> std::io::Result<()> {
24    let path = setup_done_path().ok_or_else(|| {
25        std::io::Error::new(std::io::ErrorKind::NotFound, "no home dir for .rpi/agent")
26    })?;
27    if let Some(parent) = path.parent() {
28        std::fs::create_dir_all(parent)?;
29    }
30    std::fs::write(&path, b"1")
31}
32
33/// If first-time setup hasn't run yet, show a brief setup note in the chat
34/// container. Returns `true` if anything was shown.
35pub fn maybe_first_time_setup(chat: &Arc<Container>) -> bool {
36    use rpi_tui::Component;
37    use rpi_tui::{DynamicBorder, Spacer, Text};
38    if setup_done() {
39        return false;
40    }
41    let accent = rpi_tui::theme().colors.accent;
42    let muted = rpi_tui::theme().colors.muted;
43    let border = DynamicBorder::with_color(accent);
44    let mut lines: Vec<String> = Vec::new();
45    lines.extend(border.render(80));
46    lines.push(format!(
47        " {} Welcome to rpi!",
48        accent.fg(&bold("Welcome to rpi!"))
49    ));
50    lines.push(format!(
51        " {} Pick a theme with /theme (dark/light/monochrome).",
52        muted.fg("Pick a theme with /theme (dark/light/monochrome).")
53    ));
54    lines.push(format!(
55        " {} Type /help for commands.",
56        muted.fg("Type /help for commands.")
57    ));
58    lines.extend(border.render(80));
59    for line in lines {
60        chat.add_child(Arc::new(Text::new(line, 1, 0)));
61    }
62    chat.add_child(Arc::new(Spacer::new(1)));
63    let _ = mark_setup_done();
64    true
65}
66
67fn bold(s: &str) -> String {
68    format!("\x1b[1m{s}\x1b[22m")
69}
70
71#[cfg(test)]
72mod tests {
73    use super::*;
74
75    #[test]
76    fn test_sentinel_paths_under_agent_dir() {
77        // The sentinel lives directly under the resolved agent dir and honors
78        // the `RPI_CODING_AGENT_DIR` override (delegated to `config::agent_dir`).
79        let _guard = crate::config::test_support::env_lock().lock().unwrap();
80        let prev = std::env::var_os(crate::config::CONFIG_DIR_ENV);
81        let tmp = tempfile::TempDir::new().unwrap();
82        std::env::set_var(crate::config::CONFIG_DIR_ENV, tmp.path());
83        let agent = crate::config::agent_dir().unwrap();
84        assert_eq!(agent.as_path(), tmp.path());
85        if let Some(p) = setup_done_path() {
86            assert!(p.ends_with(".setup_done"));
87            assert_eq!(p.parent().unwrap(), agent);
88        }
89        match prev {
90            Some(v) => std::env::set_var(crate::config::CONFIG_DIR_ENV, v),
91            None => std::env::remove_var(crate::config::CONFIG_DIR_ENV),
92        }
93    }
94}