1use crate::inspector::inspector_drill::{Drill, JsonWait, Level, Node};
7use crate::inspector::inspector_reader::{Reader, Wrap};
8use crate::table::{InspectField, InspectRow};
9use crate::widgets::inspector::Pane;
10use polars::prelude::DataFrame;
11use std::sync::Arc;
12
13pub const CHUNK_BYTES: usize = 16 * 1024;
17
18#[derive(Debug, Clone)]
20pub enum FieldRead {
21 Reading { frame: u64, row: usize },
23 Read {
25 frame: u64,
26 row: usize,
27 values: DataFrame,
28 },
29 Failed {
31 frame: u64,
32 row: usize,
33 message: String,
34 },
35}
36
37impl FieldRead {
38 pub fn key(&self) -> (u64, usize) {
40 match self {
41 Self::Reading { frame, row }
42 | Self::Read { frame, row, .. }
43 | Self::Failed { frame, row, .. } => (*frame, *row),
44 }
45 }
46}
47
48#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
50pub enum Focus {
51 #[default]
52 List,
53 Value,
54}
55
56#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
58pub enum Order {
59 #[default]
61 Table,
62 Name,
64 Filled,
66}
67
68impl Order {
69 pub fn next(self) -> Self {
70 match self {
71 Order::Table => Order::Name,
72 Order::Name => Order::Filled,
73 Order::Filled => Order::Table,
74 }
75 }
76
77 pub fn label(self) -> Option<&'static str> {
79 match self {
80 Order::Table => None,
81 Order::Name => Some("A-Z"),
82 Order::Filled => Some("nulls last"),
83 }
84 }
85}
86
87#[derive(Debug, Clone, Copy, PartialEq, Eq)]
89pub enum View {
90 Json,
92 Raw,
94 Escaped,
96 Hex,
98 Text,
100}
101
102impl View {
103 pub fn label(self) -> &'static str {
104 match self {
105 View::Json => "JSON",
106 View::Raw => "Raw",
107 View::Escaped => "Escaped",
108 View::Hex => "Hex",
109 View::Text => "Text",
110 }
111 }
112}
113
114#[derive(Debug, Clone, Default)]
116pub struct ValueFind {
117 pub text: String,
118 pub editing: bool,
120 pub hits: Vec<usize>,
122 pub current: Option<usize>,
123 pub pane: u64,
124}
125
126#[derive(Debug, Clone)]
128pub enum Pretty {
129 Pending {
130 token: u64,
131 place: (u64, usize, String),
132 },
133 Ready {
134 place: (u64, usize, String),
135 text: Arc<str>,
136 },
137 Failed {
138 place: (u64, usize, String),
139 },
140}
141
142impl Pretty {
143 pub fn place(&self) -> &(u64, usize, String) {
144 match self {
145 Pretty::Pending { place, .. }
146 | Pretty::Ready { place, .. }
147 | Pretty::Failed { place } => place,
148 }
149 }
150}
151
152#[derive(Debug, Clone)]
154pub enum Unpack {
155 Pending {
156 token: u64,
157 place: (u64, usize, String),
158 },
159 Ready {
160 place: (u64, usize, String),
161 text: Arc<crate::inspector::inspector_bytes::Decoded>,
162 },
163 Failed {
164 place: (u64, usize, String),
165 },
166}
167
168impl Unpack {
169 pub fn place(&self) -> &(u64, usize, String) {
170 match self {
171 Unpack::Pending { place, .. }
172 | Unpack::Ready { place, .. }
173 | Unpack::Failed { place } => place,
174 }
175 }
176}
177
178#[derive(Debug, Clone, PartialEq, Eq)]
181pub struct PaneKey {
182 pub frame: u64,
183 pub row: usize,
184 pub field: String,
185 pub view: Option<View>,
186 pub width: u16,
187 pub state: u8,
189 pub pretty: u8,
191 pub unpacked: u8,
193}
194
195impl PaneKey {
196 pub fn same_value(&self, other: &Self) -> bool {
199 *self
200 == Self {
201 width: self.width,
202 ..other.clone()
203 }
204 }
205}
206
207#[derive(Default)]
208pub struct InspectorModal {
209 pub fields: Vec<InspectField>,
210 pub filter: String,
212 pub finding: bool,
214 selected: usize,
216 pub visible: Vec<usize>,
219 pub list_offset: usize,
221 pub list_page: usize,
223 pub order: Order,
224 pub filled_only: bool,
226 pub focus: Focus,
227 pub view: Option<View>,
229 view_field: Option<String>,
230 pub wrap: Wrap,
231 pub reader: Reader,
233 pub pane: Option<(PaneKey, Pane)>,
234 pane_id: u64,
235 pub value_find: Option<ValueFind>,
236 pub page: usize,
238 pub shown_row: Option<(u64, usize)>,
240 pub read: Option<FieldRead>,
241 pub follow: Option<String>,
244 pub compare: bool,
246 pub pinned: Option<InspectRow>,
248 pub compare_both: bool,
251 pub drill: Option<Drill>,
253 pub json_wait: Option<JsonWait>,
255 pub json_token: u64,
257 pub not_json: Option<(u64, usize, String)>,
260 pub pretty: Option<Pretty>,
262 pub pretty_token: u64,
263 pub unpack: Option<Unpack>,
265 pub unpack_token: u64,
266 pub(crate) listed: Option<ListKey>,
269 #[cfg(test)]
271 pub(crate) list_builds: usize,
272}
273
274#[derive(Debug, Clone, PartialEq, Eq)]
277pub(crate) struct ListKey {
278 pub order: Order,
279 pub filled_only: bool,
280 pub filter: String,
281 pub row: Option<(u64, usize)>,
283 pub buffered: (usize, usize),
285 pub compare: bool,
286 pub compare_both: bool,
287 pub pinned: Option<(u64, usize)>,
288 pub read: Option<((u64, usize), u8)>,
290}
291
292impl InspectorModal {
293 pub fn new() -> Self {
294 Self::default()
295 }
296
297 pub fn open(&mut self, fields: Vec<InspectField>, current: Option<&str>) {
300 let keep = current
301 .map(str::to_string)
302 .or_else(|| self.focused().map(|f| f.name.clone()))
303 .and_then(|name| fields.iter().position(|f| f.name == name));
304 self.selected = keep.unwrap_or(0);
305 self.visible = (0..fields.len()).collect();
306 self.fields = fields;
307 self.listed = None;
308 self.finding = false;
309 self.focus = Focus::List;
310 self.list_offset = 0;
311 self.read = None;
312 self.follow = None;
313 self.pane = None;
314 self.shown_row = None;
315 self.drill = None;
316 self.json_wait = None;
317 self.not_json = None;
318 self.pretty = None;
319 self.unpack = None;
320 self.value_find = None;
321 self.reader = Reader::default();
322 }
323
324 pub fn close(&mut self) {
325 self.finding = false;
326 self.focus = Focus::List;
327 self.read = None;
328 self.follow = None;
329 self.pane = None;
330 self.drill = None;
331 self.json_wait = None;
332 self.not_json = None;
333 self.pretty = None;
334 self.unpack = None;
335 self.value_find = None;
336 }
337
338 pub fn next_pane_id(&mut self) -> u64 {
340 self.pane_id += 1;
341 self.pane_id
342 }
343
344 pub fn unpacked(&self, place: &(u64, usize, String)) -> crate::widgets::inspector::Unpacked {
346 use crate::widgets::inspector::Unpacked;
347 match &self.unpack {
348 Some(Unpack::Pending { place: p, .. }) if p == place => Unpacked::Pending,
349 Some(Unpack::Ready { place: p, text }) if p == place => Unpacked::Ready(text.clone()),
350 Some(Unpack::Failed { place: p }) if p == place => Unpacked::Failed,
351 _ => Unpacked::None,
352 }
353 }
354
355 pub fn known_not_json(&self, frame: u64, row: usize, path: &str) -> bool {
357 self.not_json
358 .as_ref()
359 .is_some_and(|(f, r, p)| (*f, *r) == (frame, row) && p == path)
360 }
361
362 pub fn focused(&self) -> Option<&InspectField> {
365 self.focused_index().and_then(|i| self.fields.get(i))
366 }
367
368 fn focused_index(&self) -> Option<usize> {
369 if self.visible.contains(&self.selected) {
370 Some(self.selected)
371 } else {
372 self.visible.first().copied()
373 }
374 }
375
376 pub fn focused_position(&self) -> usize {
378 self.focused_index()
379 .and_then(|i| self.visible.iter().position(|&v| v == i))
380 .unwrap_or(0)
381 }
382
383 pub fn set_visible(&mut self, visible: Vec<usize>) {
386 if !visible.contains(&self.selected)
387 && let Some(&first) = visible.first()
388 {
389 self.selected = first;
390 }
391 self.visible = visible;
392 }
393
394 fn select_position(&mut self, at: usize) {
395 if let Some(&i) = self.visible.get(at)
396 && i != self.selected
397 {
398 self.selected = i;
399 self.field_changed();
400 }
401 }
402
403 fn field_changed(&mut self) {
406 let name = self.focused().map(|f| f.name.clone());
407 if self.view_field != name {
408 self.view = None;
409 }
410 if self.follow.is_some() && self.follow != name {
411 self.follow = None;
412 }
413 }
414
415 fn step(&mut self, delta: isize) -> bool {
417 let Some(drill) = self.drill.as_mut() else {
418 return false;
419 };
420 let level = drill.level_mut();
421 let last = level.node.len().saturating_sub(1);
422 level.selected = level.selected.saturating_add_signed(delta).min(last);
423 true
424 }
425
426 pub fn move_field(&mut self, step: crate::app::form::ListMove) {
429 use crate::app::form::ListMove;
430 let page = self.list_page.max(1);
431 if self.step(step.delta(page)) || self.visible.is_empty() {
432 return;
433 }
434 let n = self.visible.len();
435 let at = self.focused_position();
436 let to = match step {
437 ListMove::Up => (at + n - 1) % n,
438 ListMove::Down => (at + 1) % n,
439 ListMove::PageUp | ListMove::PageDown => {
440 self.list_offset = self
441 .list_offset
442 .saturating_add_signed(step.delta(page))
443 .min(n - 1);
444 step.apply(at, n, page)
445 }
446 ListMove::Home | ListMove::End => step.apply(at, n, page),
447 };
448 self.select_position(to);
449 }
450
451 pub fn choose_view(&mut self, view: View) {
453 self.view = Some(view);
454 self.view_field = self.focused().map(|f| f.name.clone());
455 }
456
457 pub fn drill_in(&mut self, frame: u64, row: usize, label: String, node: Node) {
459 let level = Level {
460 label,
461 node,
462 selected: 0,
463 };
464 match self.drill.as_mut() {
465 Some(drill) if (drill.frame, drill.row) == (frame, row) => drill.levels.push(level),
466 _ => {
467 self.drill = Some(Drill {
468 frame,
469 row,
470 levels: vec![level],
471 })
472 }
473 }
474 self.json_wait = None;
475 self.focus = Focus::List;
476 }
477
478 pub fn drill_out(&mut self) -> bool {
480 let Some(drill) = self.drill.as_mut() else {
481 return false;
482 };
483 drill.levels.pop();
484 if drill.levels.is_empty() {
485 self.drill = None;
486 }
487 self.json_wait = None;
488 self.focus = Focus::List;
489 true
490 }
491
492 pub fn wait_for_json(&mut self, frame: u64, row: usize, label: String, path: String) -> u64 {
494 self.json_token += 1;
495 self.json_wait = Some(JsonWait {
496 token: self.json_token,
497 frame,
498 row,
499 label,
500 path,
501 });
502 self.json_token
503 }
504
505 pub fn find_key(&mut self, c: char, mods: crossterm::event::KeyModifiers) {
508 edit_find(&mut self.filter, c, mods);
509 }
510
511 pub fn find_backspace(&mut self) {
512 self.filter.pop();
513 }
514
515 pub fn clear_find(&mut self) {
516 self.filter.clear();
517 self.finding = false;
518 }
519
520 pub fn row_shown(&mut self, frame: u64, row: usize) {
523 if self.shown_row != Some((frame, row)) {
524 self.shown_row = Some((frame, row));
525 if self
527 .drill
528 .as_ref()
529 .is_some_and(|d| (d.frame, d.row) != (frame, row))
530 {
531 self.drill = None;
532 }
533 if self
534 .json_wait
535 .as_ref()
536 .is_some_and(|w| (w.frame, w.row) != (frame, row))
537 {
538 self.json_wait = None;
539 }
540 if self.read.as_ref().is_some_and(|r| r.key() != (frame, row)) {
541 self.read = None;
542 }
543 if self
544 .pretty
545 .as_ref()
546 .is_some_and(|p| (p.place().0, p.place().1) != (frame, row))
547 {
548 self.pretty = None;
549 }
550 if self
551 .unpack
552 .as_ref()
553 .is_some_and(|u| (u.place().0, u.place().1) != (frame, row))
554 {
555 self.unpack = None;
556 }
557 }
558 }
559
560 pub fn read_values(&self, frame: u64, row: usize) -> Option<&DataFrame> {
562 match &self.read {
563 Some(FieldRead::Read {
564 frame: f,
565 row: r,
566 values,
567 }) if (*f, *r) == (frame, row) => Some(values),
568 _ => None,
569 }
570 }
571
572 pub fn pane_for(&self, frame: u64, row: usize, field: &str) -> Option<&Pane> {
574 self.pane
575 .as_ref()
576 .filter(|(key, _)| (key.frame, key.row) == (frame, row) && key.field == field)
577 .map(|(_, pane)| pane)
578 }
579}
580
581pub fn edit_find(text: &mut String, c: char, mods: crossterm::event::KeyModifiers) {
584 use crossterm::event::KeyModifiers;
585 let ctrl = mods.contains(KeyModifiers::CONTROL);
586 if ctrl && c == 'w' {
587 while text.ends_with(' ') {
588 text.pop();
589 }
590 while text.chars().next_back().is_some_and(|c| c != ' ') {
591 text.pop();
592 }
593 } else if ctrl && c == 'u' {
594 text.clear();
595 } else if !ctrl && !mods.contains(KeyModifiers::ALT) {
596 text.push(c);
597 }
598}
599
600#[cfg(test)]
601mod tests {
602 use super::*;
603 use crossterm::event::KeyModifiers;
604 use polars::prelude::DataType;
605
606 fn fields(names: &[&str]) -> Vec<InspectField> {
607 names
608 .iter()
609 .map(|n| InspectField {
610 name: n.to_string(),
611 dtype: DataType::String,
612 hidden: false,
613 })
614 .collect()
615 }
616
617 #[test]
618 fn the_focus_moves_among_the_fields_listed() {
619 let mut m = InspectorModal::new();
620 m.open(fields(&["id", "description", "amount", "status"]), None);
621 m.move_field(crate::app::form::ListMove::Down);
622 assert_eq!(m.focused().unwrap().name, "description");
623 m.set_visible(vec![2, 3]);
624 assert_eq!(
625 m.focused().unwrap().name,
626 "amount",
627 "unlisted: the first listed"
628 );
629 m.move_field(crate::app::form::ListMove::Down);
630 assert_eq!(m.focused().unwrap().name, "status");
631 m.move_field(crate::app::form::ListMove::Down);
632 assert_eq!(m.focused().unwrap().name, "amount", "round to the top");
633 m.set_visible(vec![0, 1, 2, 3]);
634 assert_eq!(m.focused().unwrap().name, "amount");
635 m.find_key('a', KeyModifiers::NONE);
636 m.find_key('m', KeyModifiers::CONTROL);
637 assert_eq!(m.filter, "a", "a chord types nothing");
638 m.find_key('w', KeyModifiers::CONTROL);
639 assert!(m.filter.is_empty());
640 }
641
642 #[test]
643 fn a_page_moves_the_focus_and_the_list_alike() {
644 let mut m = InspectorModal::new();
645 let names: Vec<String> = (0..50).map(|i| format!("f{i}")).collect();
646 let names: Vec<&str> = names.iter().map(String::as_str).collect();
647 m.open(fields(&names), None);
648 m.list_page = 10;
649 m.move_field(crate::app::form::ListMove::PageDown);
650 assert_eq!((m.focused_position(), m.list_offset), (10, 10));
651 for _ in 0..4 {
652 m.move_field(crate::app::form::ListMove::PageDown);
653 }
654 assert_eq!(m.focused_position(), 49, "stops at the last");
655 m.move_field(crate::app::form::ListMove::PageUp);
656 assert_eq!(m.focused_position(), 39);
657 }
658
659 #[test]
660 fn a_new_row_drops_the_last_read() {
661 let mut m = InspectorModal::new();
662 m.open(fields(&["a"]), None);
663 m.row_shown(1, 5);
664 m.read = Some(FieldRead::Reading { frame: 1, row: 5 });
665 m.row_shown(1, 5);
666 assert!(m.read.is_some(), "the same row keeps it");
667 m.row_shown(1, 6);
668 assert!(m.read.is_none());
669 }
670
671 #[test]
672 fn reopening_keeps_the_field_while_it_exists() {
673 let mut m = InspectorModal::new();
674 m.open(fields(&["a", "b", "c"]), None);
675 m.move_field(crate::app::form::ListMove::End);
676 m.close();
677 m.open(fields(&["c", "a"]), None);
678 assert_eq!(m.focused().unwrap().name, "c");
679 m.close();
680 m.open(fields(&["x", "y"]), None);
681 assert_eq!(m.focused().unwrap().name, "x");
682 }
683
684 #[test]
685 fn opens_on_the_column_cursors_field() {
686 let mut m = InspectorModal::new();
687 m.open(fields(&["a", "b", "c"]), Some("b"));
688 assert_eq!(m.focused().unwrap().name, "b");
689 m.move_field(crate::app::form::ListMove::End);
690 m.close();
691 m.open(fields(&["a", "b", "c"]), Some("a"));
693 assert_eq!(m.focused().unwrap().name, "a");
694 }
695
696 #[test]
697 fn a_view_is_chosen_for_its_field() {
698 let mut m = InspectorModal::new();
699 m.open(fields(&["a", "b"]), None);
700 m.choose_view(View::Escaped);
701 assert_eq!(m.view, Some(View::Escaped));
702 m.move_field(crate::app::form::ListMove::Down);
703 assert_eq!(m.view, None, "another field starts in its own view");
704 }
705}