1use crate::copy_modal::thousands;
12use crate::exact;
13use crate::inspector_bytes::{self, Decoded, Sniffed};
14use crate::inspector_drill::{JSON_INLINE_BYTES, Node, Shape, json_text, looks_like_json};
15use crate::inspector_modal::{
16 CHUNK_BYTES, FieldRead, Focus, InspectorModal, Order, PaneKey, Pretty, View,
17};
18pub use crate::inspector_reader::Tone;
19use crate::inspector_reader::{self as reader, Content, TextForm, Window};
20use crate::render::context::RenderContext;
21use crate::widgets::datatable::{DataTableState, InspectField, InspectRow, NullKind, dtype_label};
22use crate::widgets::ui::{HintBar, SectionRule, Surface};
23use polars::prelude::*;
24use ratatui::buffer::Buffer;
25use ratatui::layout::Rect;
26use ratatui::style::{Modifier, Style};
27use ratatui::text::{Line, Span};
28use ratatui::widgets::{Paragraph, Widget};
29use serde_json::Value as JsonValue;
30use std::sync::Arc;
31
32const MEASURE: usize = 100;
35const GAP: usize = 2;
37const VALUE_MIN: usize = 3;
39pub const WIDE: usize = 136;
42pub const WIDER: usize = 236;
45const PANE_GAP: usize = 3;
47const VALUE_FLOOR: usize = 56;
50const HEX_WIDE: usize = 1 + 8 + 1 + 32 * 3 + 2 + 32;
53const PREVIEW_MIN: usize = 14;
55pub const PRETTY_MAX: usize = 1024 * 1024;
57const ESCAPED_BYTES: usize = 64 * 1024;
59const MATCH_BYTES: usize = 4096;
61
62#[derive(Debug, Clone)]
64pub enum Shown<'a> {
65 Value(AnyValue<'a>),
66 Null(NullKind),
67 Unread,
69 Reading,
70 Failed(&'a str),
71}
72
73impl Shown<'_> {
74 fn kind(&self) -> u8 {
76 match self {
77 Self::Value(_) => 0,
78 Self::Null(_) => 1,
79 Self::Unread => 2,
80 Self::Reading => 3,
81 Self::Failed(_) => 4,
82 }
83 }
84}
85
86pub fn shown<'a>(
88 field: &InspectField,
89 row: &'a InspectRow,
90 read: Option<&'a FieldRead>,
91 state: &DataTableState,
92) -> Shown<'a> {
93 let value = if field.buffered() {
94 row.values
95 .column(&field.name)
96 .ok()
97 .and_then(|c| c.get(0).ok())
98 } else {
99 match read {
100 Some(r) if r.key() != (row.frame, row.row) => return Shown::Unread,
101 Some(FieldRead::Read { values, .. }) => {
102 values.column(&field.name).ok().and_then(|c| c.get(0).ok())
103 }
104 Some(FieldRead::Reading { .. }) => return Shown::Reading,
105 Some(FieldRead::Failed { message, .. }) => return Shown::Failed(message),
106 None => return Shown::Unread,
107 }
108 };
109 match value {
110 None => Shown::Unread,
111 Some(AnyValue::Null) => Shown::Null(state.null_kind(&field.name, row.drift_group)),
112 Some(v) => Shown::Value(v),
113 }
114}
115
116#[derive(Debug, Clone, Copy, PartialEq, Eq)]
119pub enum Fill {
120 Value,
121 Null,
122 Empty,
123 Unknown,
124}
125
126pub fn fill_of(shown: &Shown) -> Fill {
127 match shown {
128 Shown::Null(_) => Fill::Null,
129 Shown::Value(v) if empty_preview(v).is_some() => Fill::Empty,
130 Shown::Value(_) => Fill::Value,
131 _ => Fill::Unknown,
132 }
133}
134
135pub fn differs(a: &Shown, b: &Shown) -> Option<bool> {
137 match (a, b) {
138 (Shown::Value(x), Shown::Value(y)) => {
139 if x == y {
140 return Some(false);
141 }
142 let nested = exact::is_nested_value(x) || exact::is_nested_value(y);
144 Some(nested || exact::value_text(x) != exact::value_text(y))
145 }
146 (Shown::Null(_), Shown::Null(_)) => Some(false),
147 (Shown::Null(_), Shown::Value(_)) | (Shown::Value(_), Shown::Null(_)) => Some(true),
148 _ => None,
149 }
150}
151
152pub fn type_text(dtype: &DataType) -> String {
155 let unit = |u: &TimeUnit| match u {
156 TimeUnit::Milliseconds => "ms",
157 TimeUnit::Microseconds => "us",
158 TimeUnit::Nanoseconds => "ns",
159 };
160 match dtype {
161 DataType::Datetime(u, Some(tz)) => format!("datetime[{}, {tz}]", unit(u)),
162 DataType::Datetime(u, None) => format!("datetime[{}]", unit(u)),
163 DataType::Duration(u) => format!("duration[{}]", unit(u)),
164 DataType::Decimal(p, s) => format!("decimal({p},{s})"),
165 DataType::List(inner) => format!("list[{}]", type_text(inner)),
166 DataType::Array(inner, n) => format!("array[{}; {n}]", type_text(inner)),
167 other => dtype_label(other),
168 }
169}
170
171fn plural(n: usize, one: &str, many: &str) -> String {
172 format!("{} {}", thousands(n), if n == 1 { one } else { many })
173}
174
175pub fn size_text(n: usize) -> String {
177 if n < 1024 {
178 plural(n, "byte", "bytes")
179 } else {
180 format!(
181 "{} ({} bytes)",
182 crate::discover::format_size(n as u64),
183 thousands(n)
184 )
185 }
186}
187
188#[derive(Debug, Clone)]
190pub enum CopyAs {
191 Stored,
193 Base64,
195 Text(Arc<str>),
197 Escaped,
199}
200
201#[derive(Debug, Clone)]
204pub struct Pane {
205 pub id: u64,
206 pub facts: String,
207 pub content: Content,
208 pub views: Vec<View>,
210 pub view: Option<View>,
211 pub copy: CopyAs,
212 pub indent: bool,
214 pub unpack: bool,
216}
217
218impl Pane {
219 fn lines(lines: Vec<(String, Tone)>, facts: Vec<String>) -> Self {
220 Self {
221 id: 0,
222 facts: join_facts(&facts),
223 content: Content::Lines(lines),
224 views: Vec::new(),
225 view: None,
226 copy: CopyAs::Stored,
227 indent: false,
228 unpack: false,
229 }
230 }
231
232 pub fn next_view(&self) -> Option<View> {
234 if self.views.len() < 2 {
235 return None;
236 }
237 let at = self
238 .view
239 .and_then(|v| self.views.iter().position(|w| *w == v))
240 .unwrap_or(0);
241 Some(self.views[(at + 1) % self.views.len()])
242 }
243}
244
245fn join_facts(facts: &[String]) -> String {
246 facts.join(&format!(" {} ", crate::glyphs::get().middot))
247}
248
249#[derive(Debug, Clone)]
251pub enum Indented {
252 None,
254 Pending,
255 Ready(Arc<str>),
256 Failed,
257}
258
259impl Indented {
260 fn code(&self) -> u8 {
261 match self {
262 Indented::None => 0,
263 Indented::Pending => 1,
264 Indented::Ready(_) => 2,
265 Indented::Failed => 3,
266 }
267 }
268}
269
270fn text_pane(
272 s: &str,
273 kind: String,
274 choice: Option<View>,
275 indented: &Indented,
276 not_json: bool,
277) -> Pane {
278 let f = exact::text_facts(s);
279 let mut facts = vec![kind];
280 facts.push(if s.is_empty() {
281 "empty".to_string()
282 } else {
283 plural(f.chars, "char", "chars")
284 });
285 if f.lines > 1 {
286 facts.push(plural(f.lines, "line", "lines"));
287 }
288 if f.leading_spaces > 0 {
289 facts.push(plural(f.leading_spaces, "leading space", "leading spaces"));
290 }
291 if f.trailing_spaces > 0 {
292 facts.push(plural(
293 f.trailing_spaces,
294 "trailing space",
295 "trailing spaces",
296 ));
297 }
298 let json = !not_json && s.len() <= PRETTY_MAX && looks_like_json(s);
299 let (json_ok, pretty) = if json && s.len() <= JSON_INLINE_BYTES {
300 match serde_json::from_str::<JsonValue>(s) {
301 Ok(v) => (
302 true,
303 Some(Arc::<str>::from(json_text(&v, true, usize::MAX).0)),
304 ),
305 Err(_) => (false, None),
306 }
307 } else if json {
308 match indented {
309 Indented::Ready(text) => (true, Some(text.clone())),
310 Indented::Failed => (false, None),
311 _ => (true, None),
312 }
313 } else {
314 (false, None)
315 };
316 let mut views = Vec::new();
317 if json_ok {
318 views.push(View::Json);
319 }
320 views.extend([View::Raw, View::Escaped]);
321 let view = choice.filter(|v| views.contains(v)).unwrap_or(views[0]);
322 let mut pane = Pane {
323 id: 0,
324 facts: String::new(),
325 content: Content::Lines(Vec::new()),
326 views,
327 view: Some(view),
328 copy: CopyAs::Stored,
329 indent: false,
330 unpack: false,
331 };
332 match view {
333 View::Json => match pretty {
334 Some(text) => {
335 facts.push("json".to_string());
336 pane.content = Content::text(text.clone(), TextForm::Raw);
337 pane.copy = CopyAs::Text(text);
338 }
339 None => {
340 facts.push("json, indenting...".to_string());
341 pane.content = Content::text(Arc::from(s), TextForm::Raw);
342 pane.indent = true;
343 }
344 },
345 View::Escaped => {
346 facts.push("escaped".to_string());
347 pane.content = Content::text(Arc::from(s), TextForm::Escaped);
348 pane.copy = CopyAs::Escaped;
349 }
350 _ if s.is_empty() => {
351 pane.content = Content::Lines(vec![("empty string".to_string(), Tone::Dim)]);
352 }
353 _ => pane.content = Content::text(Arc::from(s), TextForm::Raw),
354 }
355 pane.facts = join_facts(&facts);
356 pane
357}
358
359#[derive(Debug, Clone)]
361pub enum Unpacked {
362 Unavailable,
365 None,
367 Pending,
368 Ready(Arc<Decoded>),
369 Failed,
370}
371
372impl Unpacked {
373 fn code(&self) -> u8 {
374 match self {
375 Unpacked::Unavailable => 0,
376 Unpacked::None => 1,
377 Unpacked::Pending => 2,
378 Unpacked::Ready(_) => 3,
379 Unpacked::Failed => 4,
380 }
381 }
382}
383
384fn binary_pane(bytes: &[u8], choice: Option<View>, width: usize, unpacked: &Unpacked) -> Pane {
388 let sniffed = inspector_bytes::sniff(bytes);
389 let mut facts = vec!["binary".to_string(), size_text(bytes.len())];
390 if bytes.is_empty() {
391 facts.push("empty".to_string());
392 }
393 if let Some(kind) = sniffed.filter(|k| *k != Sniffed::Utf8) {
394 facts.push(kind.label());
395 }
396 let compressed = matches!(sniffed, Some(Sniffed::Gzip | Sniffed::Zstd));
397 let decoded = match unpacked {
398 Unpacked::Ready(d) if compressed => Some(Decoded::clone(d)),
399 _ if compressed => None,
400 _ => inspector_bytes::decode_text(bytes, sniffed),
401 };
402 if compressed && matches!(unpacked, Unpacked::Failed) {
403 facts.push("not text".to_string());
404 }
405 let unpacks = compressed && matches!(unpacked, Unpacked::None | Unpacked::Pending);
406 let mut views = Vec::new();
407 if decoded.is_some() && sniffed == Some(Sniffed::Utf8) {
408 views.push(View::Text);
409 }
410 views.push(View::Hex);
411 if (decoded.is_some() || unpacks) && sniffed != Some(Sniffed::Utf8) {
412 views.push(View::Text);
413 }
414 views.push(View::Escaped);
415 let view = choice.filter(|v| views.contains(v)).unwrap_or(views[0]);
416 let mut pane = Pane {
417 id: 0,
418 facts: String::new(),
419 content: Content::Lines(Vec::new()),
420 views,
421 view: Some(view),
422 copy: CopyAs::Base64,
423 indent: false,
424 unpack: false,
425 };
426 match (view, decoded) {
427 (View::Text, Some(d)) => {
428 facts.push(if d.from == "UTF-8" {
429 "UTF-8 text".to_string()
430 } else {
431 format!("{} text", d.from)
432 });
433 if d.cut {
434 facts.push(format!("first {} KB", inspector_bytes::DECODE_MAX / 1024));
435 }
436 let text: Arc<str> = Arc::from(d.text);
437 pane.content = Content::text(text.clone(), TextForm::Raw);
438 pane.copy = CopyAs::Text(text);
439 }
440 (View::Text, None) => {
441 facts.push("decompressing...".to_string());
442 pane.content = Content::Lines(vec![("Decompressing...".to_string(), Tone::Dim)]);
443 pane.unpack = true;
444 }
445 (View::Escaped, _) => {
446 let head = &bytes[..bytes.len().min(ESCAPED_BYTES)];
447 let mut literal = exact::escaped_bytes(head);
448 if head.len() < bytes.len() {
449 literal.pop();
450 facts.push(format!("first {} KB", ESCAPED_BYTES / 1024));
451 }
452 facts.push("escaped".to_string());
453 pane.content = Content::text(Arc::from(literal), TextForm::Raw);
454 }
455 _ if bytes.is_empty() => {
456 pane.content = Content::Lines(vec![("empty binary".to_string(), Tone::Dim)]);
457 }
458 _ => {
459 pane.content = Content::Hex {
460 bytes: Arc::from(bytes),
461 per_line: reader::hex_per_line(width),
462 };
463 }
464 }
465 pane.facts = join_facts(&facts);
466 pane
467}
468
469pub struct PaneAsk<'a> {
471 pub choice: Option<View>,
472 pub width: usize,
475 pub table: Option<&'a str>,
477 pub indented: Indented,
478 pub not_json: bool,
480 pub unpacked: Unpacked,
482 pub read_key: &'a str,
484}
485
486pub fn pane(dtype: &DataType, shown: &Shown, ask: &PaneAsk) -> Pane {
488 let g = crate::glyphs::get();
489 let width = ask.width.clamp(1, MEASURE);
490 let kind = type_text(dtype);
491 let mut lines = Vec::new();
492 match shown {
493 Shown::Unread => {
494 reader::wrap_lines(
495 &format!(
496 "Not read {} {} reads hidden and binary fields",
497 g.middot, ask.read_key
498 ),
499 width,
500 Tone::Dim,
501 &mut lines,
502 );
503 Pane::lines(lines, vec![kind, "not read".to_string()])
504 }
505 Shown::Reading => {
506 reader::wrap_lines("Reading...", width, Tone::Dim, &mut lines);
507 Pane::lines(lines, vec![kind])
508 }
509 Shown::Failed(message) => {
510 reader::wrap_lines(message, width, Tone::Warn, &mut lines);
511 Pane::lines(lines, vec![kind])
512 }
513 Shown::Null(null) => {
514 let (glyph, word, why) = match null {
515 NullKind::Null => (g.null, "null", None),
516 NullKind::Absent => (g.absent, "absent", Some("not in this row's file")),
517 NullKind::Conflict => (
518 g.conflict,
519 "conflicting",
520 Some("another type in this row's file, not read"),
521 ),
522 };
523 let text = match why {
524 Some(why) => format!("{glyph} {word}: {why}"),
525 None => format!("{glyph} {word}"),
526 };
527 reader::wrap_lines(&text, width, Tone::Dim, &mut lines);
528 Pane::lines(lines, vec![kind, word.to_string()])
529 }
530 Shown::Value(value) => match value {
531 AnyValue::String(s) => text_pane(s, kind, ask.choice, &ask.indented, ask.not_json),
532 AnyValue::StringOwned(s) => {
533 text_pane(s.as_str(), kind, ask.choice, &ask.indented, ask.not_json)
534 }
535 AnyValue::Categorical(..)
536 | AnyValue::CategoricalOwned(..)
537 | AnyValue::Enum(..)
538 | AnyValue::EnumOwned(..) => {
539 let s = exact::value_text(value);
540 text_pane(&s, kind, ask.choice, &Indented::None, true)
541 }
542 AnyValue::Binary(b) => binary_pane(b, ask.choice, ask.width, &ask.unpacked),
543 AnyValue::BinaryOwned(b) => binary_pane(b, ask.choice, ask.width, &ask.unpacked),
544 v if exact::is_nested_value(v) => {
545 let mut facts = vec![kind];
546 if let Some(n) = exact::nested_len(v) {
547 facts.push(match v {
548 AnyValue::Struct(..) | AnyValue::StructOwned(_) => {
549 plural(n, "field", "fields")
550 }
551 _ => plural(n, "item", "items"),
552 });
553 }
554 let pretty = exact::nested_pretty(v, CHUNK_BYTES);
555 if pretty.cut {
556 facts.push(format!("first {} KB", CHUNK_BYTES / 1024));
557 }
558 Pane {
559 id: 0,
560 facts: join_facts(&facts),
561 content: Content::text(Arc::from(pretty.text), TextForm::Raw),
562 views: Vec::new(),
563 view: None,
564 copy: CopyAs::Stored,
565 indent: false,
566 unpack: false,
567 }
568 }
569 v => {
570 let text = exact::value_text(v);
571 reader::wrap_lines(&text, width, Tone::Plain, &mut lines);
572 if let Some(table) = ask.table.filter(|t| *t != text) {
573 lines.push((String::new(), Tone::Plain));
574 reader::wrap_lines(
575 &format!("In the table: {table}"),
576 width,
577 Tone::Dim,
578 &mut lines,
579 );
580 }
581 Pane::lines(lines, vec![kind])
582 }
583 },
584 }
585}
586
587fn empty_preview(value: &AnyValue) -> Option<String> {
590 let g = crate::glyphs::get();
591 match value {
592 AnyValue::String("") => Some("\"\"".to_string()),
593 AnyValue::StringOwned(s) if s.is_empty() => Some("\"\"".to_string()),
594 AnyValue::Binary([]) => Some(format!("0 bytes {} empty", g.middot)),
595 AnyValue::BinaryOwned(b) if b.is_empty() => Some(format!("0 bytes {} empty", g.middot)),
596 _ => None,
597 }
598}
599
600fn preview(field: &InspectField, value: &AnyValue, room: usize, ctx: &RenderContext) -> String {
604 let g = crate::glyphs::get();
605 let budget = room.saturating_mul(4).max(16);
606 let sized = |text: String, len: usize| {
607 if len > 1024 && crate::glyphs::cell_width(&text) > room {
608 let size = format!(" {} {}", g.middot, crate::discover::format_size(len as u64));
609 let keep = room.saturating_sub(crate::glyphs::cell_width(&size));
610 let cut = crate::glyphs::fit_cells(&text, keep, g.ellipsis).into_owned();
611 format!("{cut}{size}")
612 } else {
613 text
614 }
615 };
616 match value {
617 AnyValue::String(s) => sized(
618 exact::preview(exact::prefix(s, budget), g).into_owned(),
619 s.len(),
620 ),
621 AnyValue::StringOwned(s) => sized(
622 exact::preview(exact::prefix(s, budget), g).into_owned(),
623 s.len(),
624 ),
625 AnyValue::Binary(b) => binary_preview(b),
626 AnyValue::BinaryOwned(b) => binary_preview(b),
627 v if exact::is_nested_value(v) => {
628 exact::preview(&exact::nested_compact(v, budget).text, g).into_owned()
629 }
630 v => {
631 let formatter = ctx.number_format.formatter_for(&field.name, &field.dtype);
632 let mut scratch = String::new();
633 let text = crate::numfmt::format_any_value(&formatter, v, &mut scratch);
634 exact::preview(&text, g).into_owned()
635 }
636 }
637}
638
639fn binary_preview(b: &[u8]) -> String {
641 let size = if b.len() < 1024 {
642 plural(b.len(), "byte", "bytes")
643 } else {
644 crate::discover::format_size(b.len() as u64)
645 };
646 match inspector_bytes::sniff(&b[..b.len().min(4096)]) {
647 Some(Sniffed::Utf8) | None => size,
649 Some(kind) => format!("{size} {} {}", crate::glyphs::get().middot, kind.label()),
650 }
651}
652
653fn match_text(shown: &Shown) -> Option<String> {
655 let Shown::Value(v) = shown else {
656 return None;
657 };
658 Some(
659 match v {
660 AnyValue::String(s) => exact::prefix(s, MATCH_BYTES).to_string(),
661 AnyValue::StringOwned(s) => exact::prefix(s, MATCH_BYTES).to_string(),
662 AnyValue::Binary(_) | AnyValue::BinaryOwned(_) => return None,
663 v if exact::is_nested_value(v) => exact::nested_compact(v, MATCH_BYTES).text,
664 v => exact::value_text(v),
665 }
666 .to_lowercase(),
667 )
668}
669
670fn is_scalar(value: &AnyValue) -> bool {
672 !matches!(
673 value,
674 AnyValue::String(_)
675 | AnyValue::StringOwned(_)
676 | AnyValue::Binary(_)
677 | AnyValue::BinaryOwned(_)
678 ) && !exact::is_nested_value(value)
679}
680
681fn null_glyph(kind: NullKind) -> &'static str {
682 let g = crate::glyphs::get();
683 match kind {
684 NullKind::Null => g.null,
685 NullKind::Absent => g.absent,
686 NullKind::Conflict => g.conflict,
687 }
688}
689
690#[derive(Clone)]
693pub struct Compared {
694 pub before: Option<InspectRow>,
697 pub after: Option<InspectRow>,
698 pub both: bool,
700 pub pinned: bool,
701}
702
703impl Compared {
704 pub fn rows(&self) -> impl Iterator<Item = &InspectRow> {
706 self.before.iter().chain(self.after.iter())
707 }
708}
709
710pub fn compared(
712 modal: &InspectorModal,
713 state: &DataTableState,
714 row: &InspectRow,
715) -> Option<Compared> {
716 if !modal.compare {
717 return None;
718 }
719 if let Some(pinned) = &modal.pinned
720 && (pinned.frame, pinned.row) != (row.frame, row.row)
721 {
722 return Some(Compared {
723 before: None,
724 after: Some(pinned.clone()),
725 both: false,
726 pinned: true,
727 });
728 }
729 let after = state.inspect_row_at(row.row + 1);
730 let before = modal
731 .compare_both
732 .then(|| row.row.checked_sub(1))
733 .flatten()
734 .and_then(|r| state.inspect_row_at(r));
735 (after.is_some() || before.is_some()).then_some(Compared {
736 before,
737 after,
738 both: modal.compare_both,
739 pinned: false,
740 })
741}
742
743fn differs_from(
746 this: &Shown,
747 field: &InspectField,
748 other: &Compared,
749 state: &DataTableState,
750) -> Option<bool> {
751 let mut known = None;
752 for row in other.rows() {
753 match differs(this, &shown(field, row, None, state)) {
754 Some(true) => return Some(true),
755 Some(false) => known = Some(false),
756 None => {}
757 }
758 }
759 known
760}
761
762pub fn visible_fields(modal: &InspectorModal, state: &DataTableState) -> Vec<usize> {
766 let fields = &modal.fields;
767 let row = state.inspect_row();
768 let mut order: Vec<usize> = (0..fields.len()).collect();
769 if modal.order == Order::Name {
770 order.sort_by_cached_key(|&i| fields[i].name.to_lowercase());
771 }
772 let Some(row) = row else {
773 return order;
774 };
775 let read = modal.read.as_ref();
776 let shown_at = |i: usize| shown(&fields[i], &row, read, state);
777 if modal.order == Order::Filled {
778 order.sort_by_key(|&i| fill_of(&shown_at(i)) != Fill::Value);
779 }
780 if modal.filled_only {
781 match compared(modal, state, &row) {
782 Some(other) => order
783 .retain(|&i| differs_from(&shown_at(i), &fields[i], &other, state) == Some(true)),
784 None => order.retain(|&i| matches!(fill_of(&shown_at(i)), Fill::Value | Fill::Unknown)),
785 }
786 }
787 if !modal.filter.is_empty() {
788 let needle = modal.filter.to_lowercase();
789 let (names, rest): (Vec<usize>, Vec<usize>) = order
790 .into_iter()
791 .partition(|&i| fields[i].name.to_lowercase().contains(&needle));
792 let values = rest
793 .into_iter()
794 .filter(|&i| match_text(&shown_at(i)).is_some_and(|t| t.contains(&needle)));
795 order = names.into_iter().chain(values).collect();
796 }
797 order
798}
799
800struct Counts {
803 nulls: usize,
804 empties: usize,
805 differ: Option<usize>,
806}
807
808fn counts(
809 modal: &InspectorModal,
810 state: &DataTableState,
811 row: &InspectRow,
812 other: Option<&Compared>,
813) -> Counts {
814 let mut c = Counts {
815 nulls: 0,
816 empties: 0,
817 differ: other.map(|_| 0),
818 };
819 for field in &modal.fields {
820 let this = shown(field, row, modal.read.as_ref(), state);
821 match fill_of(&this) {
822 Fill::Null => c.nulls += 1,
823 Fill::Empty => c.empties += 1,
824 _ => {}
825 }
826 if let (Some(other), Some(n)) = (other, c.differ.as_mut())
827 && differs_from(&this, field, other, state) == Some(true)
828 {
829 *n += 1;
830 }
831 }
832 c
833}
834
835fn title(display_row: usize, state: &DataTableState, other: Option<&Compared>) -> String {
838 let g = crate::glyphs::get();
839 let mut title = format!("Row {}", thousands(display_row));
840 if let Some(total) = state.num_rows_if_valid() {
841 title.push_str(&format!(" of {}", thousands(total)));
842 }
843 if state.is_drilled_down()
844 && let Some((columns, values)) = state.drilled_group_key()
845 {
846 let key: Vec<String> = columns
847 .iter()
848 .zip(values)
849 .map(|(c, v)| format!("{c}={v}"))
850 .collect();
851 if !key.is_empty() {
852 title.push_str(&format!(
853 " {} {}",
854 g.middot,
855 key.join(&format!(" {} ", g.middot))
856 ));
857 }
858 }
859 if let Some(other) = other {
860 let pinned = if other.pinned { "pinned " } else { "" };
861 let rows: Vec<String> = other.rows().map(|r| thousands(r.display_row)).collect();
862 title.push_str(&format!(
863 " {} compare with {pinned}{}",
864 g.middot,
865 rows.join(" and ")
866 ));
867 }
868 title
869}
870
871pub fn list_window(n: usize, sel: usize, offset: usize, cap: usize) -> (usize, bool, bool) {
875 if n <= cap {
876 return (0, false, false);
877 }
878 if cap < 3 {
879 let o = sel.min(n.saturating_sub(cap));
880 return (o, false, false);
881 }
882 let fits = |o: usize| {
883 let above = o > 0;
884 let room = cap - usize::from(above);
885 let below = o + room < n;
886 (above, below, room - usize::from(below))
887 };
888 let mut o = offset.min(n + 1 - cap);
890 for _ in 0..4 {
891 let (above, below, items) = fits(o);
892 if sel < o {
893 o = sel;
894 } else if sel >= o + items {
895 o += sel + 1 - (o + items);
896 } else {
897 return (o, above, below);
898 }
899 }
900 let (above, below, _) = fits(o);
901 (o, above, below)
902}
903
904#[derive(Debug, Clone, Copy, PartialEq, Eq)]
906pub struct ListShape {
907 pub fields: usize,
909 pub min_col: usize,
911 pub single: bool,
913 pub bytes: bool,
916}
917
918fn value_pane_width(width: usize, rows: usize, list: ListShape) -> usize {
925 let mut max = (MEASURE + 1).min(width * 45 / 100);
926 if list.bytes && !list.single && width >= WIDER {
927 max = HEX_WIDE;
928 }
929 if list.single {
930 return max;
931 }
932 let floor = VALUE_FLOOR.min(max);
933 let need = list.fields.div_ceil(rows.max(1)).max(1);
934 let fit = ((width.saturating_sub(floor + PANE_GAP) + GAP) / (list.min_col + GAP)).max(1);
935 let cols = need.min(fit);
936 let list_w = cols * list.min_col + (cols - 1) * GAP;
937 width.saturating_sub(list_w + PANE_GAP).clamp(floor, max)
938}
939
940#[derive(Debug, Clone, Copy, PartialEq, Eq)]
942pub struct Layout {
943 pub wide: bool,
944 pub list_rule: Rect,
945 pub list: Rect,
946 pub cols: usize,
948 pub value_rule: Rect,
949 pub value: Rect,
951}
952
953pub fn layout(content: Rect, fields: usize, value_need: usize, list: ListShape) -> Layout {
961 let line = |y: u16, x: u16, width: u16| Rect {
962 x,
963 y,
964 width,
965 height: 1,
966 };
967 let width = content.width as usize;
968 let h = content.height as usize;
969 if width >= WIDE {
970 let rows = h.saturating_sub(1).max(1);
971 let value_w = value_pane_width(width, rows, list);
972 let list_w = width - value_w - PANE_GAP;
973 let mut cols = ((list_w + GAP) / (list.min_col + GAP)).max(1);
974 if list.single {
975 cols = 1;
976 }
977 while cols > 1 && (cols - 1) * rows >= fields {
978 cols -= 1;
979 }
980 let value_x = content.x + (list_w + PANE_GAP) as u16;
981 return Layout {
982 wide: true,
983 list_rule: line(content.y, content.x, list_w as u16),
984 list: Rect {
985 x: content.x,
986 y: content.y + 1,
987 width: list_w as u16,
988 height: rows as u16,
989 },
990 cols,
991 value_rule: line(content.y, value_x, value_w as u16),
992 value: Rect {
993 x: value_x,
994 y: content.y + 1,
995 width: value_w as u16,
996 height: rows as u16,
997 },
998 };
999 }
1000 let avail = h.saturating_sub(2);
1001 let fields = fields.max(1);
1002 let list_h = if fields + value_need.max(VALUE_MIN) <= avail {
1003 fields
1004 } else {
1005 let value_h = value_need.clamp(VALUE_MIN, (avail / 2).max(VALUE_MIN));
1008 avail.saturating_sub(value_h).clamp(1, fields)
1009 }
1010 .min(avail.saturating_sub(1))
1011 .max(1);
1012 let value_y = content.y + 1 + list_h as u16;
1013 Layout {
1014 wide: false,
1015 list_rule: line(content.y, content.x, content.width),
1016 list: Rect {
1017 x: content.x,
1018 y: content.y + 1,
1019 width: content.width,
1020 height: list_h as u16,
1021 },
1022 cols: 1,
1023 value_rule: line(value_y, content.x, content.width),
1024 value: Rect {
1025 x: content.x,
1026 y: value_y + 1,
1027 width: content.width,
1028 height: (content.y + content.height).saturating_sub(value_y + 1),
1029 },
1030 }
1031}
1032
1033fn pane_width(value: Rect) -> usize {
1035 (value.width as usize).saturating_sub(1).max(1)
1036}
1037
1038fn value_width(value: Rect) -> usize {
1040 pane_width(value).min(MEASURE)
1041}
1042
1043fn field_pane(
1045 modal: &mut InspectorModal,
1046 state: &DataTableState,
1047 row: &InspectRow,
1048 field: &InspectField,
1049 width: usize,
1050 ctx: &RenderContext,
1051) -> Pane {
1052 let value = shown(field, row, modal.read.as_ref(), state);
1053 let place = (row.frame, row.row, field.name.clone());
1054 let indented = match &modal.pretty {
1055 Some(Pretty::Pending { place: p, .. }) if *p == place => Indented::Pending,
1056 Some(Pretty::Ready { place: p, text }) if *p == place => Indented::Ready(text.clone()),
1057 Some(Pretty::Failed { place: p }) if *p == place => Indented::Failed,
1058 _ => Indented::None,
1059 };
1060 let unpacked = modal.unpacked(&place);
1061 let key = PaneKey {
1062 frame: row.frame,
1063 row: row.row,
1064 field: field.name.clone(),
1065 view: modal.view,
1066 width: width as u16,
1067 state: value.kind(),
1068 pretty: indented.code(),
1069 unpacked: unpacked.code(),
1070 };
1071 if let Some((cached, pane)) = &modal.pane
1072 && *cached == key
1073 {
1074 return pane.clone();
1075 }
1076 let table = match &value {
1079 Shown::Value(v) if is_scalar(v) => Some(preview(field, v, 64, ctx)),
1080 _ => None,
1081 };
1082 let read_key = if state.can_drill_down() { "r" } else { "Enter" };
1083 let mut built = pane(
1084 &field.dtype,
1085 &value,
1086 &PaneAsk {
1087 choice: modal.view,
1088 width,
1089 table: table.as_deref(),
1090 indented,
1091 not_json: modal.known_not_json(row.frame, row.row, &field.name),
1092 unpacked,
1093 read_key,
1094 },
1095 );
1096 built.id = renewed(modal, &key, &built);
1097 modal.pane = Some((key, built.clone()));
1098 built
1099}
1100
1101fn renewed(modal: &mut InspectorModal, key: &PaneKey, built: &Pane) -> u64 {
1105 match &modal.pane {
1106 Some((cached, old)) if cached.same_value(key) => {
1107 let id = old.id;
1108 let old = old.content.clone();
1109 modal.reader.carry(&old, &built.content);
1110 id
1111 }
1112 _ => modal.next_pane_id(),
1113 }
1114}
1115
1116fn enter_label(
1118 modal: &InspectorModal,
1119 state: &DataTableState,
1120 row: &InspectRow,
1121 field: &InspectField,
1122) -> Option<&'static str> {
1123 if state.can_drill_down() {
1125 return Some("Rows");
1126 }
1127 match shown(field, row, modal.read.as_ref(), state) {
1128 Shown::Unread => Some("Read"),
1129 Shown::Failed(_) => Some("Retry"),
1130 Shown::Value(v)
1131 if value_opens(&v) && !modal.known_not_json(row.frame, row.row, &field.name) =>
1132 {
1133 Some("Open")
1134 }
1135 _ => None,
1136 }
1137}
1138
1139pub fn render(
1141 area: Rect,
1142 buf: &mut Buffer,
1143 modal: &mut InspectorModal,
1144 state: &DataTableState,
1145 codebook: Option<&crate::codebook::Codebook>,
1146 ctx: &RenderContext,
1147) {
1148 let row = state.inspect_row();
1149 if let Some(row) = &row {
1150 modal.row_shown(row.frame, row.row);
1151 }
1152 if let (Some(row), Some(_)) = (&row, &modal.drill) {
1153 let title = title(row.display_row, state, None);
1154 render_drill(area, buf, modal, &title, ctx);
1155 return;
1156 }
1157 let content = Surface::content_area(area);
1158 modal.compare_both = content.width as usize >= WIDER;
1159 let visible = visible_fields(modal, state);
1160 modal.set_visible(visible);
1161 let other = row.as_ref().and_then(|r| compared(modal, state, r));
1162
1163 let focused = modal.focused().cloned();
1164
1165 let g = crate::glyphs::get();
1167 let name_w = modal
1168 .fields
1169 .iter()
1170 .map(|f| {
1171 crate::glyphs::cell_width(&f.name)
1172 + if f.hidden {
1173 1 + crate::glyphs::cell_width(g.hidden_mark)
1174 } else {
1175 0
1176 }
1177 })
1178 .max()
1179 .unwrap_or(0)
1180 .clamp(4, 28);
1181 let type_w = modal
1182 .fields
1183 .iter()
1184 .map(|f| crate::glyphs::cell_width(&dtype_label(&f.dtype)))
1185 .max()
1186 .unwrap_or(0)
1187 .min(14);
1188 let shape = ListShape {
1189 fields: modal.fields.len(),
1190 min_col: 1 + name_w + GAP + type_w + GAP + PREVIEW_MIN,
1191 single: other.is_some(),
1192 bytes: modal
1193 .fields
1194 .iter()
1195 .any(|f| matches!(f.dtype, DataType::Binary | DataType::BinaryOffset)),
1196 };
1197 let probe = layout(content, modal.visible.len(), VALUE_MIN, shape);
1200 let width = value_width(probe.value);
1201 let pane = match (&row, &focused) {
1202 (Some(row), Some(field)) => Some(field_pane(
1203 modal,
1204 state,
1205 row,
1206 field,
1207 pane_width(probe.value),
1208 ctx,
1209 )),
1210 _ => None,
1211 };
1212 let pane = pane.unwrap_or_else(|| {
1213 let message = if row.is_none() {
1214 "Reading the row..."
1215 } else {
1216 "No field matches"
1217 };
1218 Pane::lines(vec![(message.to_string(), Tone::Dim)], Vec::new())
1219 });
1220 modal.reader.prepare(pane.id, width, modal.wrap);
1221 let note = match (&row, &focused, codebook) {
1223 (Some(row), Some(field), Some(book)) => book
1224 .column(&field.name)
1225 .map(|column| {
1226 codebook_lines(
1227 column,
1228 &shown(field, row, modal.read.as_ref(), state),
1229 width,
1230 )
1231 })
1232 .unwrap_or_default(),
1233 _ => Vec::new(),
1234 };
1235 let value_need = modal
1236 .reader
1237 .rows_needed(&pane.content, content.height as usize);
1238 let need = value_need + if note.is_empty() { 0 } else { note.len() + 1 };
1239
1240 let mut lay = layout(content, modal.visible.len(), need, shape);
1241 let note_rows = if note.is_empty() || lay.value.height < 3 {
1244 0
1245 } else {
1246 (note.len() + 1).min(lay.value.height as usize / 2) as u16
1247 };
1248 lay.value.height -= note_rows;
1249 let note_area = Rect {
1250 y: lay.value.y + lay.value.height.min(value_need as u16),
1251 height: note_rows,
1252 ..lay.value
1253 };
1254
1255 let enter = match (&row, &focused) {
1257 (Some(row), Some(field)) => enter_label(modal, state, row, field),
1258 _ => None,
1259 };
1260 let unread_on_group = state.can_drill_down()
1261 && matches!(
1262 (&row, &focused),
1263 (Some(row), Some(field))
1264 if matches!(shown(field, row, modal.read.as_ref(), state), Shown::Unread | Shown::Failed(_))
1265 );
1266 let value_rows = lay.value.height as usize;
1267 let overflows = need > value_rows || modal.reader.window(&pane.content, value_rows).above;
1268 let footer = footer(
1269 modal,
1270 &pane,
1271 &FooterFacts {
1272 enter,
1273 read_key: unread_on_group,
1274 has_value: row.is_some() && focused.is_some(),
1275 many_fields: modal.visible.len() > 1,
1276 list_overflows: modal.visible.len() > lay.list.height as usize * lay.cols,
1277 value_overflows: overflows,
1278 comparing: other.is_some(),
1279 },
1280 ctx,
1281 );
1282 let title = match &row {
1283 Some(row) => title(row.display_row, state, other.as_ref()),
1284 None => "Row".to_string(),
1285 };
1286 let title = crate::glyphs::fit_cells(&title, area.width.saturating_sub(4) as usize, g.ellipsis);
1287 let content = Surface::new(&title).footer(&footer).render(area, buf, ctx);
1288 if content.height < 4 || content.width < 12 {
1289 return;
1290 }
1291
1292 let mut rule_used = None;
1294 if modal.finding || !modal.filter.is_empty() {
1295 draw_find_line(
1296 buf,
1297 lay.list_rule,
1298 &modal.filter,
1299 modal.finding,
1300 &format!(
1301 "{} of {}",
1302 thousands(modal.visible.len()),
1303 thousands(modal.fields.len())
1304 ),
1305 ctx,
1306 );
1307 } else {
1308 let chip = match &row {
1309 Some(row) => list_chip(modal, state, row, other.as_ref()),
1310 None => thousands(modal.fields.len()),
1311 };
1312 SectionRule {
1313 title: "Fields",
1314 chip: Some(&chip),
1315 }
1316 .render(lay.list_rule, buf, ctx);
1317 rule_used = Some("Fields".len() + 1 + crate::glyphs::cell_width(&chip) + 3);
1318 }
1319
1320 draw_fields(
1321 buf,
1322 &lay,
1323 modal,
1324 state,
1325 ListOf {
1326 row: row.as_ref(),
1327 other: other.as_ref(),
1328 name_w,
1329 type_w,
1330 rule_used,
1331 },
1332 ctx,
1333 );
1334
1335 let name = focused.as_ref().map(|f| f.name.clone()).unwrap_or_default();
1336 draw_value(buf, &lay, &name, &pane, modal, ctx);
1337 for (i, (text, tone)) in note
1338 .iter()
1339 .take(note_rows.saturating_sub(1) as usize)
1340 .enumerate()
1341 {
1342 let style = match tone {
1343 Tone::Dim => Style::default().fg(ctx.dimmed),
1344 _ => Style::default().fg(ctx.text_primary),
1345 };
1346 Paragraph::new(text.as_str()).style(style).render(
1347 Rect {
1348 x: note_area.x + 1,
1349 y: note_area.y + 1 + i as u16,
1350 width: note_area.width.saturating_sub(1),
1351 height: 1,
1352 },
1353 buf,
1354 );
1355 }
1356}
1357
1358pub fn codebook_lines(
1361 column: &crate::codebook::Column,
1362 shown: &Shown,
1363 width: usize,
1364) -> Vec<(String, Tone)> {
1365 let mut lines = Vec::new();
1366 let about = column.about();
1367 if !about.is_empty() {
1368 reader::wrap_lines(&about, width.max(1), Tone::Dim, &mut lines);
1369 }
1370 let value = match shown {
1371 Shown::Null(_) => Some(None),
1372 Shown::Value(v) => Some(Some(exact::value_text(v))),
1373 _ => None,
1374 };
1375 if let Some(line) = value.and_then(|v| column.legend_line(v.as_deref())) {
1376 reader::wrap_lines(&line, width.max(1), Tone::Plain, &mut lines);
1377 }
1378 lines
1379}
1380
1381fn list_chip(
1384 modal: &InspectorModal,
1385 state: &DataTableState,
1386 row: &InspectRow,
1387 other: Option<&Compared>,
1388) -> String {
1389 let c = counts(modal, state, row, other);
1390 let total = thousands(modal.fields.len());
1391 let mut parts = vec![if modal.visible.len() < modal.fields.len() {
1392 format!("{} of {total}", thousands(modal.visible.len()))
1393 } else {
1394 total
1395 }];
1396 match c.differ {
1397 Some(n) => parts.push(format!("{} differ", thousands(n))),
1398 None => {
1399 if c.nulls > 0 {
1400 parts.push(format!("{} null", thousands(c.nulls)));
1401 }
1402 if c.empties > 0 {
1403 parts.push(format!("{} empty", thousands(c.empties)));
1404 }
1405 }
1406 }
1407 if modal.filled_only {
1408 parts.push(
1409 if c.differ.is_some() {
1410 "differ only"
1411 } else {
1412 "nulls hidden"
1413 }
1414 .to_string(),
1415 );
1416 }
1417 if let Some(order) = modal.order.label() {
1418 parts.push(order.to_string());
1419 }
1420 join_facts(&parts)
1421}
1422
1423fn draw_find_line(
1425 buf: &mut Buffer,
1426 at: Rect,
1427 text: &str,
1428 typing: bool,
1429 count: &str,
1430 ctx: &RenderContext,
1431) {
1432 let g = crate::glyphs::get();
1433 let label_style = if typing {
1434 Style::default().fg(ctx.accent).add_modifier(Modifier::BOLD)
1435 } else {
1436 Style::default().fg(ctx.dimmed)
1437 };
1438 let mut spans = vec![
1439 Span::styled("Find: ", label_style),
1440 Span::styled(text.to_string(), Style::default().fg(ctx.text_primary)),
1441 ];
1442 if typing {
1443 spans.push(Span::styled(g.cursor, Style::default().fg(ctx.accent)));
1444 }
1445 spans.push(Span::styled(
1446 format!(" {count}"),
1447 Style::default().fg(ctx.dimmed),
1448 ));
1449 Paragraph::new(Line::from(spans)).render(at, buf);
1450}
1451
1452struct FooterFacts {
1454 enter: Option<&'static str>,
1455 read_key: bool,
1457 has_value: bool,
1458 many_fields: bool,
1459 list_overflows: bool,
1460 value_overflows: bool,
1461 comparing: bool,
1462}
1463
1464fn footer<'a>(
1467 modal: &InspectorModal,
1468 pane: &'a Pane,
1469 f: &FooterFacts,
1470 ctx: &RenderContext,
1471) -> HintBar<'a> {
1472 let g = crate::glyphs::get();
1473 let mut bar = HintBar::from_ctx(ctx);
1474 if modal.finding {
1475 return bar
1476 .hint_weighted("Enter", "Done", 3)
1477 .hint_weighted("type", "Find", 1)
1478 .hint_weighted("Esc", "Clear", 4);
1479 }
1480 if modal.value_find.as_ref().is_some_and(|f| f.editing) {
1481 return bar
1482 .hint_weighted("Enter", "Find", 3)
1483 .hint_weighted("type", "Text", 1)
1484 .hint_weighted("Esc", "Clear", 4);
1485 }
1486 let copy = match pane.copy {
1487 CopyAs::Base64 => "Copy base64",
1488 CopyAs::Text(_) if pane.view == Some(View::Json) => "Copy JSON",
1489 CopyAs::Text(_) => "Copy text",
1490 _ => "Copy",
1491 };
1492 let view = pane.next_view().map(View::label);
1493 if modal.focus == Focus::Value {
1494 if f.value_overflows {
1495 bar = bar
1496 .hint_weighted(g.updown, "Scroll", 8)
1497 .hint_weighted("Home/End", "Top/End", 6);
1498 }
1499 bar = bar.hint_weighted("/", "Find", 7);
1500 if modal
1501 .value_find
1502 .as_ref()
1503 .is_some_and(|f| !f.hits.is_empty())
1504 {
1505 bar = bar.hint_weighted("n/N", "Next", 7);
1506 }
1507 if let Some(view) = view {
1508 bar = bar.hint_weighted("e", view, 5);
1509 }
1510 if pane.content.wraps() {
1511 let wrap = match modal.wrap {
1512 reader::Wrap::Word => "Hard wrap",
1513 reader::Wrap::Hard => "Word wrap",
1514 };
1515 bar = bar.hint_weighted("w", wrap, 3);
1516 }
1517 if f.has_value {
1518 bar = bar
1519 .hint_weighted("y", copy, 4)
1520 .hint_weighted("o", "Open", 2);
1521 }
1522 bar = bar.hint_weighted(g.updown_lr, "Row", 1);
1523 return bar.hint_weighted("Esc", "Fields", 10);
1524 }
1525 if let Some(label) = f.enter {
1526 bar = bar.hint_weighted("Enter", label, 9);
1527 }
1528 if f.read_key {
1529 bar = bar.hint_weighted("r", "Read", 9);
1530 }
1531 if f.has_value {
1532 bar = bar.hint_weighted("Tab", "Value", 8);
1533 if !matches!(f.enter, Some("Read" | "Retry")) {
1534 bar = bar.hint_weighted("y", copy, 7);
1535 }
1536 }
1537 if f.many_fields {
1540 bar = bar.hint_weighted(g.updown, "Field", 4);
1541 }
1542 bar = bar
1543 .hint_weighted(g.updown_lr, "Row", 6)
1544 .hint_weighted("/", "Find", 5);
1545 if let Some(view) = view {
1546 bar = bar.hint_weighted("e", view, 5);
1547 }
1548 if f.list_overflows {
1549 bar = bar.hint_weighted("PgUp/PgDn", "Page", 3);
1550 }
1551 let filled = match (f.comparing, modal.filled_only) {
1553 (true, _) => "Differ",
1554 (false, true) => "Nulls: hidden",
1555 (false, false) => "Nulls: shown",
1556 };
1557 bar = bar.hint_weighted("Y", "Copy row", 2);
1558 if !modal.compare {
1560 bar = bar.hint_weighted("c", "Compare", 2);
1561 }
1562 bar = bar.hint_weighted("f", filled, 1);
1563 if f.comparing {
1564 bar = bar.hint_weighted("m", "Pin", 1);
1565 }
1566 let esc = if !modal.filter.is_empty() {
1568 "Clear"
1569 } else if modal.compare {
1570 "No compare"
1571 } else {
1572 "Close"
1573 };
1574 bar.hint_weighted("Esc", esc, 10)
1575}
1576
1577struct ListOf<'a> {
1579 row: Option<&'a InspectRow>,
1580 other: Option<&'a Compared>,
1581 name_w: usize,
1582 type_w: usize,
1583 rule_used: Option<usize>,
1586}
1587
1588fn draw_fields(
1593 buf: &mut Buffer,
1594 lay: &Layout,
1595 modal: &mut InspectorModal,
1596 state: &DataTableState,
1597 of: ListOf,
1598 ctx: &RenderContext,
1599) {
1600 let ListOf {
1601 row,
1602 other,
1603 name_w,
1604 type_w,
1605 rule_used,
1606 } = of;
1607 let g = crate::glyphs::get();
1608 let rows = lay.list.height as usize;
1609 let cols = lay.cols.max(1);
1610 let cap = rows * cols;
1611 let n = modal.visible.len();
1612 let sel = modal.focused_position();
1613 let (offset, above, below) = list_window(n, sel, modal.list_offset, cap);
1614 let items = cap - usize::from(above) - usize::from(below);
1615 modal.list_offset = offset;
1616 modal.list_page = items.max(1);
1617 let list_w = lay.list.width as usize;
1618 let col_w = if cols > 1 {
1619 (list_w - GAP * (cols - 1)) / cols
1620 } else {
1621 list_w
1622 };
1623 let beside = 1 + GAP + type_w + GAP + PREVIEW_MIN;
1625 let name_w = name_w.min((col_w / 3).max(col_w.saturating_sub(beside)).max(4));
1626 let rest = col_w.saturating_sub(1 + name_w + GAP + type_w + GAP);
1627 let read = modal.read.as_ref();
1628 let cells: Vec<(Option<&InspectRow>, Option<&FieldRead>)> = match other {
1630 Some(c) if c.both => vec![
1631 (c.before.as_ref(), None),
1632 (row, read),
1633 (c.after.as_ref(), None),
1634 ],
1635 Some(c) => vec![(row, read), (c.after.as_ref(), None)],
1636 None => vec![(row, read)],
1637 };
1638 let each = match cells.len() {
1640 1 => rest,
1641 n => rest.saturating_sub(GAP * (n - 1) + 2) / n,
1642 };
1643 let slot_rect = |slot: usize| {
1644 let col = slot / rows;
1645 let r = slot % rows;
1646 Rect {
1647 x: lay.list.x + (col * (col_w + GAP)) as u16,
1648 y: lay.list.y + r as u16,
1649 width: col_w as u16,
1650 height: 1,
1651 }
1652 };
1653 if n == 0 {
1654 Paragraph::new("No field matches")
1655 .style(Style::default().fg(ctx.dimmed))
1656 .render(slot_rect(0), buf);
1657 return;
1658 }
1659 let dim = Style::default().fg(ctx.dimmed);
1660 if above {
1661 Paragraph::new(format!(" {} {} above", g.ellipsis, thousands(offset)))
1662 .style(dim)
1663 .render(slot_rect(0), buf);
1664 }
1665 if below {
1666 let left = n - offset - items;
1667 Paragraph::new(format!(" {} {} more", g.ellipsis, thousands(left)))
1668 .style(dim)
1669 .render(slot_rect(cap - 1), buf);
1670 }
1671 if let (Some(used), Some(row), Some(_)) = (rule_used, row, other) {
1672 let first = 1 + name_w + GAP + type_w + GAP;
1673 let rows: Vec<(usize, usize, bool)> = cells
1674 .iter()
1675 .enumerate()
1676 .filter_map(|(j, (at, _))| {
1677 at.map(|r| (first + j * (each + GAP), r.display_row, r.row == row.row))
1678 })
1679 .collect();
1680 draw_compare_labels(buf, lay.list_rule, &rows, each, used, ctx);
1681 }
1682 let list_focused = modal.focus == Focus::List && !modal.finding;
1683 let value_cell = |field: &InspectField, at: Option<&InspectRow>, room: usize, own_read| match at
1684 {
1685 Some(r) => match shown(field, r, own_read, state) {
1686 Shown::Value(v) => match empty_preview(&v) {
1687 Some(empty) => (empty, dim),
1688 None => (
1689 preview(field, &v, room, ctx),
1690 Style::default().fg(ctx.text_primary),
1691 ),
1692 },
1693 Shown::Null(kind) => (
1694 null_glyph(kind).to_string(),
1695 dim.add_modifier(Modifier::ITALIC),
1696 ),
1697 Shown::Unread => ("not read".to_string(), dim),
1698 Shown::Reading => ("reading...".to_string(), dim),
1699 Shown::Failed(_) => ("not read".to_string(), Style::default().fg(ctx.warning)),
1700 },
1701 None => (String::new(), Style::default()),
1702 };
1703 for k in 0..items.min(n - offset) {
1704 let i = offset + k;
1705 let index = modal.visible[i];
1706 let field = &modal.fields[index];
1707 let at = slot_rect(k + usize::from(above));
1708 let is_selected = i == sel;
1709 let marker = match (is_selected, modal.finding, list_focused) {
1710 (true, false, true) => g.rail,
1711 (true, true, _) => g.middot,
1712 _ => " ",
1713 };
1714 let mut name = field.name.clone();
1715 if field.hidden {
1716 name = format!("{name} {}", g.hidden_mark);
1717 }
1718 let name = crate::glyphs::fit_cells(&name, name_w, g.ellipsis);
1719 let name_pad = name_w.saturating_sub(crate::glyphs::cell_width(&name));
1720 let label =
1721 crate::glyphs::fit_cells(&dtype_label(&field.dtype), type_w, g.ellipsis).into_owned();
1722 let label_pad = type_w.saturating_sub(crate::glyphs::cell_width(&label));
1723 let name_style = if is_selected {
1724 Style::default().fg(ctx.accent).add_modifier(Modifier::BOLD)
1725 } else if ctx.column_colors {
1726 Style::default().fg(ctx.type_color(&field.dtype))
1727 } else {
1728 Style::default().fg(ctx.text_primary)
1729 };
1730 let mut spans = vec![
1731 Span::styled(marker, Style::default().fg(ctx.accent)),
1732 Span::styled(name.into_owned(), name_style),
1733 Span::raw(" ".repeat(name_pad + GAP)),
1734 Span::styled(label, dim),
1735 Span::raw(" ".repeat(label_pad + GAP)),
1736 ];
1737 for (j, (at_row, own)) in cells.iter().enumerate() {
1738 let (text, style) = value_cell(field, *at_row, each, *own);
1739 let text = crate::glyphs::fit_cells(&text, each, g.ellipsis).into_owned();
1740 let pad = each.saturating_sub(crate::glyphs::cell_width(&text));
1741 spans.push(Span::styled(text, style));
1742 if j + 1 < cells.len() {
1743 spans.push(Span::raw(" ".repeat(pad + GAP)));
1744 } else if other.is_some() {
1745 spans.push(Span::raw(" ".repeat(pad + 1)));
1746 }
1747 }
1748 if let (Some(other), Some(this_row)) = (other, row) {
1749 let differ = differs_from(&shown(field, this_row, read, state), field, other, state);
1750 if differ == Some(true) {
1751 spans.push(Span::styled(
1752 g.diff_mark,
1753 Style::default()
1754 .fg(ctx.text_primary)
1755 .add_modifier(Modifier::BOLD),
1756 ));
1757 }
1758 }
1759 let mut paragraph = Paragraph::new(Line::from(spans));
1760 if is_selected && list_focused {
1761 paragraph = paragraph.style(ctx.highlight_style());
1762 }
1763 paragraph.render(at, buf);
1764 }
1765}
1766
1767fn draw_compare_labels(
1773 buf: &mut Buffer,
1774 rule: Rect,
1775 rows: &[(usize, usize, bool)],
1776 each: usize,
1777 used: usize,
1778 ctx: &RenderContext,
1779) {
1780 for &(x, n, this) in rows {
1781 let label = format!(" Row {} ", thousands(n));
1782 if x <= used + 1
1785 || crate::glyphs::cell_width(&label) > each + 1
1786 || x + each >= rule.width as usize
1787 {
1788 continue;
1789 }
1790 let style = if this {
1791 Style::default().fg(ctx.text_primary)
1792 } else {
1793 Style::default().fg(ctx.dimmed)
1794 };
1795 Paragraph::new(label.clone()).style(style).render(
1796 Rect {
1797 x: rule.x + (x - 1) as u16,
1798 width: crate::glyphs::cell_width(&label) as u16,
1799 ..rule
1800 },
1801 buf,
1802 );
1803 }
1804}
1805
1806fn draw_value(
1809 buf: &mut Buffer,
1810 lay: &Layout,
1811 name: &str,
1812 pane: &Pane,
1813 modal: &mut InspectorModal,
1814 ctx: &RenderContext,
1815) {
1816 let g = crate::glyphs::get();
1817 let h = lay.value.height as usize;
1818 modal.page = h.max(1);
1819 let focused = modal.focus == Focus::Value;
1820 if let Some(find) = modal.value_find.as_mut()
1822 && find.pane != pane.id
1823 && !find.text.is_empty()
1824 {
1825 find.hits = reader::find_hits(&pane.content, &find.text);
1826 find.current = None;
1827 find.pane = pane.id;
1828 }
1829 let win: Window = modal.reader.window(&pane.content, h);
1830 let rule = lay.value_rule;
1831 match modal
1832 .value_find
1833 .as_ref()
1834 .filter(|f| f.editing || !f.text.is_empty())
1835 {
1836 Some(find) => {
1837 let count = match (find.hits.len(), find.current) {
1838 (0, _) if !find.editing => "no match".to_string(),
1839 (0, _) => String::new(),
1840 (n, Some(at)) => format!("{} of {}", thousands(at + 1), thousands(n)),
1841 (n, None) => plural(n, "match", "matches"),
1842 };
1843 draw_find_line(buf, rule, &find.text, find.editing, &count, ctx);
1844 }
1845 None => {
1846 let width = rule.width as usize;
1847 let name = crate::glyphs::fit_cells(name, width / 3, g.ellipsis);
1848 let name_w = crate::glyphs::cell_width(&name);
1849 let overflows = win.above || win.below;
1850 let reserve = if overflows {
1854 reader::Reader::position_width(&pane.content) + 3
1855 } else {
1856 0
1857 };
1858 let room = width.saturating_sub(name_w + 1 + 3 + 2 + reserve);
1860 let facts = (!pane.facts.is_empty() && room >= 4)
1861 .then(|| crate::glyphs::fit_cells(&pane.facts, room, g.ellipsis));
1862 SectionRule {
1863 title: &name,
1864 chip: facts.as_deref(),
1865 }
1866 .render(rule, buf, ctx);
1867 let used = name_w
1868 + 1
1869 + facts
1870 .as_deref()
1871 .map_or(0, |f| crate::glyphs::cell_width(f) + 3);
1872 if overflows {
1873 let text = format!(" {} ", modal.reader.position(&pane.content, &win));
1874 let w = crate::glyphs::cell_width(&text);
1875 if used + 2 + w < width {
1877 Paragraph::new(text).style(dim_or(ctx, focused)).render(
1878 Rect {
1879 x: rule.x + (width - w - 1) as u16,
1880 width: w as u16,
1881 ..rule
1882 },
1883 buf,
1884 );
1885 }
1886 }
1887 }
1888 }
1889 let needle = modal
1890 .value_find
1891 .as_ref()
1892 .map(|f| f.text.clone())
1893 .unwrap_or_default();
1894 let text_w = pane_width(lay.value) as u16;
1895 for (i, row) in win.rows.iter().enumerate() {
1896 let y = lay.value.y + i as u16;
1897 if focused {
1898 Paragraph::new(g.rail)
1899 .style(Style::default().fg(ctx.accent))
1900 .render(
1901 Rect {
1902 x: lay.value.x,
1903 y,
1904 width: 1,
1905 height: 1,
1906 },
1907 buf,
1908 );
1909 }
1910 let style = match row.tone {
1911 Tone::Plain => Style::default().fg(ctx.text_primary),
1912 Tone::Dim => Style::default().fg(ctx.dimmed),
1913 Tone::Warn => Style::default().fg(ctx.warning),
1914 };
1915 let mut spans = Vec::new();
1916 let mut at = 0;
1917 for (a, b) in reader::hits_in_row(&row.text, &needle) {
1918 spans.push(Span::styled(row.text[at..a].to_string(), style));
1919 spans.push(Span::styled(row.text[a..b].to_string(), ctx.find_match));
1920 at = b;
1921 }
1922 spans.push(Span::styled(row.text[at..].to_string(), style));
1923 Paragraph::new(Line::from(spans)).render(
1924 Rect {
1925 x: lay.value.x + 1,
1926 y,
1927 width: text_w.min(lay.value.width.saturating_sub(1)),
1928 height: 1,
1929 },
1930 buf,
1931 );
1932 }
1933}
1934
1935fn dim_or(ctx: &RenderContext, focused: bool) -> Style {
1936 if focused {
1937 Style::default().fg(ctx.accent)
1938 } else {
1939 Style::default().fg(ctx.dimmed)
1940 }
1941}
1942
1943pub fn value_opens(value: &AnyValue) -> bool {
1946 match value {
1947 AnyValue::String(s) => crate::inspector_drill::opens_as_json(s),
1948 AnyValue::StringOwned(s) => crate::inspector_drill::opens_as_json(s),
1949 v => exact::is_nested_value(v),
1950 }
1951}
1952
1953pub fn node_pane(node: &Node, choice: Option<View>, width: usize) -> Pane {
1955 let ask = PaneAsk {
1956 choice,
1957 width,
1958 table: None,
1959 indented: Indented::None,
1960 not_json: false,
1961 unpacked: Unpacked::Unavailable,
1962 read_key: "Enter",
1963 };
1964 match node {
1965 Node::Native(series) => {
1966 let shown = match series.get(0) {
1967 Ok(AnyValue::Null) | Err(_) => Shown::Null(NullKind::Null),
1968 Ok(v) => Shown::Value(v),
1969 };
1970 pane(series.dtype(), &shown, &ask)
1971 }
1972 Node::Json { .. } => json_pane(node.json().unwrap_or(&JsonValue::Null), &ask),
1973 }
1974}
1975
1976fn json_pane(value: &JsonValue, ask: &PaneAsk) -> Pane {
1979 let g = crate::glyphs::get();
1980 let width = ask.width.clamp(1, MEASURE);
1981 let scalar = |text: String, kind: &str| {
1982 let mut lines = Vec::new();
1983 reader::wrap_lines(&text, width, Tone::Plain, &mut lines);
1984 Pane::lines(lines, vec![kind.to_string()])
1985 };
1986 match value {
1987 JsonValue::String(s) => pane(&DataType::String, &Shown::Value(AnyValue::String(s)), ask),
1988 JsonValue::Null => Pane::lines(
1989 vec![(format!("{} null", g.null), Tone::Dim)],
1990 vec!["null".to_string()],
1991 ),
1992 JsonValue::Bool(b) => scalar(b.to_string(), "bool"),
1993 JsonValue::Number(n) => scalar(n.to_string(), "number"),
1994 JsonValue::Array(_) | JsonValue::Object(_) => {
1995 let (kind, count) = match value {
1996 JsonValue::Object(map) => ("object", plural(map.len(), "key", "keys")),
1997 JsonValue::Array(items) => ("array", plural(items.len(), "item", "items")),
1998 _ => unreachable!(),
1999 };
2000 let cap = CHUNK_BYTES * 4;
2001 let (text, cut) = json_text(value, true, cap);
2002 let mut facts = vec![kind.to_string(), count];
2003 if cut {
2004 facts.push(format!("first {} KB", cap / 1024));
2005 }
2006 Pane {
2007 id: 0,
2008 facts: join_facts(&facts),
2009 content: Content::text(Arc::from(text), TextForm::Raw),
2010 views: Vec::new(),
2011 view: None,
2012 copy: CopyAs::Stored,
2013 indent: false,
2014 unpack: false,
2015 }
2016 }
2017 }
2018}
2019
2020fn node_preview(node: &Node, room: usize, ctx: &RenderContext) -> (String, Style) {
2022 let g = crate::glyphs::get();
2023 let plain = Style::default().fg(ctx.text_primary);
2024 let dim = Style::default().fg(ctx.dimmed);
2025 let null = (g.null.to_string(), dim.add_modifier(Modifier::ITALIC));
2026 let budget = room.saturating_mul(4).max(16);
2027 match node {
2028 Node::Native(series) => match series.get(0) {
2029 Ok(AnyValue::Null) | Err(_) => null,
2030 Ok(v) => match empty_preview(&v) {
2031 Some(empty) => (empty, dim),
2032 None => {
2033 let field = InspectField {
2034 name: series.name().to_string(),
2035 dtype: series.dtype().clone(),
2036 hidden: false,
2037 };
2038 (preview(&field, &v, room, ctx), plain)
2039 }
2040 },
2041 },
2042 Node::Json { .. } => match node.json() {
2043 None | Some(JsonValue::Null) => null,
2044 Some(JsonValue::String(s)) if s.is_empty() => ("\"\"".to_string(), dim),
2045 Some(JsonValue::String(s)) => (
2046 exact::preview(exact::prefix(s, budget), g).into_owned(),
2047 plain,
2048 ),
2049 Some(v) => (
2050 exact::preview(&json_text(v, false, budget).0, g).into_owned(),
2051 plain,
2052 ),
2053 },
2054 }
2055}
2056
2057fn drill_title(root: &str, labels: &[&str], max: usize) -> String {
2061 let g = crate::glyphs::get();
2062 let sep = format!(" {} ", g.trail);
2063 let steps: Vec<String> = labels
2064 .iter()
2065 .map(|l| exact::preview(l, g).into_owned())
2066 .collect();
2067 let joined = |skip: usize| {
2068 let mut parts = vec![root.to_string()];
2069 if skip > 0 {
2070 parts.push(g.ellipsis.to_string());
2071 }
2072 parts.extend(steps[skip..].iter().cloned());
2073 parts.join(&sep)
2074 };
2075 for skip in 0..steps.len() {
2076 let title = joined(skip);
2077 if crate::glyphs::cell_width(&title) <= max {
2078 return title;
2079 }
2080 }
2081 let last = joined(steps.len().saturating_sub(1));
2082 crate::glyphs::fit_cells(&last, max, g.ellipsis).into_owned()
2083}
2084
2085fn render_drill(
2088 area: Rect,
2089 buf: &mut Buffer,
2090 modal: &mut InspectorModal,
2091 root_title: &str,
2092 ctx: &RenderContext,
2093) {
2094 let g = crate::glyphs::get();
2095 let Some(drill) = modal.drill.clone() else {
2096 return;
2097 };
2098 let level = drill.level();
2099 let node = &level.node;
2100 let shape = node.shape();
2101 let len = node.len();
2102 let selected = level.selected.min(len.saturating_sub(1));
2103 let focused = node.child(selected);
2104 let content = Surface::content_area(area);
2105 let columns = node.table_columns();
2106 let header = usize::from(columns.is_some());
2107 let stacked = Rect {
2109 width: content.width.min(WIDE as u16 - 1),
2110 ..content
2111 };
2112 let full = pane_width(content);
2113 let width = full.min(MEASURE);
2114
2115 let pane = match &focused {
2116 Some((label, child)) => {
2117 let key = PaneKey {
2118 frame: drill.frame,
2119 row: drill.row,
2120 field: drill.item_key(label),
2121 view: modal.view,
2122 width: full as u16,
2123 state: 5,
2124 pretty: 0,
2125 unpacked: 0,
2126 };
2127 match &modal.pane {
2128 Some((cached, pane)) if *cached == key => pane.clone(),
2129 _ => {
2130 let mut built = node_pane(child, modal.view, full);
2131 built.id = renewed(modal, &key, &built);
2132 modal.pane = Some((key, built.clone()));
2133 built
2134 }
2135 }
2136 }
2137 None => Pane::lines(
2138 vec![(
2139 format!("No {}", shape.items_title().to_lowercase()),
2140 Tone::Dim,
2141 )],
2142 Vec::new(),
2143 ),
2144 };
2145 modal.reader.prepare(pane.id, width, modal.wrap);
2146 let need = modal
2147 .reader
2148 .rows_needed(&pane.content, content.height as usize);
2149 let mut lay = layout(
2150 Rect {
2151 height: content.height.saturating_sub(header as u16),
2152 ..stacked
2153 },
2154 len,
2155 need,
2156 ListShape {
2157 fields: len,
2158 min_col: 1,
2159 single: true,
2160 bytes: false,
2161 },
2162 );
2163 lay.list.y += header as u16;
2165 lay.value_rule.y += header as u16;
2166 lay.value.y += header as u16;
2167 lay.value_rule.width = content.width;
2168 lay.value.width = content.width;
2169 lay.list.width = content.width;
2170 lay.list_rule.width = content.width;
2171 let list_h = lay.list.height as usize;
2172 let value_rows = lay.value.height as usize;
2173 let overflows = need > value_rows || modal.reader.window(&pane.content, value_rows).above;
2174 let opens = focused.as_ref().is_some_and(|(label, child)| {
2175 child.opens() && !modal.known_not_json(drill.frame, drill.row, &drill.item_key(label))
2176 });
2177
2178 let mut bar = HintBar::from_ctx(ctx);
2179 if modal.focus == Focus::Value {
2180 bar = footer(
2181 modal,
2182 &pane,
2183 &FooterFacts {
2184 enter: None,
2185 read_key: false,
2186 has_value: focused.is_some(),
2187 many_fields: len > 1,
2188 list_overflows: false,
2189 value_overflows: overflows,
2190 comparing: false,
2191 },
2192 ctx,
2193 );
2194 } else {
2195 if opens {
2196 bar = bar.hint_weighted("Enter", "Open", 9);
2197 }
2198 if focused.is_some() {
2199 bar = bar
2200 .hint_weighted("Tab", "Value", 8)
2201 .hint_weighted("y", "Copy", 7);
2202 }
2203 if len > 1 {
2204 let word = match shape {
2205 Shape::Struct => "Field",
2206 Shape::Object => "Key",
2207 _ => "Item",
2208 };
2209 bar = bar.hint_weighted(g.updown, word, 6);
2210 }
2211 if let Some(view) = pane.next_view() {
2212 bar = bar.hint_weighted("e", view.label(), 3);
2213 }
2214 if len > list_h {
2215 bar = bar.hint_weighted("PgUp/PgDn", "Page", 2);
2216 }
2217 bar = bar.hint_weighted("Esc", "Back", 10);
2218 }
2219
2220 let labels: Vec<&str> = drill.levels.iter().map(|l| l.label.as_str()).collect();
2221 let title = drill_title(root_title, &labels, area.width.saturating_sub(4) as usize);
2222 let content = Surface::new(&title).footer(&bar).render(area, buf, ctx);
2223 if content.height < 4 || content.width < 12 {
2224 return;
2225 }
2226 let count = thousands(len);
2227 SectionRule {
2228 title: shape.items_title(),
2229 chip: Some(&count),
2230 }
2231 .render(lay.list_rule, buf, ctx);
2232
2233 let offset = list_window(len, selected, modal.list_offset, list_h).0;
2234 modal.list_offset = offset;
2235 modal.list_page = list_h.saturating_sub(2).max(1);
2236 let below = len.saturating_sub(offset + list_h);
2237 let page = node.children(offset, list_h);
2238 let rows = ListRows {
2239 y: lay.list.y,
2240 offset,
2241 selected,
2242 below,
2243 list_h,
2244 focused: modal.focus == Focus::List,
2245 };
2246 match columns {
2247 Some(columns) => draw_table(buf, content, node, &columns, &page, &rows, ctx),
2248 None => draw_items(buf, content, node, &page, &rows, ctx),
2249 }
2250
2251 let name = focused.map(|(label, _)| label).unwrap_or_default();
2252 draw_value(buf, &lay, &name, &pane, modal, ctx);
2253}
2254
2255struct ListRows {
2257 y: u16,
2258 offset: usize,
2259 selected: usize,
2260 below: usize,
2262 list_h: usize,
2263 focused: bool,
2264}
2265
2266impl ListRows {
2267 fn more_at(&self, i: usize) -> bool {
2269 i + 1 == self.list_h && self.below > 0 && self.offset + i != self.selected
2270 }
2271
2272 fn more_line(&self) -> String {
2273 format!(
2274 " {} {} more",
2275 crate::glyphs::get().ellipsis,
2276 thousands(self.below + 1)
2277 )
2278 }
2279}
2280
2281fn item_head(
2283 label: &str,
2284 label_w: usize,
2285 name_style: Style,
2286 is_selected: bool,
2287 focused: bool,
2288 ctx: &RenderContext,
2289) -> Vec<Span<'static>> {
2290 let g = crate::glyphs::get();
2291 let label = exact::preview(label, g);
2292 let label = crate::glyphs::fit_cells(&label, label_w, g.ellipsis).into_owned();
2293 let pad = label_w.saturating_sub(crate::glyphs::cell_width(&label));
2294 let name_style = if is_selected {
2295 Style::default().fg(ctx.accent).add_modifier(Modifier::BOLD)
2296 } else {
2297 name_style
2298 };
2299 vec![
2300 Span::styled(
2301 if is_selected && focused { g.rail } else { " " },
2302 Style::default().fg(ctx.accent),
2303 ),
2304 Span::styled(label, name_style),
2305 Span::raw(" ".repeat(pad + GAP)),
2306 ]
2307}
2308
2309fn draw_items(
2311 buf: &mut Buffer,
2312 content: Rect,
2313 node: &Node,
2314 page: &[(String, Node)],
2315 rows: &ListRows,
2316 ctx: &RenderContext,
2317) {
2318 let g = crate::glyphs::get();
2319 let width = content.width as usize;
2320 let page_w = page
2323 .iter()
2324 .map(|(label, _)| crate::glyphs::cell_width(&exact::preview(label, g)))
2325 .max()
2326 .unwrap_or(0);
2327 let label_w = node
2328 .label_width()
2329 .max(page_w)
2330 .clamp(3, (width / 3).clamp(4, 28));
2331 let type_w = match node.shape() {
2333 Shape::Object | Shape::Array => "object".len(),
2334 Shape::List => node
2335 .child(0)
2336 .map_or(0, |(_, c)| crate::glyphs::cell_width(&c.type_label())),
2337 _ => node
2338 .children(0, usize::MAX)
2339 .iter()
2340 .map(|(_, c)| crate::glyphs::cell_width(&c.type_label()))
2341 .max()
2342 .unwrap_or(0),
2343 }
2344 .min(14);
2345 let preview_w = width.saturating_sub(1 + label_w + GAP + type_w + GAP);
2346 for (i, (label, child)) in page.iter().enumerate() {
2347 let y = rows.y + i as u16;
2348 let at = Rect {
2349 y,
2350 height: 1,
2351 ..content
2352 };
2353 if rows.more_at(i) {
2354 Paragraph::new(rows.more_line())
2355 .style(Style::default().fg(ctx.dimmed))
2356 .render(at, buf);
2357 break;
2358 }
2359 let is_selected = rows.offset + i == rows.selected;
2360 let name_style = if ctx.column_colors {
2361 Style::default().fg(ctx.type_color(&child.color_dtype()))
2362 } else {
2363 Style::default().fg(ctx.text_primary)
2364 };
2365 let mut spans = item_head(label, label_w, name_style, is_selected, rows.focused, ctx);
2366 let kind = crate::glyphs::fit_cells(&child.type_label(), type_w, g.ellipsis).into_owned();
2367 let kind_pad = type_w.saturating_sub(crate::glyphs::cell_width(&kind));
2368 spans.push(Span::styled(kind, Style::default().fg(ctx.dimmed)));
2369 spans.push(Span::raw(" ".repeat(kind_pad + GAP)));
2370 let (text, style) = node_preview(child, preview_w, ctx);
2371 let text = crate::glyphs::fit_cells(&text, preview_w, g.ellipsis).into_owned();
2372 spans.push(Span::styled(text, style));
2373 let mut paragraph = Paragraph::new(Line::from(spans));
2374 if is_selected && rows.focused {
2375 paragraph = paragraph.style(ctx.highlight_style());
2376 }
2377 paragraph.render(at, buf);
2378 }
2379}
2380
2381const TABLE_SAMPLE: usize = 32;
2384const TABLE_CELL_MAX: usize = 24;
2386
2387fn draw_table(
2390 buf: &mut Buffer,
2391 content: Rect,
2392 node: &Node,
2393 columns: &[(String, DataType)],
2394 page: &[(String, Node)],
2395 rows: &ListRows,
2396 ctx: &RenderContext,
2397) {
2398 let g = crate::glyphs::get();
2399 let width = content.width as usize;
2400 let label_w = node.label_width().max(3);
2401 let sample = node.children(0, TABLE_SAMPLE);
2402 let cell_text = |item: &Node, column: &str, room: usize| -> (String, Style) {
2403 if item.shape() == Shape::Leaf {
2404 return node_preview(item, room, ctx);
2406 }
2407 match item.cell(column) {
2408 Some(cell) => node_preview(&cell, room, ctx),
2409 None => (g.absent.to_string(), Style::default().fg(ctx.dimmed)),
2410 }
2411 };
2412 let mut widths = Vec::new();
2413 let mut used = 1 + label_w + GAP;
2414 for (column, _) in columns {
2415 let w = sample
2416 .iter()
2417 .map(|(_, item)| crate::glyphs::cell_width(&cell_text(item, column, TABLE_CELL_MAX).0))
2418 .chain(std::iter::once(crate::glyphs::cell_width(column)))
2419 .max()
2420 .unwrap_or(0)
2421 .clamp(3, TABLE_CELL_MAX);
2422 if used + w > width {
2423 break;
2424 }
2425 used += w + GAP;
2426 widths.push(w);
2427 }
2428 if widths.is_empty() {
2430 widths.push(width.saturating_sub(1 + label_w + GAP).max(1));
2431 }
2432 let hidden = columns.len() - widths.len();
2433
2434 let cell = |text: &str, w: usize| {
2435 let text = crate::glyphs::fit_cells(text, w, g.ellipsis).into_owned();
2436 let pad = w.saturating_sub(crate::glyphs::cell_width(&text));
2437 (text, " ".repeat(pad + GAP))
2438 };
2439 let mut head = vec![Span::raw(" ".repeat(1 + label_w + GAP))];
2440 for ((column, dtype), &w) in columns.iter().zip(&widths) {
2441 let (text, pad) = cell(&exact::preview(column, g), w);
2442 let fg = if ctx.column_colors {
2444 ctx.type_color(dtype)
2445 } else {
2446 ctx.dimmed
2447 };
2448 head.push(Span::styled(
2449 text,
2450 Style::default().fg(fg).add_modifier(Modifier::BOLD),
2451 ));
2452 head.push(Span::raw(pad));
2453 }
2454 if hidden > 0 {
2455 head.push(Span::styled(
2456 format!("+{hidden}"),
2457 Style::default().fg(ctx.dimmed),
2458 ));
2459 }
2460 Paragraph::new(Line::from(head)).render(
2461 Rect {
2462 y: rows.y - 1,
2464 height: 1,
2465 ..content
2466 },
2467 buf,
2468 );
2469
2470 for (i, (label, item)) in page.iter().enumerate() {
2471 let at = Rect {
2472 y: rows.y + i as u16,
2473 height: 1,
2474 ..content
2475 };
2476 if rows.more_at(i) {
2477 Paragraph::new(rows.more_line())
2478 .style(Style::default().fg(ctx.dimmed))
2479 .render(at, buf);
2480 break;
2481 }
2482 let is_selected = rows.offset + i == rows.selected;
2483 let mut spans = item_head(
2484 label,
2485 label_w,
2486 Style::default().fg(ctx.dimmed),
2487 is_selected,
2488 rows.focused,
2489 ctx,
2490 );
2491 for (k, ((column, _), &w)) in columns.iter().zip(&widths).enumerate() {
2492 if k > 0 && item.shape() == Shape::Leaf {
2493 break;
2494 }
2495 let (text, style) = cell_text(item, column, w);
2496 let (text, pad) = cell(&text, w);
2497 spans.push(Span::styled(text, style));
2498 spans.push(Span::raw(pad));
2499 }
2500 let mut paragraph = Paragraph::new(Line::from(spans));
2501 if is_selected && rows.focused {
2502 paragraph = paragraph.style(ctx.highlight_style());
2503 }
2504 paragraph.render(at, buf);
2505 }
2506}
2507
2508pub fn row_json(
2512 fields: &[InspectField],
2513 row: &InspectRow,
2514 read: Option<&FieldRead>,
2515) -> (String, usize, usize) {
2516 let mut out = String::from("{");
2517 let (mut kept, mut unread) = (0usize, 0usize);
2518 for field in fields {
2519 let column = if field.buffered() {
2520 row.values.column(&field.name).ok().cloned()
2521 } else {
2522 match read {
2523 Some(FieldRead::Read { values, .. })
2524 if read.is_some_and(|r| r.key() == (row.frame, row.row)) =>
2525 {
2526 values.column(&field.name).ok().cloned()
2527 }
2528 _ => None,
2529 }
2530 };
2531 let Some(column) = column else {
2532 unread += 1;
2533 continue;
2534 };
2535 let Ok(value) = column.get(0) else {
2536 unread += 1;
2537 continue;
2538 };
2539 if kept > 0 {
2540 out.push_str(", ");
2541 }
2542 out.push_str(&serde_json::to_string(&field.name).unwrap_or_default());
2543 out.push_str(": ");
2544 out.push_str(&json_value_text(&column, &value));
2545 kept += 1;
2546 }
2547 out.push('}');
2548 (out, kept, unread)
2549}
2550
2551fn json_value_text(column: &Column, value: &AnyValue) -> String {
2554 let quoted = |s: &str| serde_json::to_string(s).unwrap_or_default();
2555 match value {
2556 AnyValue::Null => "null".to_string(),
2557 AnyValue::Boolean(b) => b.to_string(),
2558 AnyValue::Int8(_)
2559 | AnyValue::Int16(_)
2560 | AnyValue::Int32(_)
2561 | AnyValue::Int64(_)
2562 | AnyValue::Int128(_)
2563 | AnyValue::UInt8(_)
2564 | AnyValue::UInt16(_)
2565 | AnyValue::UInt32(_)
2566 | AnyValue::UInt64(_) => exact::value_text(value),
2567 AnyValue::Float32(_) | AnyValue::Float64(_) => {
2568 let text = exact::value_text(value);
2569 if text.parse::<f64>().is_ok_and(f64::is_finite) {
2571 text
2572 } else {
2573 quoted(&text)
2574 }
2575 }
2576 v if exact::is_nested_value(v) => {
2577 exact::copy_text(column).unwrap_or_else(|_| "null".to_string())
2578 }
2579 v => quoted(&exact::value_text(v)),
2580 }
2581}
2582
2583#[cfg(test)]
2584mod tests {
2585 use super::*;
2586
2587 fn ask(width: usize) -> PaneAsk<'static> {
2588 PaneAsk {
2589 choice: None,
2590 width,
2591 table: None,
2592 indented: Indented::None,
2593 not_json: false,
2594 unpacked: Unpacked::None,
2595 read_key: "Enter",
2596 }
2597 }
2598
2599 fn lines(p: &Pane) -> Vec<String> {
2600 let mut r = reader::Reader::default();
2601 r.prepare(1, 100, reader::Wrap::Word);
2602 r.window(&p.content, 1000)
2603 .rows
2604 .into_iter()
2605 .map(|s| s.text)
2606 .collect()
2607 }
2608
2609 #[test]
2610 fn a_float_shows_exact_and_what_the_table_rounds_it_to() {
2611 let p = pane(
2612 &DataType::Float64,
2613 &Shown::Value(AnyValue::Float64(1000000.125)),
2614 &PaneAsk {
2615 table: Some("1.0000e6"),
2616 ..ask(40)
2617 },
2618 );
2619 assert_eq!(lines(&p), ["1000000.125", "", "In the table: 1.0000e6"]);
2620 assert_eq!(p.facts, "f64");
2621 let p = pane(
2622 &DataType::Float64,
2623 &Shown::Value(AnyValue::Float64(2.5)),
2624 &PaneAsk {
2625 table: Some("2.5"),
2626 ..ask(40)
2627 },
2628 );
2629 assert_eq!(lines(&p), ["2.5"]);
2630 assert!(p.views.is_empty(), "a number has one view");
2631 }
2632
2633 #[test]
2634 fn text_reads_raw_or_escaped_and_names_its_facts() {
2635 let value = Shown::Value(AnyValue::String("line1\nline2\ttab\\n"));
2636 let raw = pane(&DataType::String, &value, &ask(40));
2637 assert_eq!(lines(&raw), ["line1", "line2 tab\\n"]);
2638 let m = crate::glyphs::get().middot;
2639 assert_eq!(raw.facts, format!("str {m} 17 chars {m} 2 lines"));
2640 assert_eq!(raw.views, [View::Raw, View::Escaped]);
2641 assert_eq!(raw.next_view(), Some(View::Escaped));
2642 let esc = pane(
2643 &DataType::String,
2644 &value,
2645 &PaneAsk {
2646 choice: Some(View::Escaped),
2647 ..ask(40)
2648 },
2649 );
2650 assert_eq!(lines(&esc), [r#""line1\nline2\ttab\\n""#]);
2651 assert!(esc.facts.ends_with("escaped"));
2652 assert_eq!(esc.next_view(), Some(View::Raw));
2653 }
2654
2655 #[test]
2656 fn empty_text_and_edge_spaces_are_named() {
2657 let empty = pane(
2658 &DataType::String,
2659 &Shown::Value(AnyValue::String("")),
2660 &ask(40),
2661 );
2662 assert_eq!(lines(&empty), ["empty string"]);
2663 let esc = pane(
2664 &DataType::String,
2665 &Shown::Value(AnyValue::String("")),
2666 &PaneAsk {
2667 choice: Some(View::Escaped),
2668 ..ask(40)
2669 },
2670 );
2671 assert_eq!(lines(&esc), ["\"\""]);
2672 let padded = pane(
2673 &DataType::String,
2674 &Shown::Value(AnyValue::String(" x ")),
2675 &ask(40),
2676 );
2677 assert!(
2678 padded.facts.contains("2 leading spaces"),
2679 "{}",
2680 padded.facts
2681 );
2682 assert!(
2683 padded.facts.contains("1 trailing space"),
2684 "{}",
2685 padded.facts
2686 );
2687 }
2688
2689 #[test]
2690 fn a_null_is_not_an_empty_string_and_says_which_null() {
2691 let g = crate::glyphs::get();
2692 let null = pane(&DataType::String, &Shown::Null(NullKind::Null), &ask(60));
2693 assert_eq!(lines(&null), [format!("{} null", g.null)]);
2694 let absent = pane(&DataType::String, &Shown::Null(NullKind::Absent), &ask(80));
2695 assert!(lines(&absent)[0].starts_with(&format!("{} absent", g.absent)));
2696 }
2697
2698 #[test]
2701 fn json_text_reads_indented_and_cycles_its_views() {
2702 let doc = r#"{"order": {"id": 1, "items": [{"sku": "A1"}]}, "flags": ["vip"]}"#;
2703 let p = pane(
2704 &DataType::String,
2705 &Shown::Value(AnyValue::String(doc)),
2706 &ask(80),
2707 );
2708 assert_eq!(p.views, [View::Json, View::Raw, View::Escaped]);
2709 assert_eq!(p.view, Some(View::Json));
2710 assert!(p.facts.ends_with("json"), "{}", p.facts);
2711 assert_eq!(lines(&p)[..3], ["{", " \"order\": {", " \"id\": 1,"]);
2712 assert!(matches!(p.copy, CopyAs::Text(_)));
2713 assert_eq!(p.next_view(), Some(View::Raw));
2714 let bad = pane(
2716 &DataType::String,
2717 &Shown::Value(AnyValue::String("{not json}")),
2718 &ask(80),
2719 );
2720 assert_eq!(bad.views, [View::Raw, View::Escaped]);
2721 let long = format!("[{}0]", "1, ".repeat(30_000));
2723 let p = pane(
2724 &DataType::String,
2725 &Shown::Value(AnyValue::String(&long)),
2726 &ask(80),
2727 );
2728 assert!(p.indent && p.facts.ends_with("indenting..."), "{}", p.facts);
2729 }
2730
2731 #[test]
2735 fn bytes_are_sized_sniffed_and_read_as_text_where_they_are() {
2736 let m = crate::glyphs::get().middot;
2737 let utf8 = "Grüße\nsecond line".as_bytes();
2738 let p = pane(
2739 &DataType::Binary,
2740 &Shown::Value(AnyValue::Binary(utf8)),
2741 &ask(80),
2742 );
2743 assert_eq!(p.views, [View::Text, View::Hex, View::Escaped]);
2744 assert_eq!(lines(&p), ["Grüße", "second line"]);
2745 assert!(p.facts.contains("UTF-8 text"), "{}", p.facts);
2746
2747 let big = vec![0x90u8; 1 << 20];
2748 let p = pane(
2749 &DataType::Binary,
2750 &Shown::Value(AnyValue::Binary(&big)),
2751 &ask(140),
2752 );
2753 assert_eq!(p.facts, format!("binary {m} 1.0 MB (1,048,576 bytes)"));
2754 assert_eq!(p.views, [View::Hex, View::Escaped]);
2755 assert!(matches!(p.content, Content::Hex { per_line: 32, .. }));
2756 assert!(matches!(p.copy, CopyAs::Base64));
2757
2758 let mut e = flate2::write::GzEncoder::new(Vec::new(), flate2::Compression::default());
2759 std::io::Write::write_all(&mut e, b"hello gzip").unwrap();
2760 let gz = e.finish().unwrap();
2761 let p = pane(
2762 &DataType::Binary,
2763 &Shown::Value(AnyValue::Binary(&gz)),
2764 &ask(80),
2765 );
2766 assert!(p.facts.ends_with("gzip"), "{}", p.facts);
2767 assert_eq!(p.views, [View::Hex, View::Text, View::Escaped]);
2768 let text = pane(
2771 &DataType::Binary,
2772 &Shown::Value(AnyValue::Binary(&gz)),
2773 &PaneAsk {
2774 choice: Some(View::Text),
2775 ..ask(80)
2776 },
2777 );
2778 assert!(text.unpack);
2779 assert_eq!(lines(&text), ["Decompressing..."]);
2780 let decoded = inspector_bytes::decode_text(&gz, inspector_bytes::sniff(&gz)).unwrap();
2781 let text = pane(
2782 &DataType::Binary,
2783 &Shown::Value(AnyValue::Binary(&gz)),
2784 &PaneAsk {
2785 choice: Some(View::Text),
2786 unpacked: Unpacked::Ready(Arc::new(decoded)),
2787 ..ask(80)
2788 },
2789 );
2790 assert!(!text.unpack);
2791 assert_eq!(lines(&text), ["hello gzip"]);
2792 let failed = pane(
2794 &DataType::Binary,
2795 &Shown::Value(AnyValue::Binary(&gz)),
2796 &PaneAsk {
2797 choice: Some(View::Text),
2798 unpacked: Unpacked::Failed,
2799 ..ask(80)
2800 },
2801 );
2802 assert_eq!(failed.views, [View::Hex, View::Escaped]);
2803 assert_eq!(failed.view, Some(View::Hex));
2804 assert!(failed.facts.ends_with("not text"), "{}", failed.facts);
2805
2806 let empty = pane(
2807 &DataType::Binary,
2808 &Shown::Value(AnyValue::Binary(b"")),
2809 &ask(80),
2810 );
2811 assert_eq!(lines(&empty), ["empty binary"]);
2812 assert!(empty.facts.contains("empty"));
2813 }
2814
2815 #[test]
2816 fn empty_values_are_visible_in_the_list() {
2817 let g = crate::glyphs::get();
2818 assert_eq!(
2819 empty_preview(&AnyValue::String("")).as_deref(),
2820 Some("\"\"")
2821 );
2822 assert_eq!(empty_preview(&AnyValue::String(" ")), None);
2823 assert_eq!(
2824 empty_preview(&AnyValue::Binary(b"")),
2825 Some(format!("0 bytes {} empty", g.middot))
2826 );
2827 assert_eq!(fill_of(&Shown::Value(AnyValue::String(""))), Fill::Empty);
2828 assert_eq!(fill_of(&Shown::Null(NullKind::Null)), Fill::Null);
2829 assert_eq!(fill_of(&Shown::Unread), Fill::Unknown);
2830 }
2831
2832 #[test]
2833 fn values_differ_by_their_exact_text() {
2834 let v = |x: f64| Shown::Value(AnyValue::Float64(x));
2835 assert_eq!(differs(&v(1.0), &v(1.0)), Some(false));
2836 assert_eq!(differs(&v(f64::NAN), &v(f64::NAN)), Some(false));
2837 assert_eq!(differs(&v(1.0), &v(2.0)), Some(true));
2838 assert_eq!(differs(&v(1.0), &Shown::Null(NullKind::Null)), Some(true));
2839 assert_eq!(differs(&v(1.0), &Shown::Unread), None);
2840 }
2841
2842 #[test]
2845 fn the_list_window_keeps_the_focus_and_marks_both_ends() {
2846 assert_eq!(list_window(10, 3, 0, 20), (0, false, false));
2847 let (o, above, below) = list_window(214, 0, 0, 20);
2849 assert_eq!((o, above, below), (0, false, true));
2850 let (o, above, below) = list_window(214, 19, 0, 20);
2851 assert!(above && below);
2852 assert!(19 >= o && 19 < o + 18, "{o}");
2853 let (o, above, below) = list_window(214, 213, 0, 20);
2854 assert!(above && !below);
2855 assert_eq!(o, 214 - 19);
2856 for sel in 0..214 {
2857 let (o, above, below) = list_window(214, sel, 100, 20);
2858 let items = 20 - usize::from(above) - usize::from(below);
2859 assert!(sel >= o && sel < o + items, "{sel}: {o}");
2860 }
2861 }
2862
2863 #[test]
2867 fn the_layout_fits_the_row_and_the_terminal() {
2868 let list = |fields: usize, min_col: usize| ListShape {
2869 fields,
2870 min_col,
2871 single: false,
2872 bytes: false,
2873 };
2874 let content = Rect::new(2, 1, 76, 20);
2876 let l = layout(content, 14, 1, list(14, 40));
2877 assert!(!l.wide);
2878 assert_eq!(l.list.height, 14, "every field listed");
2879 assert_eq!(l.value.height, 4);
2880 let l = layout(content, 214, 1, list(214, 40));
2881 assert_eq!(
2882 (l.list.height, l.value.height),
2883 (15, 3),
2884 "the value keeps three"
2885 );
2886 let l = layout(content, 214, 40, list(214, 40));
2887 assert_eq!(l.value.height, 9, "a long value takes half");
2888 let l = layout(content, 2, 1_000, list(2, 40));
2890 assert_eq!((l.list.height, l.value.height), (2, 16));
2891 let content = Rect::new(2, 1, 196, 46);
2894 let l = layout(content, 214, 1, list(214, 44));
2895 assert!(l.wide);
2896 assert_eq!(l.value.height, 45);
2897 assert_eq!(l.cols, 3, "{l:?}");
2898 assert!(l.value.width as usize >= VALUE_FLOOR, "{l:?}");
2899 assert!(l.cols * l.list.height as usize >= 130);
2900 let l = layout(content, 14, 1, list(14, 44));
2901 assert_eq!((l.cols, l.value.width), (1, 88), "{l:?}");
2902 let narrowed = layout(content, 3, 1, list(214, 44));
2905 assert_eq!(narrowed.value, layout(content, 214, 1, list(214, 44)).value);
2906 let content = Rect::new(2, 1, 296, 76);
2909 let l = layout(content, 214, 1, list(214, 45));
2910 assert!(l.cols * l.list.height as usize >= 214, "{l:?}");
2911 assert_eq!(l.value.width as usize, MEASURE + 1);
2912 let bytes = ListShape {
2913 bytes: true,
2914 ..list(214, 45)
2915 };
2916 let l = layout(content, 214, 1, bytes);
2917 assert_eq!(l.value.width as usize, HEX_WIDE);
2918 assert_eq!(reader::hex_per_line(pane_width(l.value)), 32);
2919 assert!(l.cols * l.list.height as usize >= 214, "{l:?}");
2920 let compare = ListShape {
2922 single: true,
2923 ..bytes
2924 };
2925 let l = layout(content, 214, 1, compare);
2926 assert_eq!((l.cols, l.value.width as usize), (1, MEASURE + 1));
2927 let l = layout(content, 14, 1, list(14, 45));
2929 assert_eq!(l.cols, 1);
2930 assert!(layout(Rect::new(2, 1, 136, 30), 14, 1, list(14, 45)).wide);
2932 assert!(!layout(Rect::new(2, 1, 135, 30), 14, 1, list(14, 45)).wide);
2933 }
2934
2935 #[test]
2938 fn a_long_trail_drops_its_middle_steps() {
2939 let g = crate::glyphs::get();
2940 let t = g.trail;
2941 let labels = ["customer", "address", "geo", "point"];
2942 let whole = drill_title("Row 1 of 5", &labels, 200);
2943 assert_eq!(
2944 whole,
2945 format!("Row 1 of 5 {t} customer {t} address {t} geo {t} point")
2946 );
2947 let cut = drill_title("Row 1 of 5", &labels, 34);
2948 assert_eq!(
2949 cut,
2950 format!("Row 1 of 5 {t} {} {t} geo {t} point", g.ellipsis)
2951 );
2952 let marked = drill_title("Row 1", &["a\nb"], 40);
2953 assert!(!marked.contains('\n'), "{marked}");
2954 }
2955
2956 #[test]
2957 fn json_values_show_as_themselves() {
2958 let m = crate::glyphs::get().middot;
2959 let doc: JsonValue = serde_json::from_str(r#"{"a": [1, 2], "s": "x\ny"}"#).unwrap();
2960 let b = json_pane(&doc, &ask(40));
2961 assert_eq!(b.facts, format!("object {m} 2 keys"));
2962 assert_eq!(lines(&b)[..2], ["{", " \"a\": ["]);
2963 let s = json_pane(&doc["s"], &ask(40));
2964 assert_eq!(lines(&s), ["x", "y"]);
2965 assert!(s.views.contains(&View::Escaped), "text has an escaped form");
2966 let n = json_pane(&JsonValue::from(1.5), &ask(40));
2967 assert_eq!(
2968 (lines(&n), n.facts.as_str()),
2969 (vec!["1.5".to_string()], "number")
2970 );
2971 }
2972
2973 #[test]
2974 fn types_name_their_unit_and_zone_in_ascii() {
2975 let tz = TimeZone::opt_try_new(Some("Europe/Paris")).unwrap();
2976 assert_eq!(
2977 type_text(&DataType::Datetime(TimeUnit::Microseconds, tz)),
2978 "datetime[us, Europe/Paris]"
2979 );
2980 assert_eq!(type_text(&DataType::Decimal(10, 4)), "decimal(10,4)");
2981 }
2982
2983 #[test]
2984 fn sizes_read_in_human_units_and_exactly() {
2985 assert_eq!(size_text(73), "73 bytes");
2986 assert_eq!(size_text(1), "1 byte");
2987 assert_eq!(size_text(4 << 20), "4.0 MB (4,194,304 bytes)");
2988 }
2989}