1use std::env;
2use std::fs;
3use std::os::unix::fs::PermissionsExt;
4use std::process::Command;
5
6#[derive(Debug, Clone)]
7pub struct BentoCommand {
8 pub name: String,
9 pub category: String,
10}
11
12impl BentoCommand {
13 pub fn new(name: String, category: String) -> Self {
14 Self { name, category }
15 }
16}
17
18pub fn get_commands() -> Vec<BentoCommand> {
19 let mut commands = Vec::new();
20
21 if let Ok(path_var) = env::var("PATH") {
23 for dir in path_var.split(':') {
24 if let Ok(entries) = fs::read_dir(dir) {
25 for entry in entries.filter_map(Result::ok) {
26 if let Ok(metadata) = entry.metadata() {
27 if metadata.is_file() && (metadata.permissions().mode() & 0o111 != 0) {
28 if let Ok(name) = entry.file_name().into_string() {
29 commands.push(BentoCommand {
30 name,
31 category: "bin".to_string(),
32 });
33 }
34 }
35 }
36 }
37 }
38 }
39 }
40
41 if let Ok(output) = Command::new("brew").arg("list").arg("--formula").output() {
43 let stdout = String::from_utf8_lossy(&output.stdout);
44 for line in stdout.lines() {
45 let name = line.trim();
46 if !name.is_empty() {
47 commands.push(BentoCommand {
48 name: name.to_string(),
49 category: "homebrew".to_string(),
50 });
51 }
52 }
53 }
54
55 if let Ok(output) = Command::new("brew").arg("list").arg("--cask").output() {
57 let stdout = String::from_utf8_lossy(&output.stdout);
58 for line in stdout.lines() {
59 let name = line.trim();
60 if !name.is_empty() {
61 commands.push(BentoCommand {
62 name: name.to_string(),
63 category: "cask".to_string(),
64 });
65 }
66 }
67 }
68
69 if let Ok(output) = Command::new("pip")
71 .arg("list")
72 .arg("--format=freeze")
73 .output()
74 {
75 let stdout = String::from_utf8_lossy(&output.stdout);
76 for line in stdout.lines() {
77 if let Some(name) = line.split("==").next() {
78 commands.push(BentoCommand {
79 name: name.to_string(),
80 category: "pip".to_string(),
81 });
82 }
83 }
84 }
85
86 if let Ok(output) = Command::new("npm")
88 .arg("list")
89 .arg("-g")
90 .arg("--depth=0")
91 .arg("--parseable")
92 .output()
93 {
94 let stdout = String::from_utf8_lossy(&output.stdout);
95 for line in stdout.lines() {
96 if let Some(name) = line.split('/').last() {
97 if name != "lib" && !name.is_empty() {
98 commands.push(BentoCommand {
99 name: name.to_string(),
100 category: "npm".to_string(),
101 });
102 }
103 }
104 }
105 }
106
107 if let Ok(output) = Command::new("yarn").arg("global").arg("list").output() {
109 let stdout = String::from_utf8_lossy(&output.stdout);
110 for line in stdout.lines() {
111 if line.starts_with("info ") && line.contains("@") {
112 if let Some(name) = line.split("@").next() {
113 let clean_name = name.trim_start_matches("info ");
114 commands.push(BentoCommand {
115 name: clean_name.to_string(),
116 category: "yarn".to_string(),
117 });
118 }
119 }
120 }
121 }
122
123 if let Ok(output) = Command::new("cargo").arg("install").arg("--list").output() {
125 let stdout = String::from_utf8_lossy(&output.stdout);
126 for line in stdout.lines() {
127 if !line.starts_with(" ") && line.contains(" v") {
128 if let Some(name) = line.split(" v").next() {
129 commands.push(BentoCommand {
130 name: name.to_string(),
131 category: "cargo".to_string(),
132 });
133 }
134 }
135 }
136 }
137
138 if let Ok(output) = Command::new("go").arg("list").arg("-m").arg("all").output() {
140 let stdout = String::from_utf8_lossy(&output.stdout);
141 for line in stdout.lines() {
142 if let Some(name) = line.split_whitespace().next() {
143 if name.contains("/") {
144 if let Some(pkg_name) = name.split("/").last() {
145 commands.push(BentoCommand {
146 name: pkg_name.to_string(),
147 category: "go".to_string(),
148 });
149 }
150 }
151 }
152 }
153 }
154
155 for cmd in ["zsh -c 'alias'", "bash -c 'alias'", "sh -c 'alias'"] {
157 if let Ok(output) = Command::new("sh").arg("-c").arg(cmd).output() {
158 let stdout = String::from_utf8_lossy(&output.stdout);
159 for line in stdout.lines() {
160 if line.contains('=') && !line.trim().is_empty() {
161 if let Some(name) = line.split('=').next() {
162 let clean_name = name.trim_start_matches("alias ").trim();
163 if !clean_name.is_empty() && !clean_name.starts_with('-') {
164 commands.push(BentoCommand {
165 name: clean_name.to_string(),
166 category: "alias".to_string(),
167 });
168 }
169 }
170 }
171 }
172 if !stdout.is_empty() {
173 break;
174 }
175 }
176 }
177
178 for cmd in [
180 "zsh -c 'print -l ${(k)functions}'",
181 "bash -c 'declare -F | cut -d\" \" -f3'",
182 "zsh -c 'functions | grep \"^[a-zA-Z]\" | cut -d\" \" -f1'",
183 ] {
184 if let Ok(output) = Command::new("sh").arg("-c").arg(cmd).output() {
185 let stdout = String::from_utf8_lossy(&output.stdout);
186 for line in stdout.lines() {
187 let name = line.trim();
188 if !name.is_empty() && !name.contains(' ') && !name.starts_with('_') {
189 commands.push(BentoCommand {
190 name: name.to_string(),
191 category: "function".to_string(),
192 });
193 }
194 }
195 if !stdout.is_empty() {
196 break;
197 }
198 }
199 }
200
201 if let Ok(shell) = env::var("SHELL") {
203 if let Ok(output) = Command::new(&shell)
205 .arg("-i")
206 .arg("-c")
207 .arg("alias")
208 .output()
209 {
210 let stdout = String::from_utf8_lossy(&output.stdout);
211 for line in stdout.lines() {
212 if line.contains('=') && !line.trim().is_empty() {
213 if let Some(name) = line.split('=').next() {
214 let clean_name = name
215 .trim_start_matches("alias ")
216 .trim()
217 .trim_matches('\'')
218 .trim_matches('"');
219 if !clean_name.is_empty()
220 && clean_name
221 .chars()
222 .all(|c| c.is_alphanumeric() || "_-~.".contains(c))
223 {
224 commands.push(BentoCommand {
225 name: clean_name.to_string(),
226 category: "alias".to_string(),
227 });
228 }
229 }
230 }
231 }
232 }
233
234 if shell.contains("zsh") {
236 if let Ok(output) = Command::new(&shell)
237 .arg("-i")
238 .arg("-c")
239 .arg("print -l ${(k)functions}")
240 .output()
241 {
242 let stdout = String::from_utf8_lossy(&output.stdout);
243 for line in stdout.lines() {
244 let name = line.trim();
245 if !name.is_empty()
246 && !name.starts_with('_')
247 && name
248 .chars()
249 .all(|c| c.is_alphanumeric() || "_-".contains(c))
250 {
251 commands.push(BentoCommand {
252 name: name.to_string(),
253 category: "function".to_string(),
254 });
255 }
256 }
257 }
258 }
259 }
260
261 commands
262}
263
264pub fn fuzzy_match(query: &str, target: &str) -> usize {
265 let query = query.to_lowercase();
266 let target = target.to_lowercase();
267
268 if target.contains(&query) {
269 return query.len() * 2;
270 }
271
272 let mut score = 0;
273 let mut target_chars = target.chars().peekable();
274
275 for q_char in query.chars() {
276 while let Some(&t_char) = target_chars.peek() {
277 target_chars.next();
278 if q_char == t_char {
279 score += 1;
280 break;
281 }
282 }
283 }
284 score
285}