Skip to content

Commit cfcac8a

Browse files
Add command history search with PiP mode (Cmd+R)
- Rust backend reads and parses ~/.zsh_history and ~/.bash_history with deduplication and timestamps - Fuzzy search with match highlighting and infinite scroll pagination - Draggable, resizable floating modal with Picture-in-Picture mode for side-by-side terminal use - Selected command pastes into terminal for review before execution - Full dark/light mode styling and keyboard navigation Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent ece13d5 commit cfcac8a

6 files changed

Lines changed: 906 additions & 0 deletions

File tree

‎src-tauri/src/commands/autocomplete.rs‎

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -141,3 +141,119 @@ pub fn list_path_completions(
141141
pub fn get_home_dir() -> String {
142142
std::env::var("HOME").unwrap_or_else(|_| String::from("/tmp"))
143143
}
144+
145+
#[derive(Debug, Clone, Serialize)]
146+
#[serde(rename_all = "camelCase")]
147+
pub struct HistoryEntry {
148+
pub command: String,
149+
pub timestamp: Option<u64>,
150+
}
151+
152+
#[tauri::command]
153+
pub fn read_shell_history(max_entries: Option<usize>) -> Vec<HistoryEntry> {
154+
let home = std::env::var("HOME").unwrap_or_default();
155+
if home.is_empty() {
156+
return vec![];
157+
}
158+
159+
let limit = max_entries.unwrap_or(2000);
160+
161+
// Try zsh first, then bash
162+
let zsh_path = PathBuf::from(&home).join(".zsh_history");
163+
let bash_path = PathBuf::from(&home).join(".bash_history");
164+
165+
if zsh_path.exists() {
166+
parse_zsh_history(&zsh_path, limit)
167+
} else if bash_path.exists() {
168+
parse_bash_history(&bash_path, limit)
169+
} else {
170+
vec![]
171+
}
172+
}
173+
174+
fn parse_zsh_history(path: &PathBuf, limit: usize) -> Vec<HistoryEntry> {
175+
let data = match fs::read(path) {
176+
Ok(d) => d,
177+
Err(_) => return vec![],
178+
};
179+
let content = String::from_utf8_lossy(&data);
180+
181+
let mut entries: Vec<HistoryEntry> = Vec::new();
182+
let mut seen = std::collections::HashSet::new();
183+
184+
for line in content.lines().rev() {
185+
let line = line.trim();
186+
if line.is_empty() {
187+
continue;
188+
}
189+
190+
let (timestamp, command) = if line.starts_with(": ") {
191+
// Extended format: ": 1234567890:0;command here"
192+
if let Some(semi) = line.find(';') {
193+
let meta = &line[2..semi];
194+
let ts = meta.split(':').next()
195+
.and_then(|s| s.trim().parse::<u64>().ok());
196+
(ts, line[semi + 1..].to_string())
197+
} else {
198+
(None, line.to_string())
199+
}
200+
} else {
201+
(None, line.to_string())
202+
};
203+
204+
let cmd = command.trim().to_string();
205+
if cmd.is_empty() || cmd.len() < 2 {
206+
continue;
207+
}
208+
209+
if seen.contains(&cmd) {
210+
continue;
211+
}
212+
seen.insert(cmd.clone());
213+
214+
entries.push(HistoryEntry {
215+
command: cmd,
216+
timestamp,
217+
});
218+
219+
if entries.len() >= limit {
220+
break;
221+
}
222+
}
223+
224+
entries
225+
}
226+
227+
fn parse_bash_history(path: &PathBuf, limit: usize) -> Vec<HistoryEntry> {
228+
let data = match fs::read(path) {
229+
Ok(d) => d,
230+
Err(_) => return vec![],
231+
};
232+
let content = String::from_utf8_lossy(&data);
233+
234+
let mut entries: Vec<HistoryEntry> = Vec::new();
235+
let mut seen = std::collections::HashSet::new();
236+
237+
for line in content.lines().rev() {
238+
let cmd = line.trim().to_string();
239+
if cmd.is_empty() || cmd.len() < 2 || cmd.starts_with('#') {
240+
continue;
241+
}
242+
243+
if seen.contains(&cmd) {
244+
continue;
245+
}
246+
seen.insert(cmd.clone());
247+
248+
entries.push(HistoryEntry {
249+
command: cmd,
250+
timestamp: None,
251+
});
252+
253+
if entries.len() >= limit {
254+
break;
255+
}
256+
}
257+
258+
entries
259+
}

‎src-tauri/src/lib.rs‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ pub fn run() {
2222
commands::terminal::get_default_working_directory,
2323
commands::autocomplete::list_path_completions,
2424
commands::autocomplete::get_home_dir,
25+
commands::autocomplete::read_shell_history,
2526
commands::shell_exec::execute_shell_command,
2627
commands::specs::list_installed_specs,
2728
commands::specs::read_spec_file,

‎src/components/Settings/Settings.tsx‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,6 +724,7 @@ function ShortcutsTab() {
724724
icon: <Command size={13} />,
725725
shortcuts: [
726726
{ keys: ["⌘", "F"], description: "Find in terminal" },
727+
{ keys: ["⌘", "R"], description: "Search command history" },
727728
{ keys: ["⌘", "K"], description: "Clear terminal" },
728729
{ keys: ["⌘", "C"], description: "Copy selection" },
729730
{ keys: ["⌘", "V"], description: "Paste from clipboard" },

0 commit comments

Comments
 (0)