the second field of history entries in the hist file now contain command runtime in seconds, instead of an id

updated rustfmt.toml and formatted codebase
This commit is contained in:
2026-03-05 10:29:54 -05:00
parent 1efaf0e516
commit e07a853074
17 changed files with 522 additions and 477 deletions

View File

@@ -1,7 +1,5 @@
max_width = 100
tab_spaces = 2
edition = "2021"
edition = "2024"
newline_style = "Unix"
wrap_comments = true

View File

@@ -1404,7 +1404,6 @@ impl FromStr for ParamExp {
))
};
// Handle indirect var expansion: ${!var}
if let Some(var) = s.strip_prefix('!') {
if var.ends_with('*') || var.ends_with('@') {

View File

@@ -37,7 +37,7 @@ use crate::prelude::*;
use crate::readline::term::{LineWriter, RawModeGuard, raw_mode};
use crate::readline::{Prompt, ReadlineEvent, ShedVi};
use crate::signal::{GOT_SIGWINCH, JOB_DONE, QUIT_CODE, check_signals, sig_setup, signals_pending};
use crate::state::{AutoCmdKind, read_logic, source_rc, write_jobs, write_meta};
use crate::state::{AutoCmdKind, read_logic, read_shopts, source_rc, write_jobs, write_meta};
use clap::Parser;
use state::{read_vars, write_vars};
@@ -292,36 +292,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
let action = km.action_expanded();
readline.pending_keymap.clear();
for key in action {
if let Some(event) = readline.handle_key(key)? {
match event {
ReadlineEvent::Line(input) => {
let start = Instant::now();
write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| {
exec_input(input, None, true, Some("<stdin>".into()))
}) {
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
}
_ => e.print_error(),
}
}
let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer());
readline.fix_column()?;
readline.writer.flush_write("\n\r")?;
readline.reset(true)?;
break;
}
ReadlineEvent::Eof => {
QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(());
}
ReadlineEvent::Pending => {}
}
let event = readline.handle_key(key).transpose();
if let Some(event) = event {
handle_readline_event(&mut readline, event)?;
}
}
} else {
@@ -331,36 +304,9 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
);
let buffered = std::mem::take(&mut readline.pending_keymap);
for key in buffered {
if let Some(event) = readline.handle_key(key)? {
match event {
ReadlineEvent::Line(input) => {
let start = Instant::now();
write_meta(|m| m.start_timer());
if let Err(e) = RawModeGuard::with_cooked_mode(|| {
exec_input(input, None, true, Some("<stdin>".into()))
}) {
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
}
_ => e.print_error(),
}
}
let command_run_time = start.elapsed();
log::info!("Command executed in {:.2?}", command_run_time);
write_meta(|m| m.stop_timer());
readline.fix_column()?;
readline.writer.flush_write("\n\r")?;
readline.reset(true)?;
break;
}
ReadlineEvent::Eof => {
QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(());
}
ReadlineEvent::Pending => {}
}
let event = readline.handle_key(key).transpose();
if let Some(event) = event {
handle_readline_event(&mut readline, event)?;
}
}
}
@@ -394,7 +340,18 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
}
// Process any available input
match readline.process_input() {
let event = readline.process_input();
match handle_readline_event(&mut readline, event)? {
true => return Ok(()),
false => { /* continue looping */ }
}
}
Ok(())
}
fn handle_readline_event(readline: &mut ShedVi, event: ShResult<ReadlineEvent>) -> ShResult<bool> {
match event {
Ok(ReadlineEvent::Line(input)) => {
let pre_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PreCmd));
let post_exec = read_logic(|l| l.get_autocmds(AutoCmdKind::PostCmd));
@@ -409,7 +366,7 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
return Ok(true);
}
_ => e.print_error(),
}
@@ -420,6 +377,11 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
post_exec.exec_with(&input);
if read_shopts(|s| s.core.auto_hist) && !input.is_empty() {
readline.history.push(input.clone());
readline.history.save()?;
}
readline.fix_column()?;
readline.writer.flush_write("\n\r")?;
@@ -428,24 +390,26 @@ fn shed_interactive(args: ShedArgs) -> ShResult<()> {
let real_end = start.elapsed();
log::info!("Total round trip time: {:.2?}", real_end);
Ok(false)
}
Ok(ReadlineEvent::Eof) => {
// Ctrl+D on empty line
QUIT_CODE.store(0, Ordering::SeqCst);
return Ok(());
Ok(true)
}
Ok(ReadlineEvent::Pending) => {
// No complete input yet, keep polling
Ok(false)
}
Err(e) => match e.kind() {
ShErrKind::CleanExit(code) => {
QUIT_CODE.store(*code, Ordering::SeqCst);
return Ok(());
Ok(true)
}
_ => {
e.print_error();
Ok(false)
}
_ => e.print_error(),
},
}
}
Ok(())
}

View File

@@ -92,7 +92,7 @@ impl IoMode {
Ok(self)
}
pub fn get_pipes() -> (Self, Self) {
let (rpipe, wpipe) = pipe().unwrap();
let (rpipe, wpipe) = nix::unistd::pipe2(OFlag::O_CLOEXEC).unwrap();
(
Self::Pipe {
tgt_fd: STDIN_FILENO,
@@ -220,6 +220,12 @@ impl<'e> IoFrame {
let tgt_fd = io_mode.tgt_fd();
let src_fd = io_mode.src_fd();
dup2(src_fd, tgt_fd)?;
// Close the original pipe fd after dup2 — it's been duplicated to
// tgt_fd and keeping it open prevents SIGPIPE delivery in pipelines.
// We replace the IoMode to drop the Arc<OwnedFd>, which closes the fd.
if matches!(io_mode, IoMode::Pipe { .. }) {
*io_mode = IoMode::Close { tgt_fd };
}
}
Ok(RedirGuard::new(self))
}

View File

@@ -812,7 +812,9 @@ impl FuzzySelector {
}
fn candidate_height(&self, idx: usize) -> usize {
self.filtered.get(idx)
self
.filtered
.get(idx)
.map(|c| c.content.trim_end().lines().count().max(1))
.unwrap_or(1)
}
@@ -930,7 +932,13 @@ impl FuzzySelector {
let title = self.title.clone();
let title_width = title.len() as u16;
let number_candidates = self.number_candidates;
let min_pad = self.candidates.len().to_string().len().saturating_add(1).max(6);
let min_pad = self
.candidates
.len()
.to_string()
.len()
.saturating_add(1)
.max(6);
let max_height = self.max_height;
let visible = self.get_window();
let mut rows: u16 = 0;
@@ -995,10 +1003,22 @@ impl FuzzySelector {
if !drew_number {
let this_num = i + offset + 1;
let right_pad = " ".repeat(min_pad.saturating_sub(this_num.to_string().len()));
format!("{} {}\x1b[33m{}\x1b[39m{right_pad}{}\x1b[0m", Self::VERT_LINE, &selector,i + offset + 1, &line)
format!(
"{} {}\x1b[33m{}\x1b[39m{right_pad}{}\x1b[0m",
Self::VERT_LINE,
&selector,
i + offset + 1,
&line
)
} else {
let right_pad = " ".repeat(min_pad);
format!("{} {}{}{}\x1b[0m", Self::VERT_LINE, &selector,right_pad, &line)
format!(
"{} {}{}{}\x1b[0m",
Self::VERT_LINE,
&selector,
right_pad,
&line
)
}
} else {
format!("{} {}{}\x1b[0m", Self::VERT_LINE, &selector, &line)
@@ -1100,7 +1120,9 @@ impl Default for FuzzyCompleter {
impl Completer for FuzzyCompleter {
fn set_prompt_line_context(&mut self, line_width: u16, cursor_col: u16) {
self.selector.set_prompt_line_context(line_width, cursor_col);
self
.selector
.set_prompt_line_context(line_width, cursor_col);
}
fn reset_stay_active(&mut self) {
self.selector.reset_stay_active();

View File

@@ -1,10 +1,19 @@
use std::{
cmp::Ordering, collections::HashSet, env, fmt::{Display, Write}, fs::{self, OpenOptions}, io::Write as IoWrite, path::{Path, PathBuf}, str::FromStr, time::{Duration, SystemTime, UNIX_EPOCH}
cmp::Ordering,
collections::HashSet,
env,
fmt::{Display, Write},
fs::{self, OpenOptions},
io::Write as IoWrite,
path::{Path, PathBuf},
str::FromStr,
time::{Duration, SystemTime, UNIX_EPOCH},
};
use crate::{
libsh::error::{ShErr, ShErrKind, ShResult},
readline::{complete::FuzzySelector, linebuf::LineBuf},
state::read_meta,
};
#[derive(Default, Clone, Copy, Debug)]
@@ -28,16 +37,13 @@ impl SearchConstraint {
#[derive(Debug, Clone)]
pub struct HistEntry {
id: u32,
runtime: Duration,
timestamp: SystemTime,
command: String,
new: bool,
}
impl HistEntry {
pub fn id(&self) -> u32 {
self.id
}
pub fn timestamp(&self) -> &SystemTime {
&self.timestamp
}
@@ -73,24 +79,25 @@ impl FromStr for HistEntry {
return err;
};
//248972349;148;echo foo; echo bar
let Some((timestamp, id_and_command)) = cleaned.split_once(';') else {
let Some((timestamp, runtime_and_cmd)) = cleaned.split_once(';') else {
return err;
};
//("248972349","148;echo foo; echo bar")
let Some((id, command)) = id_and_command.split_once(';') else {
let Some((runtime, command)) = runtime_and_cmd.split_once(';') else {
return err;
};
//("148","echo foo; echo bar")
let Ok(ts_seconds) = timestamp.parse::<u64>() else {
return err;
};
let Ok(id) = id.parse::<u32>() else {
let Ok(runtime) = runtime.parse::<u64>() else {
return err;
};
let runtime = Duration::from_secs(runtime);
let timestamp = UNIX_EPOCH + Duration::from_secs(ts_seconds);
let command = command.to_string();
Ok(Self {
id,
runtime,
timestamp,
command,
new: false,
@@ -103,13 +110,14 @@ impl Display for HistEntry {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let command = self.with_escaped_newlines();
let HistEntry {
id,
runtime,
timestamp,
command: _,
new: _,
} = self;
let timestamp = timestamp.duration_since(UNIX_EPOCH).unwrap().as_secs();
writeln!(f, ": {timestamp};{id};{command}")
let runtime = runtime.as_secs();
writeln!(f, ": {timestamp};{runtime};{command}")
}
}
@@ -223,7 +231,11 @@ impl History {
}
let search_mask = dedupe_entries(&entries);
let cursor = search_mask.len();
let max_size = if max_hist < 0 { None } else { Some(max_hist as u32) };
let max_size = if max_hist < 0 {
None
} else {
Some(max_hist as u32)
};
Ok(Self {
path,
entries,
@@ -245,7 +257,10 @@ impl History {
Some(self.search_mask[0].command().to_string())
} else {
self.fuzzy_finder.set_query(initial.to_string());
let raw_entries = self.search_mask.clone().into_iter()
let raw_entries = self
.search_mask
.clone()
.into_iter()
.map(|ent| ent.command().to_string());
self.fuzzy_finder.activate(raw_entries.collect());
None
@@ -307,7 +322,9 @@ impl History {
pub fn resolve_hist_token(&self, token: &str) -> Option<String> {
let token = token.strip_prefix('!').unwrap_or(token).to_string();
if let Ok(num) = token.parse::<i32>() && num != 0 {
if let Ok(num) = token.parse::<i32>()
&& num != 0
{
match num.cmp(&0) {
Ordering::Less => {
if num.unsigned_abs() > self.entries.len() as u32 {
@@ -315,14 +332,13 @@ impl History {
}
let rev_idx = self.entries.len() - num.unsigned_abs() as usize;
self.entries.get(rev_idx)
.map(|e| e.command().to_string())
self.entries.get(rev_idx).map(|e| e.command().to_string())
}
Ordering::Greater => {
self.entries.get(num as usize)
.map(|e| e.command().to_string())
}
_ => unreachable!()
Ordering::Greater => self
.entries
.get(num as usize)
.map(|e| e.command().to_string()),
_ => unreachable!(),
}
} else {
let mut rev_search = self.entries.iter();
@@ -332,13 +348,6 @@ impl History {
}
}
pub fn get_new_id(&self) -> u32 {
let Some(ent) = self.entries.last() else {
return 0;
};
ent.id + 1
}
pub fn ignore_dups(&mut self, yn: bool) {
self.ignore_dups = yn
}
@@ -401,12 +410,12 @@ impl History {
pub fn push(&mut self, command: String) {
let timestamp = SystemTime::now();
let id = self.get_new_id();
let runtime = read_meta(|m| m.get_time()).unwrap_or_default();
if self.ignore_dups && self.is_dup(&command) {
return;
}
self.entries.push(HistEntry {
id,
runtime,
timestamp,
command,
new: true,

View File

@@ -19,7 +19,10 @@ use crate::{
},
prelude::*,
readline::{
history::History, markers, register::{RegisterContent, write_register}, term::RawModeGuard
history::History,
markers,
register::{RegisterContent, write_register},
term::RawModeGuard,
},
state::{VarFlags, VarKind, read_shopts, write_meta, write_vars},
};
@@ -3335,7 +3338,8 @@ impl LineBuf {
while let Some((i, gr)) = graphemes.next() {
match gr {
"\\" => {
"\\" | "$" => {
// skip on dollars because '$!' is a shell parameter
graphemes.next();
}
"'" => qt_state.toggle_single(),
@@ -3362,7 +3366,9 @@ impl LineBuf {
Some((j, gr)) if !is_whitespace(gr) => {
let mut end = j + gr.len();
while let Some((k, gr2)) = graphemes.next() {
if is_whitespace(gr2) { break; }
if is_whitespace(gr2) {
break;
}
end = k + gr2.len();
}
let token = &self.buffer[j..end];

View File

@@ -16,7 +16,8 @@ use crate::readline::complete::{FuzzyCompleter, SelectorResponse};
use crate::readline::term::{Pos, TermReader, calc_str_width};
use crate::readline::vimode::{ViEx, ViVerbatim};
use crate::state::{
AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta, write_vars
AutoCmdKind, ShellParam, VarFlags, VarKind, read_logic, read_shopts, with_vars, write_meta,
write_vars,
};
use crate::{
libsh::error::ShResult,
@@ -413,7 +414,9 @@ impl ShedVi {
self.editor.set_buffer(cmd.to_string());
self.editor.move_cursor_to_end();
self.history.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self.editor.set_hint(None);
self.history.fuzzy_finder.clear(&mut self.writer)?;
self.history.fuzzy_finder.reset();
@@ -422,7 +425,14 @@ impl ShedVi {
post_cmds.exec_with(&cmd);
});
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
self.needs_redraw = true;
continue;
@@ -433,7 +443,14 @@ impl ShedVi {
self.editor.set_hint(None);
self.history.fuzzy_finder.clear(&mut self.writer)?;
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
self.needs_redraw = true;
continue;
@@ -481,7 +498,14 @@ impl ShedVi {
let hint = self.history.get_hint();
self.editor.set_hint(hint);
self.completer.clear(&mut self.writer)?;
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
self.completer.reset();
continue;
@@ -532,7 +556,14 @@ impl ShedVi {
}
}
if !self.completer.is_active() && !self.history.fuzzy_finder.is_active() {
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str(self.mode.report_mode().to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str(self.mode.report_mode().to_string()),
VarFlags::NONE,
)
})
.ok();
}
// Redraw if we processed any input
@@ -546,7 +577,10 @@ impl ShedVi {
pub fn handle_key(&mut self, key: KeyEvent) -> ShResult<Option<ReadlineEvent>> {
if self.should_accept_hint(&key) {
log::debug!("Accepting hint on key {key:?} in mode {:?}", self.mode.report_mode());
log::debug!(
"Accepting hint on key {key:?} in mode {:?}",
self.mode.report_mode()
);
self.editor.accept_hint();
if !self.history.at_pending() {
self.history.reset_to_pending();
@@ -605,8 +639,6 @@ impl ShedVi {
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
let hint = self.history.get_hint();
self.editor.set_hint(hint);
}
Ok(None) => {
let post_cmds = read_logic(|l| l.get_autocmds(AutoCmdKind::OnCompletionStart));
@@ -615,7 +647,14 @@ impl ShedVi {
self.writer.send_bell().ok();
if self.completer.is_active() {
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str("COMPLETE".to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str("COMPLETE".to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
self.needs_redraw = true;
self.editor.set_hint(None);
@@ -636,7 +675,9 @@ impl ShedVi {
self.editor.set_buffer(entry);
self.editor.move_cursor_to_end();
self.history.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self
.history
.update_pending_cmd((self.editor.as_str(), self.editor.cursor.get()));
self.editor.set_hint(None);
}
None => {
@@ -645,7 +686,14 @@ impl ShedVi {
self.writer.send_bell().ok();
if self.history.fuzzy_finder.is_active() {
write_vars(|v| v.set_var("SHED_VI_MODE", VarKind::Str("SEARCH".to_string()), VarFlags::NONE)).ok();
write_vars(|v| {
v.set_var(
"SHED_VI_MODE",
VarKind::Str("SEARCH".to_string()),
VarFlags::NONE,
)
})
.ok();
self.prompt.refresh();
self.needs_redraw = true;
self.editor.set_hint(None);
@@ -695,12 +743,6 @@ impl ShedVi {
self.print_line(true)?;
self.writer.flush_write("\n")?;
let buf = self.editor.take_buf();
if read_shopts(|s| s.core.auto_hist) && !buf.is_empty() {
self.history.push(buf.clone());
if let Err(e) = self.history.save() {
eprintln!("Failed to save history: {e}");
}
}
self.history.reset();
return Ok(Some(ReadlineEvent::Line(buf)));
}
@@ -942,7 +984,10 @@ impl ShedVi {
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self.completer.draw(&mut self.writer)?;
self.history.fuzzy_finder.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self
.history
.fuzzy_finder
.set_prompt_line_context(preceding_width, new_layout.cursor.col);
self.history.fuzzy_finder.draw(&mut self.writer)?;
self.old_layout = Some(new_layout);

View File

@@ -54,15 +54,13 @@ impl ViMode for ViInsert {
.set_motion(MotionCmd(1, Motion::ForwardChar));
self.register_and_return()
}
E(K::ExMode, _) => {
Some(ViCmd {
E(K::ExMode, _) => Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::ExMode)),
motion: None,
raw_seq: String::new(),
flags: Default::default(),
})
}
}),
E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(

View File

@@ -41,15 +41,13 @@ impl ViMode for ViReplace {
.set_motion(MotionCmd(1, Motion::ForwardChar));
self.register_and_return()
}
E(K::ExMode, _) => {
Some(ViCmd {
E(K::ExMode, _) => Some(ViCmd {
register: Default::default(),
verb: Some(VerbCmd(1, Verb::ExMode)),
motion: None,
raw_seq: String::new(),
flags: Default::default(),
})
}
}),
E(K::Char('W'), M::CTRL) => {
self.pending_cmd.set_verb(VerbCmd(1, Verb::Delete));
self.pending_cmd.set_motion(MotionCmd(