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