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