use ratatui::{
buffer::Buffer,
layout::Rect,
style::Style,
widgets::{Block, Borders, StatefulWidget, Widget},
};
use crate::color::ColorTheme;
use crate::void_backend::{ObjectInfo, ObjectType};
#[derive(Debug)]
pub struct ObjectListState {
objects: Vec<ObjectInfo>,
selected: usize,
offset: usize,
}
impl ObjectListState {
pub fn new(objects: Vec<ObjectInfo>) -> Self {
Self {
objects,
selected: 0,
offset: 0,
}
}
pub fn len(&self) -> usize {
self.objects.len()
}
pub fn is_empty(&self) -> bool {
self.objects.is_empty()
}
pub fn selected_object(&self) -> Option<&ObjectInfo> {
let idx = self.offset + self.selected;
self.objects.get(idx)
}
pub fn selected_index(&self) -> usize {
self.offset + self.selected
}
pub fn select_next(&mut self, viewport_height: usize) {
let max_idx = self.objects.len().saturating_sub(1);
let current_abs = self.offset + self.selected;
if current_abs < max_idx {
if self.selected < viewport_height.saturating_sub(1) {
self.selected += 1;
} else {
self.offset += 1;
}
}
}
pub fn select_prev(&mut self) {
if self.selected > 0 {
self.selected -= 1;
} else if self.offset > 0 {
self.offset -= 1;
}
}
pub fn scroll_down_half(&mut self, viewport_height: usize) {
let half = viewport_height / 2;
for _ in 0..half {
self.select_next(viewport_height);
}
}
pub fn scroll_up_half(&mut self, viewport_height: usize) {
let half = viewport_height / 2;
for _ in 0..half {
self.select_prev();
}
}
pub fn scroll_down_page(&mut self, viewport_height: usize) {
for _ in 0..viewport_height {
self.select_next(viewport_height);
}
}
pub fn scroll_up_page(&mut self, viewport_height: usize) {
for _ in 0..viewport_height {
self.select_prev();
}
}
pub fn select_first(&mut self) {
self.selected = 0;
self.offset = 0;
}
pub fn select_last(&mut self, viewport_height: usize) {
let total = self.objects.len();
if total <= viewport_height {
self.offset = 0;
self.selected = total.saturating_sub(1);
} else {
self.offset = total - viewport_height;
self.selected = viewport_height.saturating_sub(1);
}
}
pub fn update_object(&mut self, index: usize, info: ObjectInfo) {
if index < self.objects.len() {
self.objects[index] = info;
}
}
pub fn get_object_mut(&mut self, index: usize) -> Option<&mut ObjectInfo> {
self.objects.get_mut(index)
}
}
pub struct ObjectList<'a> {
theme: &'a ColorTheme,
}
impl<'a> ObjectList<'a> {
pub fn new(theme: &'a ColorTheme) -> Self {
Self { theme }
}
}
impl<'a> StatefulWidget for ObjectList<'a> {
type State = ObjectListState;
fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
let title = format!(" Objects ({}) ", state.len());
let block = Block::default().borders(Borders::ALL).title(title);
let inner = block.inner(area);
block.render(area, buf);
if state.is_empty() {
let msg = "No objects found";
let x = inner.x + (inner.width.saturating_sub(msg.len() as u16)) / 2;
let y = inner.y + inner.height / 2;
buf.set_string(x, y, msg, Style::default().fg(self.theme.status_warn_fg));
return;
}
let viewport_height = inner.height as usize;
let cid_width = 14_u16; let type_width = 10_u16; let format_width = 14_u16; let size_width = inner.width.saturating_sub(cid_width + type_width + format_width);
for (i, idx) in (state.offset..).take(viewport_height).enumerate() {
if idx >= state.objects.len() {
break;
}
let obj = &state.objects[idx];
let is_selected = i == state.selected;
let y = inner.y + i as u16;
let base_style = if is_selected {
Style::default()
.fg(self.theme.list_selected_fg)
.bg(self.theme.list_selected_bg)
} else {
Style::default()
};
let indicator = if is_selected { "> " } else { " " };
buf.set_string(inner.x, y, indicator, base_style);
let cid_style = if is_selected {
base_style
} else {
Style::default().fg(self.theme.list_cid_fg)
};
buf.set_string(inner.x + 2, y, obj.short_cid(), cid_style);
let type_x = inner.x + 2 + cid_width;
let (type_str, type_color) = match obj.object_type {
ObjectType::Commit => ("[commit]", self.theme.list_type_commit_fg),
ObjectType::Metadata => ("[metadata]", self.theme.list_type_metadata_fg),
ObjectType::Manifest => ("[manifest]", self.theme.list_type_metadata_fg),
ObjectType::RepoManifest => ("[repo-manifest]", self.theme.list_type_metadata_fg),
ObjectType::Shard => ("[shard]", self.theme.list_type_shard_fg),
ObjectType::Unknown => ("[unknown]", self.theme.list_type_unknown_fg),
};
let type_style = if is_selected {
base_style
} else {
Style::default().fg(type_color)
};
buf.set_string(type_x, y, type_str, type_style);
let format_x = type_x + type_width;
let (format_str, format_color) = if obj.format.is_known() {
(obj.format.as_str(), self.theme.list_format_known_fg)
} else {
(obj.format.as_str(), self.theme.list_format_unknown_fg)
};
let format_style = if is_selected {
base_style
} else {
Style::default().fg(format_color)
};
buf.set_string(format_x, y, format_str, format_style);
if size_width > 4 {
let size_str = format_size(obj.encrypted_size);
let size_x = inner.x + inner.width - size_str.len() as u16 - 1;
buf.set_string(size_x, y, &size_str, base_style);
}
if is_selected {
for x in inner.x..(inner.x + inner.width) {
if let Some(cell) = buf.cell_mut((x, y)) {
if cell.symbol() == " " {
cell.set_style(base_style);
}
}
}
}
}
}
}
fn format_size(bytes: usize) -> String {
if bytes < 1024 {
format!("{}B", bytes)
} else if bytes < 1024 * 1024 {
format!("{:.1}KB", bytes as f64 / 1024.0)
} else {
format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
}
}