use std::time::Duration;
use atuin_client::history::History;
use tui::{
buffer::Buffer,
layout::Rect,
style::{Color, Modifier, Style},
widgets::{Block, StatefulWidget, Widget},
};
use super::format_duration;
pub struct HistoryList<'a> {
history: &'a [History],
block: Option<Block<'a>>,
}
#[derive(Default)]
pub struct ListState {
offset: usize,
selected: usize,
}
impl ListState {
pub fn selected(&self) -> usize {
self.selected
}
pub fn select(&mut self, index: usize) {
self.selected = index;
}
}
impl<'a> StatefulWidget for HistoryList<'a> {
type State = ListState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let list_area = self.block.take().map_or(area, |b| {
let inner_area = b.inner(area);
b.render(area, buf);
inner_area
});
if list_area.width < 1 || list_area.height < 1 || self.history.is_empty() {
return;
}
let list_height = list_area.height as usize;
let (start, end) = self.get_items_bounds(state.selected, state.offset, list_height);
state.offset = start;
let mut s = DrawState {
buf,
list_area,
x: 0,
y: 0,
state,
};
for item in self.history.iter().skip(state.offset).take(end - start) {
s.index();
s.duration(item);
s.time(item);
s.command(item);
s.y += 1;
s.x = 0;
}
}
}
impl<'a> HistoryList<'a> {
pub fn new(history: &'a [History]) -> Self {
Self {
history,
block: None,
}
}
pub fn block(mut self, block: Block<'a>) -> Self {
self.block = Some(block);
self
}
fn get_items_bounds(&self, selected: usize, offset: usize, height: usize) -> (usize, usize) {
let offset = offset.min(self.history.len().saturating_sub(1));
let max_scroll_space = height.min(10);
if offset + height < selected + max_scroll_space {
let end = selected + max_scroll_space;
(end - height, end)
} else if selected < offset {
(selected, selected + height)
} else {
(offset, offset + height)
}
}
}
struct DrawState<'a> {
buf: &'a mut Buffer,
list_area: Rect,
x: u16,
y: u16,
state: &'a ListState,
}
#[allow(clippy::cast_possible_truncation)] pub const PREFIX_LENGTH: u16 = " > 123ms 59s ago".len() as u16;
impl DrawState<'_> {
fn index(&mut self) {
static SLICES: &str = " > 1 2 3 4 5 6 7 8 9 ";
let i = self.y as usize + self.state.offset;
let i = i.checked_sub(self.state.selected);
let i = i.unwrap_or(10).min(10) * 2;
self.draw(&SLICES[i..i + 3], Style::default());
}
fn duration(&mut self, h: &History) {
let status = Style::default().fg(if h.success() {
Color::Green
} else {
Color::Red
});
let duration = Duration::from_nanos(u64::try_from(h.duration).unwrap_or(0));
self.draw(&format_duration(duration), status);
}
#[allow(clippy::cast_possible_truncation)] fn time(&mut self, h: &History) {
let style = Style::default().fg(Color::Blue);
let since = chrono::Utc::now() - h.timestamp;
let time = format_duration(since.to_std().unwrap_or_default());
self.x = PREFIX_LENGTH - 4 - time.len() as u16;
self.draw(&time, style);
self.draw(" ago", style);
}
fn command(&mut self, h: &History) {
let mut style = Style::default();
if self.y as usize + self.state.offset == self.state.selected {
style = style.fg(Color::Red).add_modifier(Modifier::BOLD);
}
for section in h.command.split_ascii_whitespace() {
self.x += 1;
if self.x > self.list_area.width {
return;
}
self.draw(section, style);
}
}
fn draw(&mut self, s: &str, style: Style) {
let cx = self.list_area.left() + self.x;
let cy = self.list_area.bottom() - self.y - 1;
let w = (self.list_area.width - self.x) as usize;
self.x += self.buf.set_stringn(cx, cy, s, w, style).0 - cx;
}
}