diff --git a/src/prompt/mod.rs b/src/prompt/mod.rs index 8522f78..668b00f 100644 --- a/src/prompt/mod.rs +++ b/src/prompt/mod.rs @@ -1,5 +1,6 @@ pub mod highlight; pub mod readline; +pub mod statusline; use readline::{FernVi, Readline}; diff --git a/src/prompt/readline/history.rs b/src/prompt/readline/history.rs index 0d8a3f5..4e2007d 100644 --- a/src/prompt/readline/history.rs +++ b/src/prompt/readline/history.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashSet, env, fmt::{Display, Write}, fs::{self, OpenOptions}, @@ -194,6 +195,22 @@ fn read_hist_file(path: &Path) -> ShResult> { Ok(raw.parse::()?.0) } +/// Deduplicate entries, keeping only the most recent occurrence of each command. +/// Preserves chronological order (oldest to newest). +fn dedupe_entries(entries: &[HistEntry]) -> Vec { + let mut seen = HashSet::new(); + // Iterate backwards (newest first), keeping first occurrence of each command + entries + .iter() + .rev() + .filter(|ent| seen.insert(ent.command.clone())) + .cloned() + .collect::>() + .into_iter() + .rev() // Restore chronological order + .collect() +} + pub struct History { path: PathBuf, entries: Vec, @@ -222,7 +239,7 @@ impl History { new: true, }) } - let search_mask = entries.clone(); + let search_mask = dedupe_entries(&entries); let cursor = entries.len() - 1; let mut new = Self { path, @@ -288,15 +305,16 @@ impl History { match kind { SearchKind::Prefix => { if term.is_empty() { - self.search_mask = self.entries.clone(); + self.search_mask = dedupe_entries(&self.entries); } else { - let filtered = self + let filtered: Vec<_> = self .entries - .clone() - .into_iter() - .filter(|ent| ent.command().starts_with(&term)); + .iter() + .filter(|ent| ent.command().starts_with(&term)) + .cloned() + .collect(); - self.search_mask = filtered.collect(); + self.search_mask = dedupe_entries(&filtered); } self.cursor = self.search_mask.len().saturating_sub(1); } diff --git a/src/prompt/readline/linebuf.rs b/src/prompt/readline/linebuf.rs index ac4a273..3f14e21 100644 --- a/src/prompt/readline/linebuf.rs +++ b/src/prompt/readline/linebuf.rs @@ -362,6 +362,8 @@ impl LineBuf { let hint = hint.strip_prefix(&self.buffer).unwrap(); // If this ever panics, I will eat my hat if !hint.is_empty() { self.hint = Some(hint.to_string()) + } else { + self.hint = None } } else { self.hint = None diff --git a/src/prompt/readline/mod.rs b/src/prompt/readline/mod.rs index 09eb835..7d77082 100644 --- a/src/prompt/readline/mod.rs +++ b/src/prompt/readline/mod.rs @@ -57,7 +57,6 @@ impl Readline for FernVi { Err(_) | Ok(None) => { flog!(DEBUG, "EOF detected"); raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; - std::mem::drop(raw_mode_guard); return Err(ShErr::simple(ShErrKind::ReadlineErr, "EOF")); } @@ -68,7 +67,6 @@ impl Readline for FernVi { if self.should_accept_hint(&key) { self.editor.accept_hint(); self.history.update_pending_cmd(self.editor.as_str()); - self.print_line()?; continue; } @@ -81,19 +79,22 @@ impl Readline for FernVi { if self.should_grab_history(&cmd) { self.scroll_history(cmd); - self.print_line()?; continue; } if cmd.should_submit() { raw_mode_guard.disable_for(|| self.writer.flush_write("\n"))?; - std::mem::drop(raw_mode_guard); - return Ok(self.editor.take_buf()); + let buf = self.editor.take_buf(); + // Save command to history + self.history.push(buf.clone()); + if let Err(e) = self.history.save() { + eprintln!("Failed to save history: {e}"); + } + return Ok(buf); } if cmd.verb().is_some_and(|v| v.1 == Verb::EndOfFile) { if self.editor.buffer.is_empty() { - std::mem::drop(raw_mode_guard); return Err(ShErr::simple(ShErrKind::CleanExit(0), "exit")); } else { self.editor.buffer.clear(); diff --git a/src/prompt/readline/term.rs b/src/prompt/readline/term.rs index 81b563b..adb8e91 100644 --- a/src/prompt/readline/term.rs +++ b/src/prompt/readline/term.rs @@ -36,6 +36,9 @@ pub fn raw_mode() -> RawModeGuard { &raw, ) .expect("Failed to set terminal to raw mode"); + + let (cols, rows) = get_win_size(STDIN_FILENO); + RawModeGuard { orig, fd: STDIN_FILENO, diff --git a/src/prompt/statusline.rs b/src/prompt/statusline.rs new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/src/prompt/statusline.rs @@ -0,0 +1 @@ + diff --git a/src/signal.rs b/src/signal.rs index 51481f1..a2e16c8 100644 --- a/src/signal.rs +++ b/src/signal.rs @@ -103,6 +103,11 @@ pub fn sig_setup() { flags, SigSet::empty(), ), + SigAction::new( + SigHandler::Handler(handle_sigwinch), + flags, + SigSet::empty(), + ), ]; @@ -114,9 +119,19 @@ pub fn sig_setup() { sigaction(Signal::SIGINT, &actions[4]).unwrap(); sigaction(Signal::SIGTTIN, &actions[5]).unwrap(); sigaction(Signal::SIGTTOU, &actions[6]).unwrap(); + sigaction(Signal::SIGWINCH, &actions[7]).unwrap(); } } +extern "C" fn handle_sigwinch(_: libc::c_int) { + /* do nothing + * this exists for the sole purpose of interrupting readline + * readline will be refreshed after the interruption, + * which will cause window size calculations to be re-run + * and we get window resize handling for free as a result + */ +} + extern "C" fn handle_sighup(_: libc::c_int) { GOT_SIGHUP.store(true, Ordering::SeqCst); SHOULD_QUIT.store(true, Ordering::SeqCst);