Skip to main content

claude_code_status_line/
commands.rs

1//! Slash commands embedded in the binary and installed with `--install-commands`.
2//!
3//! The binary is distributed through crates.io only, so shipping the commands
4//! inside it keeps them in lockstep with the installed version.
5
6use std::fs;
7use std::io;
8use std::path::{Path, PathBuf};
9
10pub const COMMANDS: &[(&str, &str)] = &[
11    (
12        "install-statusline.md",
13        include_str!("../.claude/commands/install-statusline.md"),
14    ),
15    (
16        "update-statusline.md",
17        include_str!("../.claude/commands/update-statusline.md"),
18    ),
19    (
20        "customize-statusline.md",
21        include_str!("../.claude/commands/customize-statusline.md"),
22    ),
23    (
24        "uninstall-statusline.md",
25        include_str!("../.claude/commands/uninstall-statusline.md"),
26    ),
27];
28
29#[derive(Debug, PartialEq)]
30pub enum InstallOutcome {
31    Installed,
32    UpToDate,
33    /// Existing file differed; the previous content was saved to `backup`.
34    Updated {
35        backup: PathBuf,
36    },
37}
38
39/// Write every embedded command into `dir`, backing up files whose content differs.
40pub fn install_commands(dir: &Path) -> io::Result<Vec<(&'static str, InstallOutcome)>> {
41    fs::create_dir_all(dir)?;
42
43    COMMANDS
44        .iter()
45        .map(|&(name, content)| {
46            let path = dir.join(name);
47            let outcome = match fs::read_to_string(&path) {
48                Ok(existing) if existing.replace("\r\n", "\n") == content => {
49                    InstallOutcome::UpToDate
50                }
51                Ok(_) => {
52                    let backup = next_backup_path(dir, name);
53                    fs::copy(&path, &backup)?;
54                    fs::write(&path, content)?;
55                    InstallOutcome::Updated { backup }
56                }
57                Err(err) if err.kind() == io::ErrorKind::NotFound => {
58                    fs::write(&path, content)?;
59                    InstallOutcome::Installed
60                }
61                Err(err) => return Err(err),
62            };
63            Ok((name, outcome))
64        })
65        .collect()
66}
67
68/// First free backup name (`<name>.bak`, `<name>.bak.1`, ...) so an earlier
69/// backup holding user edits is never overwritten.
70fn next_backup_path(dir: &Path, name: &str) -> PathBuf {
71    let first = dir.join(format!("{}.bak", name));
72    if !first.exists() {
73        return first;
74    }
75    (1..)
76        .map(|n| dir.join(format!("{}.bak.{}", name, n)))
77        .find(|path| !path.exists())
78        .expect("unbounded range always yields a free name")
79}
80
81#[cfg(test)]
82mod tests {
83    use super::*;
84
85    #[test]
86    fn test_install_commands_fresh_then_up_to_date() {
87        let dir = tempfile::tempdir().unwrap();
88
89        let first = install_commands(dir.path()).unwrap();
90        assert_eq!(first.len(), COMMANDS.len());
91        assert!(first
92            .iter()
93            .all(|(_, outcome)| *outcome == InstallOutcome::Installed));
94
95        let second = install_commands(dir.path()).unwrap();
96        assert!(second
97            .iter()
98            .all(|(_, outcome)| *outcome == InstallOutcome::UpToDate));
99    }
100
101    #[test]
102    fn test_install_commands_backs_up_modified_file() {
103        let dir = tempfile::tempdir().unwrap();
104        let (name, content) = COMMANDS[0];
105        fs::write(dir.path().join(name), "custom edits").unwrap();
106
107        let results = install_commands(dir.path()).unwrap();
108        let backup = dir.path().join(format!("{}.bak", name));
109
110        assert_eq!(
111            results[0],
112            (
113                name,
114                InstallOutcome::Updated {
115                    backup: backup.clone()
116                }
117            )
118        );
119        assert_eq!(fs::read_to_string(backup).unwrap(), "custom edits");
120        assert_eq!(fs::read_to_string(dir.path().join(name)).unwrap(), content);
121    }
122
123    #[test]
124    fn test_install_commands_keeps_earlier_backup() {
125        let dir = tempfile::tempdir().unwrap();
126        let (name, _) = COMMANDS[0];
127        fs::write(dir.path().join(name), "user edits").unwrap();
128        install_commands(dir.path()).unwrap();
129
130        fs::write(dir.path().join(name), "older stock").unwrap();
131        let results = install_commands(dir.path()).unwrap();
132
133        let second_backup = dir.path().join(format!("{}.bak.1", name));
134        assert_eq!(
135            results[0].1,
136            InstallOutcome::Updated {
137                backup: second_backup.clone()
138            }
139        );
140        assert_eq!(
141            fs::read_to_string(dir.path().join(format!("{}.bak", name))).unwrap(),
142            "user edits"
143        );
144        assert_eq!(fs::read_to_string(second_backup).unwrap(), "older stock");
145    }
146
147    #[test]
148    fn test_install_commands_treats_crlf_copy_as_up_to_date() {
149        let dir = tempfile::tempdir().unwrap();
150        let (name, content) = COMMANDS[0];
151        fs::write(dir.path().join(name), content.replace('\n', "\r\n")).unwrap();
152
153        let results = install_commands(dir.path()).unwrap();
154        assert_eq!(results[0].1, InstallOutcome::UpToDate);
155    }
156}