claude_code_status_line/
commands.rs1use 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 Updated {
35 backup: PathBuf,
36 },
37}
38
39pub 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
68pub(crate) fn 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!(
92 first
93 .iter()
94 .all(|(_, outcome)| *outcome == InstallOutcome::Installed)
95 );
96
97 let second = install_commands(dir.path()).unwrap();
98 assert!(
99 second
100 .iter()
101 .all(|(_, outcome)| *outcome == InstallOutcome::UpToDate)
102 );
103 }
104
105 #[test]
106 fn test_install_commands_backs_up_modified_file() {
107 let dir = tempfile::tempdir().unwrap();
108 let (name, content) = COMMANDS[0];
109 fs::write(dir.path().join(name), "custom edits").unwrap();
110
111 let results = install_commands(dir.path()).unwrap();
112 let backup = dir.path().join(format!("{}.bak", name));
113
114 assert_eq!(
115 results[0],
116 (
117 name,
118 InstallOutcome::Updated {
119 backup: backup.clone()
120 }
121 )
122 );
123 assert_eq!(fs::read_to_string(backup).unwrap(), "custom edits");
124 assert_eq!(fs::read_to_string(dir.path().join(name)).unwrap(), content);
125 }
126
127 #[test]
128 fn test_install_commands_keeps_earlier_backup() {
129 let dir = tempfile::tempdir().unwrap();
130 let (name, _) = COMMANDS[0];
131 fs::write(dir.path().join(name), "user edits").unwrap();
132 install_commands(dir.path()).unwrap();
133
134 fs::write(dir.path().join(name), "older stock").unwrap();
135 let results = install_commands(dir.path()).unwrap();
136
137 let second_backup = dir.path().join(format!("{}.bak.1", name));
138 assert_eq!(
139 results[0].1,
140 InstallOutcome::Updated {
141 backup: second_backup.clone()
142 }
143 );
144 assert_eq!(
145 fs::read_to_string(dir.path().join(format!("{}.bak", name))).unwrap(),
146 "user edits"
147 );
148 assert_eq!(fs::read_to_string(second_backup).unwrap(), "older stock");
149 }
150
151 #[test]
152 fn test_install_commands_treats_crlf_copy_as_up_to_date() {
153 let dir = tempfile::tempdir().unwrap();
154 let (name, content) = COMMANDS[0];
155 fs::write(dir.path().join(name), content.replace('\n', "\r\n")).unwrap();
156
157 let results = install_commands(dir.path()).unwrap();
158 assert_eq!(results[0].1, InstallOutcome::UpToDate);
159 }
160}