use crate::inspector_drill::{Drill, JsonWait, Level, Node};
use crate::inspector_reader::{Reader, Wrap};
use crate::widgets::datatable::{InspectField, InspectRow};
use crate::widgets::inspector::Pane;
use polars::prelude::DataFrame;
use std::sync::Arc;
pub const CHUNK_BYTES: usize = 16 * 1024;
#[derive(Debug, Clone)]
pub enum FieldRead {
Reading { frame: u64, row: usize },
Read {
frame: u64,
row: usize,
values: DataFrame,
},
Failed {
frame: u64,
row: usize,
message: String,
},
}
impl FieldRead {
pub fn key(&self) -> (u64, usize) {
match self {
Self::Reading { frame, row }
| Self::Read { frame, row, .. }
| Self::Failed { frame, row, .. } => (*frame, *row),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Focus {
#[default]
List,
Value,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum Order {
#[default]
Table,
Name,
Filled,
}
impl Order {
pub fn next(self) -> Self {
match self {
Order::Table => Order::Name,
Order::Name => Order::Filled,
Order::Filled => Order::Table,
}
}
pub fn label(self) -> Option<&'static str> {
match self {
Order::Table => None,
Order::Name => Some("A-Z"),
Order::Filled => Some("nulls last"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum View {
Json,
Raw,
Escaped,
Hex,
Text,
}
impl View {
pub fn label(self) -> &'static str {
match self {
View::Json => "JSON",
View::Raw => "Raw",
View::Escaped => "Escaped",
View::Hex => "Hex",
View::Text => "Text",
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ValueFind {
pub text: String,
pub editing: bool,
pub hits: Vec<usize>,
pub current: Option<usize>,
pub pane: u64,
}
#[derive(Debug, Clone)]
pub enum Pretty {
Pending {
token: u64,
place: (u64, usize, String),
},
Ready {
place: (u64, usize, String),
text: Arc<str>,
},
Failed {
place: (u64, usize, String),
},
}
impl Pretty {
pub fn place(&self) -> &(u64, usize, String) {
match self {
Pretty::Pending { place, .. }
| Pretty::Ready { place, .. }
| Pretty::Failed { place } => place,
}
}
}
#[derive(Debug, Clone)]
pub enum Unpack {
Pending {
token: u64,
place: (u64, usize, String),
},
Ready {
place: (u64, usize, String),
text: Arc<crate::inspector_bytes::Decoded>,
},
Failed {
place: (u64, usize, String),
},
}
impl Unpack {
pub fn place(&self) -> &(u64, usize, String) {
match self {
Unpack::Pending { place, .. }
| Unpack::Ready { place, .. }
| Unpack::Failed { place } => place,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PaneKey {
pub frame: u64,
pub row: usize,
pub field: String,
pub view: Option<View>,
pub width: u16,
pub state: u8,
pub pretty: u8,
pub unpacked: u8,
}
impl PaneKey {
pub fn same_value(&self, other: &Self) -> bool {
*self
== Self {
width: self.width,
..other.clone()
}
}
}
#[derive(Default)]
pub struct InspectorModal {
pub active: bool,
pub fields: Vec<InspectField>,
pub filter: String,
pub finding: bool,
selected: usize,
pub visible: Vec<usize>,
pub list_offset: usize,
pub list_page: usize,
pub order: Order,
pub filled_only: bool,
pub focus: Focus,
pub view: Option<View>,
view_field: Option<String>,
pub wrap: Wrap,
pub reader: Reader,
pub pane: Option<(PaneKey, Pane)>,
pane_id: u64,
pub value_find: Option<ValueFind>,
pub page: usize,
pub shown_row: Option<(u64, usize)>,
pub read: Option<FieldRead>,
pub follow: Option<String>,
pub compare: bool,
pub pinned: Option<InspectRow>,
pub compare_both: bool,
pub drill: Option<Drill>,
pub json_wait: Option<JsonWait>,
pub json_token: u64,
pub not_json: Option<(u64, usize, String)>,
pub pretty: Option<Pretty>,
pub pretty_token: u64,
pub unpack: Option<Unpack>,
pub unpack_token: u64,
}
impl InspectorModal {
pub fn new() -> Self {
Self::default()
}
pub fn open(&mut self, fields: Vec<InspectField>, current: Option<&str>) {
let keep = current
.map(str::to_string)
.or_else(|| self.focused().map(|f| f.name.clone()))
.and_then(|name| fields.iter().position(|f| f.name == name));
self.selected = keep.unwrap_or(0);
self.visible = (0..fields.len()).collect();
self.fields = fields;
self.active = true;
self.finding = false;
self.focus = Focus::List;
self.list_offset = 0;
self.read = None;
self.follow = None;
self.pane = None;
self.shown_row = None;
self.drill = None;
self.json_wait = None;
self.not_json = None;
self.pretty = None;
self.unpack = None;
self.value_find = None;
self.reader = Reader::default();
}
pub fn close(&mut self) {
self.active = false;
self.finding = false;
self.focus = Focus::List;
self.read = None;
self.follow = None;
self.pane = None;
self.drill = None;
self.json_wait = None;
self.not_json = None;
self.pretty = None;
self.unpack = None;
self.value_find = None;
}
pub fn next_pane_id(&mut self) -> u64 {
self.pane_id += 1;
self.pane_id
}
pub fn unpacked(&self, place: &(u64, usize, String)) -> crate::widgets::inspector::Unpacked {
use crate::widgets::inspector::Unpacked;
match &self.unpack {
Some(Unpack::Pending { place: p, .. }) if p == place => Unpacked::Pending,
Some(Unpack::Ready { place: p, text }) if p == place => Unpacked::Ready(text.clone()),
Some(Unpack::Failed { place: p }) if p == place => Unpacked::Failed,
_ => Unpacked::None,
}
}
pub fn known_not_json(&self, frame: u64, row: usize, path: &str) -> bool {
self.not_json
.as_ref()
.is_some_and(|(f, r, p)| (*f, *r) == (frame, row) && p == path)
}
pub fn focused(&self) -> Option<&InspectField> {
self.focused_index().and_then(|i| self.fields.get(i))
}
fn focused_index(&self) -> Option<usize> {
if self.visible.contains(&self.selected) {
Some(self.selected)
} else {
self.visible.first().copied()
}
}
pub fn focused_position(&self) -> usize {
self.focused_index()
.and_then(|i| self.visible.iter().position(|&v| v == i))
.unwrap_or(0)
}
pub fn set_visible(&mut self, visible: Vec<usize>) {
if !visible.contains(&self.selected)
&& let Some(&first) = visible.first()
{
self.selected = first;
}
self.visible = visible;
}
fn select_position(&mut self, at: usize) {
if let Some(&i) = self.visible.get(at)
&& i != self.selected
{
self.selected = i;
self.field_changed();
}
}
fn field_changed(&mut self) {
let name = self.focused().map(|f| f.name.clone());
if self.view_field != name {
self.view = None;
}
if self.follow.is_some() && self.follow != name {
self.follow = None;
}
}
fn step(&mut self, delta: isize) -> bool {
let Some(drill) = self.drill.as_mut() else {
return false;
};
let level = drill.level_mut();
let last = level.node.len().saturating_sub(1);
level.selected = level.selected.saturating_add_signed(delta).min(last);
true
}
pub fn next_field(&mut self) {
if self.step(1) || self.visible.is_empty() {
return;
}
let n = self.visible.len();
self.select_position((self.focused_position() + 1) % n);
}
pub fn prev_field(&mut self) {
if self.step(-1) || self.visible.is_empty() {
return;
}
let n = self.visible.len();
self.select_position((self.focused_position() + n - 1) % n);
}
pub fn first_field(&mut self) {
if !self.step(isize::MIN) {
self.select_position(0);
}
}
pub fn last_field(&mut self) {
if !self.step(isize::MAX) {
self.select_position(self.visible.len().saturating_sub(1));
}
}
pub fn page_fields(&mut self, direction: isize) {
let page = self.list_page.max(1) as isize;
if self.step(direction * page) {
return;
}
let last = self.visible.len().saturating_sub(1);
let at = self
.focused_position()
.saturating_add_signed(direction * page)
.min(last);
self.list_offset = self
.list_offset
.saturating_add_signed(direction * page)
.min(last);
self.select_position(at);
}
pub fn choose_view(&mut self, view: View) {
self.view = Some(view);
self.view_field = self.focused().map(|f| f.name.clone());
}
pub fn drill_in(&mut self, frame: u64, row: usize, label: String, node: Node) {
let level = Level {
label,
node,
selected: 0,
};
match self.drill.as_mut() {
Some(drill) if (drill.frame, drill.row) == (frame, row) => drill.levels.push(level),
_ => {
self.drill = Some(Drill {
frame,
row,
levels: vec![level],
})
}
}
self.json_wait = None;
self.focus = Focus::List;
}
pub fn drill_out(&mut self) -> bool {
let Some(drill) = self.drill.as_mut() else {
return false;
};
drill.levels.pop();
if drill.levels.is_empty() {
self.drill = None;
}
self.json_wait = None;
self.focus = Focus::List;
true
}
pub fn wait_for_json(&mut self, frame: u64, row: usize, label: String, path: String) -> u64 {
self.json_token += 1;
self.json_wait = Some(JsonWait {
token: self.json_token,
frame,
row,
label,
path,
});
self.json_token
}
pub fn find_key(&mut self, c: char, mods: crossterm::event::KeyModifiers) {
edit_find(&mut self.filter, c, mods);
}
pub fn find_backspace(&mut self) {
self.filter.pop();
}
pub fn clear_find(&mut self) {
self.filter.clear();
self.finding = false;
}
pub fn row_shown(&mut self, frame: u64, row: usize) {
if self.shown_row != Some((frame, row)) {
self.shown_row = Some((frame, row));
if self
.drill
.as_ref()
.is_some_and(|d| (d.frame, d.row) != (frame, row))
{
self.drill = None;
}
if self
.json_wait
.as_ref()
.is_some_and(|w| (w.frame, w.row) != (frame, row))
{
self.json_wait = None;
}
if self.read.as_ref().is_some_and(|r| r.key() != (frame, row)) {
self.read = None;
}
if self
.pretty
.as_ref()
.is_some_and(|p| (p.place().0, p.place().1) != (frame, row))
{
self.pretty = None;
}
if self
.unpack
.as_ref()
.is_some_and(|u| (u.place().0, u.place().1) != (frame, row))
{
self.unpack = None;
}
}
}
pub fn read_values(&self, frame: u64, row: usize) -> Option<&DataFrame> {
match &self.read {
Some(FieldRead::Read {
frame: f,
row: r,
values,
}) if (*f, *r) == (frame, row) => Some(values),
_ => None,
}
}
pub fn pane_for(&self, frame: u64, row: usize, field: &str) -> Option<&Pane> {
self.pane
.as_ref()
.filter(|(key, _)| (key.frame, key.row) == (frame, row) && key.field == field)
.map(|(_, pane)| pane)
}
}
pub fn edit_find(text: &mut String, c: char, mods: crossterm::event::KeyModifiers) {
use crossterm::event::KeyModifiers;
let ctrl = mods.contains(KeyModifiers::CONTROL);
if ctrl && c == 'w' {
while text.ends_with(' ') {
text.pop();
}
while text.chars().next_back().is_some_and(|c| c != ' ') {
text.pop();
}
} else if ctrl && c == 'u' {
text.clear();
} else if !ctrl && !mods.contains(KeyModifiers::ALT) {
text.push(c);
}
}
#[cfg(test)]
mod tests {
use super::*;
use crossterm::event::KeyModifiers;
use polars::prelude::DataType;
fn fields(names: &[&str]) -> Vec<InspectField> {
names
.iter()
.map(|n| InspectField {
name: n.to_string(),
dtype: DataType::String,
hidden: false,
})
.collect()
}
#[test]
fn the_focus_moves_among_the_fields_listed() {
let mut m = InspectorModal::new();
m.open(fields(&["id", "description", "amount", "status"]), None);
m.next_field();
assert_eq!(m.focused().unwrap().name, "description");
m.set_visible(vec![2, 3]);
assert_eq!(
m.focused().unwrap().name,
"amount",
"unlisted: the first listed"
);
m.next_field();
assert_eq!(m.focused().unwrap().name, "status");
m.next_field();
assert_eq!(m.focused().unwrap().name, "amount", "round to the top");
m.set_visible(vec![0, 1, 2, 3]);
assert_eq!(m.focused().unwrap().name, "amount");
m.find_key('a', KeyModifiers::NONE);
m.find_key('m', KeyModifiers::CONTROL);
assert_eq!(m.filter, "a", "a chord types nothing");
m.find_key('w', KeyModifiers::CONTROL);
assert!(m.filter.is_empty());
}
#[test]
fn a_page_moves_the_focus_and_the_list_alike() {
let mut m = InspectorModal::new();
let names: Vec<String> = (0..50).map(|i| format!("f{i}")).collect();
let names: Vec<&str> = names.iter().map(String::as_str).collect();
m.open(fields(&names), None);
m.list_page = 10;
m.page_fields(1);
assert_eq!((m.focused_position(), m.list_offset), (10, 10));
for _ in 0..4 {
m.page_fields(1);
}
assert_eq!(m.focused_position(), 49, "stops at the last");
m.page_fields(-1);
assert_eq!(m.focused_position(), 39);
}
#[test]
fn a_new_row_drops_the_last_read() {
let mut m = InspectorModal::new();
m.open(fields(&["a"]), None);
m.row_shown(1, 5);
m.read = Some(FieldRead::Reading { frame: 1, row: 5 });
m.row_shown(1, 5);
assert!(m.read.is_some(), "the same row keeps it");
m.row_shown(1, 6);
assert!(m.read.is_none());
}
#[test]
fn reopening_keeps_the_field_while_it_exists() {
let mut m = InspectorModal::new();
m.open(fields(&["a", "b", "c"]), None);
m.last_field();
m.close();
m.open(fields(&["c", "a"]), None);
assert_eq!(m.focused().unwrap().name, "c");
m.close();
m.open(fields(&["x", "y"]), None);
assert_eq!(m.focused().unwrap().name, "x");
}
#[test]
fn opens_on_the_column_cursors_field() {
let mut m = InspectorModal::new();
m.open(fields(&["a", "b", "c"]), Some("b"));
assert_eq!(m.focused().unwrap().name, "b");
m.last_field();
m.close();
m.open(fields(&["a", "b", "c"]), Some("a"));
assert_eq!(m.focused().unwrap().name, "a");
}
#[test]
fn a_view_is_chosen_for_its_field() {
let mut m = InspectorModal::new();
m.open(fields(&["a", "b"]), None);
m.choose_view(View::Escaped);
assert_eq!(m.view, Some(View::Escaped));
m.next_field();
assert_eq!(m.view, None, "another field starts in its own view");
}
}