1use anyhow::{bail, Context, Result};
5use std::path::{Path, PathBuf};
6use std::process::Command;
7
8#[derive(Debug, Clone)]
10pub struct StToolsConfig {
11 pub st_binary: PathBuf,
13 pub default_mode: String,
15 pub use_emoji: bool,
17 pub compress: bool,
19}
20
21impl Default for StToolsConfig {
22 fn default() -> Self {
23 Self {
24 st_binary: std::env::current_exe()
25 .ok()
26 .and_then(|p| {
27 let dir = p.parent()?;
28 let st = dir.join("st");
29 if st.exists() {
30 Some(st)
31 } else {
32 None
33 }
34 })
35 .unwrap_or_else(|| PathBuf::from("./target/release/st")),
36 default_mode: "ai".to_string(),
37 use_emoji: false,
38 compress: false,
39 }
40 }
41}
42
43pub struct StOnlyTools {
45 config: StToolsConfig,
46}
47
48impl Default for StOnlyTools {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl StOnlyTools {
55 pub fn new() -> Self {
56 Self {
57 config: StToolsConfig::default(),
58 }
59 }
60
61 pub fn with_config(config: StToolsConfig) -> Self {
62 Self { config }
63 }
64
65 fn run_st(&self, args: Vec<String>) -> Result<String> {
67 let mut cmd = Command::new(&self.config.st_binary);
68
69 if !self.config.use_emoji {
71 cmd.arg("--no-emoji");
72 }
73 if self.config.compress {
74 cmd.arg("--compress");
75 }
76
77 for arg in args {
79 cmd.arg(arg);
80 }
81
82 let output = cmd.output().context("Failed to execute st")?;
83
84 if !output.status.success() {
85 bail!("ST failed: {}", String::from_utf8_lossy(&output.stderr));
86 }
87
88 Ok(String::from_utf8_lossy(&output.stdout).to_string())
89 }
90
91 pub fn list(&self, path: &Path, options: ListOptions) -> Result<String> {
93 let mut args = vec![
94 "--mode".to_string(),
95 "ls".to_string(),
96 "--depth".to_string(),
97 "1".to_string(),
98 ];
99
100 if let Some(pattern) = &options.pattern {
101 args.push("--find".to_string());
102 args.push(pattern.clone());
103 }
104
105 if let Some(file_type) = &options.file_type {
106 args.push("--type".to_string());
107 args.push(file_type.clone());
108 }
109
110 if let Some(sort) = &options.sort {
111 args.push("--sort".to_string());
112 args.push(sort.clone());
113 }
114
115 if let Some(limit) = options.limit {
116 args.push("--top".to_string());
117 args.push(limit.to_string());
118 }
119
120 args.push(path.to_str().unwrap().to_string());
121
122 self.run_st(args)
123 }
124
125 pub fn search(&self, pattern: &str, path: &Path, options: SearchOptions) -> Result<String> {
127 let mut args = vec![
128 "--search".to_string(),
129 pattern.to_string(),
130 "--mode".to_string(),
131 "ai".to_string(),
132 "--depth".to_string(),
133 "0".to_string(),
134 ];
135
136 if let Some(file_type) = &options.file_type {
137 args.push("--type".to_string());
138 args.push(file_type.clone());
139 }
140
141 args.push(path.to_str().unwrap().to_string());
142
143 self.run_st(args)
144 }
145
146 pub fn overview(&self, path: &Path, depth: Option<usize>) -> Result<String> {
148 let args = vec![
149 "--mode".to_string(),
150 "summary-ai".to_string(),
151 "--depth".to_string(),
152 depth.unwrap_or(0).to_string(),
153 path.to_str().unwrap().to_string(),
154 ];
155
156 self.run_st(args)
157 }
158
159 pub fn stats(&self, path: &Path) -> Result<String> {
161 self.run_st(vec![
162 "--mode".to_string(),
163 "stats".to_string(),
164 "--depth".to_string(),
165 "0".to_string(),
166 path.to_str().unwrap().to_string(),
167 ])
168 }
169
170 pub fn semantic(&self, path: &Path) -> Result<String> {
172 self.run_st(vec![
173 "--mode".to_string(),
174 "semantic".to_string(),
175 "--depth".to_string(),
176 "0".to_string(),
177 path.to_str().unwrap().to_string(),
178 ])
179 }
180}
181
182#[derive(Default, Clone)]
184pub struct ListOptions {
185 pub pattern: Option<String>,
186 pub file_type: Option<String>,
187 pub sort: Option<String>,
188 pub limit: Option<usize>,
189}
190
191#[derive(Default, Clone)]
192pub struct SearchOptions {
193 pub file_type: Option<String>,
194 pub show_line_numbers: bool,
195 pub case_sensitive: bool,
196}