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
68fn 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}