add a timer to running exec commands (#2321)

sometimes i switch back to codex and i don't know how long a command has
been running.

<img width="744" height="462" alt="Screenshot 2025-08-14 at 3 30 07 PM"
src="https://github.com/user-attachments/assets/bd80947f-5a47-43e6-ad19-69c2995a2a29"
/>
This commit is contained in:
Jeremy Rose
2025-08-14 19:32:45 -04:00
committed by GitHub
parent 6a0f709cff
commit 235987843c

View File

@@ -35,6 +35,7 @@ use std::collections::HashMap;
use std::io::Cursor; use std::io::Cursor;
use std::path::PathBuf; use std::path::PathBuf;
use std::time::Duration; use std::time::Duration;
use std::time::Instant;
use tracing::error; use tracing::error;
#[derive(Clone)] #[derive(Clone)]
@@ -78,10 +79,16 @@ pub(crate) struct ExecCell {
pub(crate) command: Vec<String>, pub(crate) command: Vec<String>,
pub(crate) parsed: Vec<ParsedCommand>, pub(crate) parsed: Vec<ParsedCommand>,
pub(crate) output: Option<CommandOutput>, pub(crate) output: Option<CommandOutput>,
start_time: Option<Instant>,
} }
impl HistoryCell for ExecCell { impl HistoryCell for ExecCell {
fn display_lines(&self) -> Vec<Line<'static>> { fn display_lines(&self) -> Vec<Line<'static>> {
exec_command_lines(&self.command, &self.parsed, self.output.as_ref()) exec_command_lines(
&self.command,
&self.parsed,
self.output.as_ref(),
self.start_time,
)
} }
} }
@@ -199,49 +206,66 @@ pub(crate) fn new_active_exec_command(
command: Vec<String>, command: Vec<String>,
parsed: Vec<ParsedCommand>, parsed: Vec<ParsedCommand>,
) -> ExecCell { ) -> ExecCell {
new_exec_cell(command, parsed, None) ExecCell {
command,
parsed,
output: None,
start_time: Some(Instant::now()),
}
} }
pub(crate) fn new_completed_exec_command( pub(crate) fn new_completed_exec_command(
command: Vec<String>, command: Vec<String>,
parsed: Vec<ParsedCommand>, parsed: Vec<ParsedCommand>,
output: CommandOutput, output: CommandOutput,
) -> ExecCell {
new_exec_cell(command, parsed, Some(output))
}
fn new_exec_cell(
command: Vec<String>,
parsed: Vec<ParsedCommand>,
output: Option<CommandOutput>,
) -> ExecCell { ) -> ExecCell {
ExecCell { ExecCell {
command, command,
parsed, parsed,
output, output: Some(output),
start_time: None,
} }
} }
fn exec_duration(start: Instant) -> String {
format!("{}s", start.elapsed().as_secs())
}
fn exec_command_lines( fn exec_command_lines(
command: &[String], command: &[String],
parsed: &[ParsedCommand], parsed: &[ParsedCommand],
output: Option<&CommandOutput>, output: Option<&CommandOutput>,
start_time: Option<Instant>,
) -> Vec<Line<'static>> { ) -> Vec<Line<'static>> {
match parsed.is_empty() { match parsed.is_empty() {
true => new_exec_command_generic(command, output), true => new_exec_command_generic(command, output, start_time),
false => new_parsed_command(parsed, output), false => new_parsed_command(parsed, output, start_time),
} }
} }
fn new_parsed_command( fn new_parsed_command(
parsed_commands: &[ParsedCommand], parsed_commands: &[ParsedCommand],
output: Option<&CommandOutput>, output: Option<&CommandOutput>,
start_time: Option<Instant>,
) -> Vec<Line<'static>> { ) -> Vec<Line<'static>> {
let mut lines: Vec<Line> = vec![match output { let mut lines: Vec<Line> = Vec::new();
None => Line::from("⚙︎ Working".magenta().bold()), match output {
Some(o) if o.exit_code == 0 => Line::from("✓ Completed".green().bold()), None => {
Some(o) => Line::from(format!("✗ Failed (exit {})", o.exit_code).red().bold()), let mut spans = vec!["⚙︎ Working".magenta().bold()];
}]; if let Some(st) = start_time {
let dur = exec_duration(st);
spans.push(format!("{dur}").dim());
}
lines.push(Line::from(spans));
}
Some(o) if o.exit_code == 0 => {
lines.push(Line::from("✓ Completed".green().bold()));
}
Some(o) => {
lines.push(Line::from(
format!("✗ Failed (exit {})", o.exit_code).red().bold(),
));
}
};
for (i, parsed) in parsed_commands.iter().enumerate() { for (i, parsed) in parsed_commands.iter().enumerate() {
let text = match parsed { let text = match parsed {
@@ -282,17 +306,27 @@ fn new_parsed_command(
fn new_exec_command_generic( fn new_exec_command_generic(
command: &[String], command: &[String],
output: Option<&CommandOutput>, output: Option<&CommandOutput>,
start_time: Option<Instant>,
) -> Vec<Line<'static>> { ) -> Vec<Line<'static>> {
let mut lines: Vec<Line<'static>> = Vec::new(); let mut lines: Vec<Line<'static>> = Vec::new();
let command_escaped = strip_bash_lc_and_escape(command); let command_escaped = strip_bash_lc_and_escape(command);
let mut cmd_lines = command_escaped.lines(); let mut cmd_lines = command_escaped.lines();
if let Some(first) = cmd_lines.next() { if let Some(first) = cmd_lines.next() {
lines.push(Line::from(vec![ let mut spans: Vec<Span> = vec!["⚡ Running".magenta()];
"⚡ Running ".to_string().magenta(), if let Some(st) = start_time {
first.to_string().into(), let dur = exec_duration(st);
])); spans.push(format!("{dur}").dim());
}
spans.push(" ".into());
spans.push(first.to_string().into());
lines.push(Line::from(spans));
} else { } else {
lines.push(Line::from("⚡ Running".to_string().magenta())); let mut spans: Vec<Span> = vec!["⚡ Running".magenta()];
if let Some(st) = start_time {
let dur = exec_duration(st);
spans.push(format!("{dur}").dim());
}
lines.push(Line::from(spans));
} }
for cont in cmd_lines { for cont in cmd_lines {
lines.push(Line::from(cont.to_string())); lines.push(Line::from(cont.to_string()));
@@ -866,7 +900,7 @@ mod tests {
let parsed = vec![ParsedCommand::Unknown { let parsed = vec![ParsedCommand::Unknown {
cmd: "printf 'foo\nbar'".to_string(), cmd: "printf 'foo\nbar'".to_string(),
}]; }];
let lines = exec_command_lines(&[], &parsed, None); let lines = exec_command_lines(&[], &parsed, None, None);
assert!(lines.len() >= 3); assert!(lines.len() >= 3);
assert_eq!(lines[1].spans[0].content, ""); assert_eq!(lines[1].spans[0].content, "");
assert_eq!(lines[2].spans[0].content, " "); assert_eq!(lines[2].spans[0].content, " ");