void_audit_tui/widget/
object_list.rs1use ratatui::{
6 buffer::Buffer,
7 layout::Rect,
8 style::Style,
9 widgets::{Block, Borders, StatefulWidget, Widget},
10};
11
12use crate::color::ColorTheme;
13use crate::void_backend::{ObjectInfo, ObjectType};
14
15#[derive(Debug)]
17pub struct ObjectListState {
18 objects: Vec<ObjectInfo>,
20 selected: usize,
22 offset: usize,
24}
25
26impl ObjectListState {
27 pub fn new(objects: Vec<ObjectInfo>) -> Self {
29 Self {
30 objects,
31 selected: 0,
32 offset: 0,
33 }
34 }
35
36 pub fn len(&self) -> usize {
38 self.objects.len()
39 }
40
41 pub fn is_empty(&self) -> bool {
43 self.objects.is_empty()
44 }
45
46 pub fn selected_object(&self) -> Option<&ObjectInfo> {
48 let idx = self.offset + self.selected;
49 self.objects.get(idx)
50 }
51
52 pub fn selected_index(&self) -> usize {
54 self.offset + self.selected
55 }
56
57 pub fn select_next(&mut self, viewport_height: usize) {
59 let max_idx = self.objects.len().saturating_sub(1);
60 let current_abs = self.offset + self.selected;
61
62 if current_abs < max_idx {
63 if self.selected < viewport_height.saturating_sub(1) {
64 self.selected += 1;
65 } else {
66 self.offset += 1;
67 }
68 }
69 }
70
71 pub fn select_prev(&mut self) {
73 if self.selected > 0 {
74 self.selected -= 1;
75 } else if self.offset > 0 {
76 self.offset -= 1;
77 }
78 }
79
80 pub fn scroll_down_half(&mut self, viewport_height: usize) {
82 let half = viewport_height / 2;
83 for _ in 0..half {
84 self.select_next(viewport_height);
85 }
86 }
87
88 pub fn scroll_up_half(&mut self, viewport_height: usize) {
90 let half = viewport_height / 2;
91 for _ in 0..half {
92 self.select_prev();
93 }
94 }
95
96 pub fn scroll_down_page(&mut self, viewport_height: usize) {
98 for _ in 0..viewport_height {
99 self.select_next(viewport_height);
100 }
101 }
102
103 pub fn scroll_up_page(&mut self, viewport_height: usize) {
105 for _ in 0..viewport_height {
106 self.select_prev();
107 }
108 }
109
110 pub fn select_first(&mut self) {
112 self.selected = 0;
113 self.offset = 0;
114 }
115
116 pub fn select_last(&mut self, viewport_height: usize) {
118 let total = self.objects.len();
119 if total <= viewport_height {
120 self.offset = 0;
121 self.selected = total.saturating_sub(1);
122 } else {
123 self.offset = total - viewport_height;
124 self.selected = viewport_height.saturating_sub(1);
125 }
126 }
127
128 pub fn update_object(&mut self, index: usize, info: ObjectInfo) {
130 if index < self.objects.len() {
131 self.objects[index] = info;
132 }
133 }
134
135 pub fn get_object_mut(&mut self, index: usize) -> Option<&mut ObjectInfo> {
137 self.objects.get_mut(index)
138 }
139}
140
141pub struct ObjectList<'a> {
143 theme: &'a ColorTheme,
144}
145
146impl<'a> ObjectList<'a> {
147 pub fn new(theme: &'a ColorTheme) -> Self {
149 Self { theme }
150 }
151}
152
153impl<'a> StatefulWidget for ObjectList<'a> {
154 type State = ObjectListState;
155
156 fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
157 let title = format!(" Objects ({}) ", state.len());
159 let block = Block::default().borders(Borders::ALL).title(title);
160 let inner = block.inner(area);
161 block.render(area, buf);
162
163 if state.is_empty() {
164 let msg = "No objects found";
166 let x = inner.x + (inner.width.saturating_sub(msg.len() as u16)) / 2;
167 let y = inner.y + inner.height / 2;
168 buf.set_string(x, y, msg, Style::default().fg(self.theme.status_warn_fg));
169 return;
170 }
171
172 let viewport_height = inner.height as usize;
173
174 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);
179
180 for (i, idx) in (state.offset..).take(viewport_height).enumerate() {
182 if idx >= state.objects.len() {
183 break;
184 }
185
186 let obj = &state.objects[idx];
187 let is_selected = i == state.selected;
188 let y = inner.y + i as u16;
189
190 let base_style = if is_selected {
192 Style::default()
193 .fg(self.theme.list_selected_fg)
194 .bg(self.theme.list_selected_bg)
195 } else {
196 Style::default()
197 };
198
199 let indicator = if is_selected { "> " } else { " " };
201 buf.set_string(inner.x, y, indicator, base_style);
202
203 let cid_style = if is_selected {
205 base_style
206 } else {
207 Style::default().fg(self.theme.list_cid_fg)
208 };
209 buf.set_string(inner.x + 2, y, obj.short_cid(), cid_style);
210
211 let type_x = inner.x + 2 + cid_width;
213 let (type_str, type_color) = match obj.object_type {
214 ObjectType::Commit => ("[commit]", self.theme.list_type_commit_fg),
215 ObjectType::Metadata => ("[metadata]", self.theme.list_type_metadata_fg),
216 ObjectType::Manifest => ("[manifest]", self.theme.list_type_metadata_fg),
217 ObjectType::RepoManifest => ("[repo-manifest]", self.theme.list_type_metadata_fg),
218 ObjectType::Shard => ("[shard]", self.theme.list_type_shard_fg),
219 ObjectType::Unknown => ("[unknown]", self.theme.list_type_unknown_fg),
220 };
221 let type_style = if is_selected {
222 base_style
223 } else {
224 Style::default().fg(type_color)
225 };
226 buf.set_string(type_x, y, type_str, type_style);
227
228 let format_x = type_x + type_width;
230 let (format_str, format_color) = if obj.format.is_known() {
231 (obj.format.as_str(), self.theme.list_format_known_fg)
232 } else {
233 (obj.format.as_str(), self.theme.list_format_unknown_fg)
234 };
235 let format_style = if is_selected {
236 base_style
237 } else {
238 Style::default().fg(format_color)
239 };
240 buf.set_string(format_x, y, format_str, format_style);
241
242 if size_width > 4 {
244 let size_str = format_size(obj.encrypted_size);
245 let size_x = inner.x + inner.width - size_str.len() as u16 - 1;
246 buf.set_string(size_x, y, &size_str, base_style);
247 }
248
249 if is_selected {
251 for x in inner.x..(inner.x + inner.width) {
252 if let Some(cell) = buf.cell_mut((x, y)) {
253 if cell.symbol() == " " {
254 cell.set_style(base_style);
255 }
256 }
257 }
258 }
259 }
260 }
261}
262
263fn format_size(bytes: usize) -> String {
265 if bytes < 1024 {
266 format!("{}B", bytes)
267 } else if bytes < 1024 * 1024 {
268 format!("{:.1}KB", bytes as f64 / 1024.0)
269 } else {
270 format!("{:.1}MB", bytes as f64 / (1024.0 * 1024.0))
271 }
272}