Skip to main content

nexus_core/app/
scripts.rs

1// Casts here are on bounded values: token counts, byte sizes, and
2// selection indices — never on unbounded input. JSON-derived indices in
3// provider/tools go through try_from instead.
4#![allow(
5    clippy::cast_possible_truncation,
6    clippy::cast_possible_wrap,
7    clippy::cast_precision_loss,
8    clippy::cast_sign_loss
9)]
10use super::App;
11
12impl App {
13    /// Read the space's scripts dir and populate `scripts_cache`. A missing or
14    /// empty dir produces an empty cache, never an error. The scripts popup's
15    /// flow (selection/edit state, $EDITOR handoff) lives in the view layer.
16    pub fn refresh_scripts(&mut self) {
17        let dir = self.space.scripts_dir(&self.active_space.name);
18        let _ = std::fs::create_dir_all(&dir);
19        self.scripts_cache = match std::fs::read_dir(&dir) {
20            Err(_) => Vec::new(),
21            Ok(entries) => entries
22                .flatten()
23                .filter(|e| e.path().is_file())
24                .filter_map(|e| {
25                    let meta = e.metadata().ok()?;
26                    let modified = meta
27                        .modified()
28                        .ok()
29                        .and_then(|t| {
30                            chrono::DateTime::from_timestamp(
31                                t.duration_since(std::time::UNIX_EPOCH).ok()?.as_secs() as i64,
32                                0,
33                            )
34                        })
35                        .map(|dt| dt.to_rfc3339())
36                        .unwrap_or_default();
37                    Some(super::ScriptMeta {
38                        name: e.file_name().to_string_lossy().to_string(),
39                        size: meta.len(),
40                        modified,
41                    })
42                })
43                .collect(),
44        };
45        self.scripts_cache.sort_by(|a, b| a.name.cmp(&b.name));
46    }
47
48    /// Domain half of script create: touch the file (if absent) and refresh
49    /// the cache. Returns the created path. The view owns the edit buffer and
50    /// the $EDITOR handoff.
51    pub fn ensure_script_file(&mut self, name: &str) -> anyhow::Result<std::path::PathBuf> {
52        use anyhow::Context as _;
53        let dir = self.space.scripts_dir(&self.active_space.name);
54        std::fs::create_dir_all(&dir).with_context(|| format!("creating {}", dir.display()))?;
55        let path = dir.join(name);
56        if !path.exists() {
57            std::fs::write(&path, "").with_context(|| format!("creating {}", path.display()))?;
58        }
59        self.refresh_scripts();
60        Ok(path)
61    }
62
63    /// Domain half of script rename: move the file on disk. Returns an error
64    /// message string when the target already exists (the view turns it into
65    /// a status line); Ok otherwise.
66    pub fn rename_script_file(&mut self, from: &str, to: &str) -> anyhow::Result<()> {
67        use anyhow::Context as _;
68        let dir = self.space.scripts_dir(&self.active_space.name);
69        let from_path = dir.join(from);
70        let to_path = dir.join(to);
71        if to_path.exists() {
72            anyhow::bail!("{to} already exists");
73        }
74        std::fs::rename(&from_path, &to_path).with_context(|| {
75            format!("renaming {} to {}", from_path.display(), to_path.display())
76        })?;
77        self.refresh_scripts();
78        Ok(())
79    }
80
81    /// Domain half of script delete: remove the file from disk and refresh.
82    /// Returns whether a row existed.
83    pub fn delete_script_file(&mut self, name: &str) -> anyhow::Result<bool> {
84        use anyhow::Context as _;
85        let dir = self.space.scripts_dir(&self.active_space.name);
86        let path = dir.join(name);
87        if path.exists() {
88            std::fs::remove_file(&path).with_context(|| format!("removing {}", path.display()))?;
89            self.refresh_scripts();
90            Ok(true)
91        } else {
92            Ok(false)
93        }
94    }
95}