bento/
lib.rs

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    // Bin commands from PATH
22    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    // Homebrew packages
42    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    // Homebrew casks
56    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    // Python packages (pip)
70    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    // Node packages (npm global)
87    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    // Yarn global packages
108    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    // Rust packages (cargo)
124    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    // Go packages
139    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    // Aliases - try multiple shell methods
156    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    // Functions - try multiple methods
179    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    // Direct shell execution for current shell
202    if let Ok(shell) = env::var("SHELL") {
203        // Get aliases from current shell
204        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        // Get functions from current shell
235        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}