use super::HistoryBrowser;
use crate::pager::text_utils::{
display_width, exceeds_width, pad_to_width, scroll_display, truncate_to_width,
};
use crate::pager::{self, MinimumSize, render_size_warning_if_too_small};
use crossterm::{
ExecutableCommand, cursor, queue,
style::Stylize,
terminal::{self, BeginSynchronizedUpdate, ClearType, EndSynchronizedUpdate},
};
use std::io::{self, Write};
pub(super) const MIN_SIZE: MinimumSize = MinimumSize { cols: 70, rows: 10 };
impl HistoryBrowser {
pub(super) fn render(&self, stdout: &mut io::Stdout) -> io::Result<()> {
if render_size_warning_if_too_small(stdout, &MIN_SIZE)? {
return Ok(());
}
queue!(stdout, BeginSynchronizedUpdate)?;
stdout.execute(cursor::MoveTo(0, 0))?;
stdout.execute(cursor::Hide)?;
let (cols, _rows) = terminal::size().unwrap_or((80, 24));
let width = cols as usize;
let selected_count = self.selected_count();
let selected_info = if selected_count > 0 {
format!(" [{} selected]", selected_count)
} else {
String::new()
};
let header = format!(
"─ History Browser [{}] [{} entries]{} ─",
self.db_mode.display_name(),
self.filtered.len(),
selected_info
);
let padded_header = format!("{:─<width$}", header, width = width);
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
println!("\r{}", padded_header.dark_grey());
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
if self.filter_active {
let before_cursor: String = self
.filter
.raw_query
.chars()
.take(self.filter.cursor_pos)
.collect();
let after_cursor: String = self
.filter
.raw_query
.chars()
.skip(self.filter.cursor_pos)
.collect();
let filter_line = format!(" Filter: {}_{}", before_cursor, after_cursor);
println!("\r{}", pad_to_width(&filter_line, width));
} else if self.filter.raw_query.is_empty() {
println!(
"\r{}",
pad_to_width(" Filter: (press / to filter)", width).dark_grey()
);
} else {
let filter_line = format!(" Filter: {}", self.filter.raw_query);
println!("\r{}", pad_to_width(&filter_line, width));
}
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
println!("\r{}", "─".repeat(width).dark_grey());
let (cmd_width, cwd_width, host_width) = calculate_layout(width);
let visible_rows = visible_result_rows();
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
let col_headers = format!(
" {:<16} {:>4} {} {} {}",
"Date",
"Exit",
pad_to_width("Command", cmd_width),
pad_to_width("Directory", cwd_width),
pad_to_width("Host", host_width),
);
println!("\r{}", pad_to_width(&col_headers, width).dark_grey());
for i in 0..visible_rows {
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
let idx = self.scroll_offset + i;
if idx < self.filtered.len() {
let (entry_idx, _score) = self.filtered[idx];
let entry = &self.entries[entry_idx];
let is_current = idx == self.cursor;
let checkbox = if entry.selected { "[x]" } else { "[ ]" };
let cursor_marker = if is_current { " > " } else { " " };
let timestamp = entry
.item
.start_timestamp
.map(|ts| {
ts.with_timezone(&chrono::Local)
.format("%Y-%m-%d %H:%M")
.to_string()
})
.unwrap_or_else(|| " ".to_string());
let exit_str = match entry.item.exit_status {
Some(code) => format!("{:>4}", code),
None => " -".to_string(),
};
let cmd = flatten_multiline(&entry.item.command_line);
let display_cmd = if is_current && exceeds_width(&cmd, cmd_width) {
let (scrolled, _) =
scroll_display(&cmd, cmd_width, self.text_scroll.scroll_pos);
scrolled
} else {
truncate_to_width(&cmd, cmd_width)
};
let cwd_full = entry.item.cwd.as_deref().unwrap_or("");
let cwd_short = std::path::Path::new(cwd_full)
.file_name()
.and_then(|f| f.to_str())
.unwrap_or(cwd_full);
let display_cwd = if is_current && exceeds_width(cwd_short, cwd_width) {
let (scrolled, _) =
scroll_display(cwd_short, cwd_width, self.text_scroll.scroll_pos);
scrolled
} else {
truncate_to_width(cwd_short, cwd_width)
};
let host = entry.item.hostname.as_deref().unwrap_or("");
let display_host = truncate_to_width(host, host_width);
let prefix_base = format!("{}{} {} ", cursor_marker, checkbox, timestamp);
let padded_cmd = pad_to_width(&display_cmd, cmd_width);
let padded_cwd = pad_to_width(&display_cwd, cwd_width);
if is_current {
let content = format!(
"{}{} {} {} {}",
prefix_base, exit_str, padded_cmd, padded_cwd, display_host
);
let line = pad_to_width(&content, width);
println!("\r{}", line.reverse());
} else if entry.selected {
let content = format!(
"{}{} {} {} {}",
prefix_base, exit_str, padded_cmd, padded_cwd, display_host
);
let line = pad_to_width(&content, width);
println!("\r{}", line.yellow());
} else {
let styled_exit = if matches!(entry.item.exit_status, Some(code) if code != 0) {
format!("{}", exit_str.as_str().red())
} else {
exit_str.clone()
};
let content_width = display_width(&prefix_base)
+ 4
+ 1
+ cmd_width
+ 1
+ cwd_width
+ 1
+ display_width(&display_host);
let padding_len = width.saturating_sub(content_width);
print!(
"\r{}{} {} {} {}{}\n",
prefix_base,
styled_exit,
padded_cmd,
padded_cwd.dark_grey(),
display_host.dark_grey(),
" ".repeat(padding_len)
);
}
} else {
println!("\r{}", " ".repeat(width));
}
}
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
println!("\r{}", "─".repeat(width).dark_grey());
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
let syntax_help = " Filter: host:<name> cwd:<path> exit:<N> <text> (space = AND)";
println!("\r{}", pad_to_width(syntax_help, width).dark_grey());
stdout.execute(terminal::Clear(ClearType::CurrentLine))?;
if self.show_delete_dialog {
let dialog_msg = format!(
" Delete {} selected entries? (Enter=confirm, Esc=cancel)",
selected_count
);
print!("\r{}", pad_to_width(&dialog_msg, width).yellow().bold());
} else if let Some(ref msg) = self.feedback_message {
print!("\r{}", pad_to_width(&format!(" {}", msg), width));
} else {
let footer = if self.filter_active {
" Enter confirm | Esc clear | ↑↓/PgUp/PgDn navigate | Tab select"
} else {
" / filter | Space/Tab select | d delete | y copy | Enter copy+exit | q exit"
};
print!("\r{}", pad_to_width(footer, width).dark_grey());
}
queue!(stdout, EndSynchronizedUpdate)?;
stdout.flush()?;
Ok(())
}
}
pub(super) fn calculate_layout(cols: usize) -> (usize, usize, usize) {
let prefix_width = 29;
let host_width = (cols / 8).clamp(5, 15);
let cwd_width = (cols / 6).clamp(8, 20);
let cmd_width = cols
.saturating_sub(prefix_width + cwd_width + host_width + 2)
.max(20);
(cmd_width, cwd_width, host_width)
}
const CHROME_ROWS: usize = 7;
pub(super) fn visible_result_rows() -> usize {
pager::visible_result_rows(CHROME_ROWS)
}
pub(super) fn flatten_multiline(s: &str) -> String {
if s.contains('\n') {
s.replace('\n', "↵")
} else {
s.to_string()
}
}