1use anyhow::{Context, Result};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8pub struct StUnified {
10 st_binary: PathBuf,
11}
12
13impl StUnified {
14 pub fn new() -> Result<Self> {
15 let st_binary = std::env::current_exe()
17 .ok()
18 .and_then(|p| {
19 let dir = p.parent()?;
20 let st = dir.join("st");
21 if st.exists() {
22 Some(st)
23 } else {
24 None
25 }
26 })
27 .unwrap_or_else(|| PathBuf::from("./target/release/st"));
28
29 Ok(Self { st_binary })
30 }
31
32 pub fn ls(&self, path: &Path, pattern: Option<&str>) -> Result<String> {
34 let mut cmd = Command::new(&self.st_binary);
35 cmd.arg("--mode")
36 .arg("ls")
37 .arg("--depth")
38 .arg("1")
39 .arg("--no-emoji")
40 .arg(path);
41
42 if let Some(pat) = pattern {
43 cmd.arg("--find").arg(pat);
44 }
45
46 let output = cmd.output().context("Failed to run st for ls")?;
47
48 Ok(String::from_utf8_lossy(&output.stdout).to_string())
49 }
50
51 pub fn read(&self, path: &Path, offset: Option<usize>, limit: Option<usize>) -> Result<String> {
53 let content = std::fs::read_to_string(path).context("Failed to read file")?;
55
56 let lines: Vec<&str> = content.lines().collect();
57 let start = offset.unwrap_or(0);
58 let end = start + limit.unwrap_or(lines.len());
59
60 Ok(lines[start.min(lines.len())..end.min(lines.len())]
61 .iter()
62 .enumerate()
63 .map(|(i, line)| format!("{:6}→{}", start + i + 1, line))
64 .collect::<Vec<_>>()
65 .join("\n"))
66 }
67
68 pub fn grep(&self, pattern: &str, path: &Path, file_type: Option<&str>) -> Result<String> {
70 let mut cmd = Command::new(&self.st_binary);
71 cmd.arg("--search")
72 .arg(pattern)
73 .arg("--mode")
74 .arg("ai")
75 .arg("--depth")
76 .arg("0")
77 .arg(path);
78
79 if let Some(ft) = file_type {
80 cmd.arg("--type").arg(ft);
81 }
82
83 let output = cmd.output().context("Failed to run st for search")?;
84
85 Ok(String::from_utf8_lossy(&output.stdout).to_string())
86 }
87
88 pub fn glob(&self, pattern: &str, path: &Path) -> Result<String> {
90 let output = Command::new(&self.st_binary)
91 .arg("--find")
92 .arg(pattern)
93 .arg("--mode")
94 .arg("json")
95 .arg("--depth")
96 .arg("0")
97 .arg("--compact")
98 .arg(path)
99 .output()
100 .context("Failed to run st for glob")?;
101
102 Ok(String::from_utf8_lossy(&output.stdout).to_string())
103 }
104
105 pub fn analyze(&self, path: &Path, mode: &str, depth: usize) -> Result<String> {
107 let output = Command::new(&self.st_binary)
108 .arg("--mode")
109 .arg(mode)
110 .arg("--depth")
111 .arg(depth.to_string())
112 .arg(path)
113 .output()
114 .context("Failed to run st for analysis")?;
115
116 Ok(String::from_utf8_lossy(&output.stdout).to_string())
117 }
118
119 pub fn stats(&self, path: &Path) -> Result<String> {
121 let output = Command::new(&self.st_binary)
122 .arg("--mode")
123 .arg("stats")
124 .arg("--depth")
125 .arg("0")
126 .arg(path)
127 .output()
128 .context("Failed to run st for stats")?;
129
130 Ok(String::from_utf8_lossy(&output.stdout).to_string())
131 }
132
133 pub fn semantic_analyze(&self, path: &Path) -> Result<String> {
135 let output = Command::new(&self.st_binary)
136 .arg("--mode")
137 .arg("semantic")
138 .arg("--depth")
139 .arg("0")
140 .arg(path)
141 .output()
142 .context("Failed to run st for semantic analysis")?;
143
144 Ok(String::from_utf8_lossy(&output.stdout).to_string())
145 }
146
147 pub fn quick(&self, path: &Path) -> Result<String> {
149 let output = Command::new(&self.st_binary)
150 .arg("--mode")
151 .arg("summary-ai")
152 .arg("--depth")
153 .arg("3")
154 .arg(path)
155 .output()
156 .context("Failed to run st for quick view")?;
157
158 Ok(String::from_utf8_lossy(&output.stdout).to_string())
159 }
160
161 pub fn understand_project(&self, path: &Path) -> Result<String> {
163 let results = [
164 "=== QUICK OVERVIEW ===".to_string(),
165 self.quick(path)?,
166 "\n=== SEMANTIC GROUPS ===".to_string(),
167 self.semantic_analyze(path)?,
168 "\n=== STATISTICS ===".to_string(),
169 self.stats(path)?,
170 ];
171
172 Ok(results.join("\n"))
173 }
174}