diff --git a/src/app.rs b/src/app.rs index 467bb4d..6c0b8fd 100644 --- a/src/app.rs +++ b/src/app.rs @@ -124,6 +124,7 @@ impl App { }; let mut viewer = JsonViewer::new(flatjson, opt.mode); + viewer.set_preview(opt.preview); viewer.scrolloff_setting = opt.scrolloff; let screen_writer = @@ -311,12 +312,6 @@ impl App { None } } - KeyEvent(Key::Char('p')) => { - self.input_state = InputState::PendingPCommand; - self.input_buffer.clear(); - self.buffer_input(b'p'); - None - } KeyEvent(Key::Char('y')) => { match &self.clipboard_context { Ok(_) => { @@ -463,6 +458,7 @@ impl App { Key::End => Some(Action::FocusBottom), Key::Char('%') => Some(Action::FocusMatchingPair), Key::Char('m') => Some(Action::ToggleMode), + Key::Char('p') => Some(Action::TogglePreview), Key::Char('<') => { self.screen_writer .decrease_indentation_level(self.viewer.flatjson.2 as u16); diff --git a/src/jless.help b/src/jless.help index 6f1a6a9..475fde9 100644 --- a/src/jless.help +++ b/src/jless.help @@ -65,6 +65,12 @@ Space Toggle the collapsed state of the currently focused node. + DISPLAY + + m Toggle display mode: line (default), data + + p Cycle preview mode: full (default), count, none + SCROLLING ^e * Scroll down one line (or N lines). diff --git a/src/lineprinter.rs b/src/lineprinter.rs index 2ef0577..22f3418 100644 --- a/src/lineprinter.rs +++ b/src/lineprinter.rs @@ -12,7 +12,7 @@ use crate::search::MatchRangeIter; use crate::terminal; use crate::terminal::{Color, Style, Terminal}; use crate::truncatedstrview::TruncatedStrView; -use crate::viewer::Mode; +use crate::viewer::{Mode, Preview}; // This module is responsible for printing single lines of JSON to // the screen, complete with syntax highlighting and highlighting @@ -138,6 +138,7 @@ pub struct LineNumber { pub struct LinePrinter<'a, 'b> { pub mode: Mode, + pub preview: Preview, pub terminal: &'a mut dyn Terminal, // The entire FlatJson data structure and the specific line @@ -796,27 +797,44 @@ impl<'a, 'b> LinePrinter<'a, 'b> { available_space -= 1; } - let always_quote_string_object_keys = self.mode == Mode::Line; - let is_nested = false; - let mut used_space = self.generate_container_preview( - row, - available_space, - is_nested, - always_quote_string_object_keys, - )?; + let mut used_space = 0; + + if self.preview == Preview::Full { + let always_quote_string_object_keys = self.mode == Mode::Line; + let is_nested = false; + let mut used_space = self.generate_container_preview( + row, + available_space, + is_nested, + always_quote_string_object_keys, + )?; + let always_quote_string_object_keys = self.mode == Mode::Line; - if self.trailing_comma { - used_space += 1; if self.trailing_comma { - self.highlight_str( - ",", - Some(self.row.range.end), - ( - &highlighting::DEFAULT_STYLE, - &highlighting::SEARCH_MATCH_HIGHLIGHTED, - ), - )?; + used_space += 1; + if self.trailing_comma { + self.highlight_str( + ",", + Some(self.row.range.end), + ( + &highlighting::DEFAULT_STYLE, + &highlighting::SEARCH_MATCH_HIGHLIGHTED, + ), + )?; + } } + } else if self.preview == Preview::None { + self.highlight_str( + " ", + Some(self.row.range.end), + ( + &highlighting::DEFAULT_STYLE, + &highlighting::SEARCH_MATCH_HIGHLIGHTED, + ), + )?; + used_space = 1; + } else { + used_space = self.generate_container_count(row, available_space)?; } Ok(used_space) @@ -944,6 +962,55 @@ impl<'a, 'b> LinePrinter<'a, 'b> { Ok(num_printed) } + // Similar to generate_container_preview, except it only prints the count + // of the number of children: + // - number of key-val pairs in a hash, ie "{ 3 }" + // - number of items in an array, ie "[ 2 ]" + fn generate_container_count( + &mut self, + row: &Row, + available_space: isize, + ) -> Result { + debug_assert!(row.is_opening_of_container()); + + // Minimum amount of space required == 3: […] + if available_space < 3 { + return Ok(0); + } + + let mut next_sibling = row.first_child(); + let mut count: usize = 0; + while let OptionIndex::Index(child) = next_sibling { + next_sibling = self.flatjson[child].next_sibling; + count += 1; + } + + let container_type = row.value.container_type().unwrap(); + let mut count_str = format!( + "{} {} item{} {}", + container_type.open_str(), + count, + if count == 1 { "" } else { "s" }, + container_type.close_str() + ); + + if count_str.len() as isize > available_space { + count_str = format!( + "{}…{}", + container_type.open_str(), + container_type.close_str() + ); + } + + self.highlight_str( + &count_str, + Some(self.row.range.start), + highlighting::PREVIEW_STYLES, + )?; + let len = count_str.chars().count() as isize; + Ok(len) + } + // {a…: …, …} // // [a, …] @@ -1214,6 +1281,7 @@ mod tests { ) -> LinePrinter<'a, 'a> { LinePrinter { mode: Mode::Data, + preview: Preview::Full, terminal, flatjson, row: &flatjson[index], @@ -1855,6 +1923,81 @@ mod tests { Ok(()) } + #[test] + fn test_generate_countainer_count() -> std::fmt::Result { + let json_arr = r#"[1,2,3] "#; + let json_obj = r#"{"a":1, "b":2}"#; + let json_one = r#"{"c":3}"#; + // 012345678901234 (14 chars) + let fj_arr = parse_top_level_json(json_arr.to_owned()).unwrap(); + let fj_obj = parse_top_level_json(json_obj.to_owned()).unwrap(); + let fj_one = parse_top_level_json(json_one.to_owned()).unwrap(); + + let mut term = TextOnlyTerminal::new(); + let mut line: LinePrinter = LinePrinter { + preview: Preview::Count, + ..default_line_printer(&mut term, &fj_arr, 0) + }; + + for (available_space, used_space, expected) in + vec![(14, 11, r#"[ 3 items ]"#), (4, 3, r#"[…]"#), (2, 0, r#""#)].into_iter() + { + let used = line.generate_container_count(&fj_arr[0], available_space)?; + assert_eq!( + expected, + line.terminal.output(), + "expected preview with {} available columns (used up {} columns)", + available_space, + UnicodeWidthStr::width(line.terminal.output()), + ); + assert_eq!(used_space, used); + + line.terminal.clear_output(); + } + + line = LinePrinter { + preview: Preview::Count, + ..default_line_printer(&mut term, &fj_obj, 0) + }; + + for (available_space, used_space, expected) in + vec![(14, 11, r#"{ 2 items }"#), (4, 3, r#"{…}"#), (2, 0, r#""#)].into_iter() + { + let used = line.generate_container_count(&fj_obj[0], available_space)?; + assert_eq!( + expected, + line.terminal.output(), + "expected preview with {} available columns (used up {} columns)", + available_space, + UnicodeWidthStr::width(line.terminal.output()), + ); + assert_eq!(used_space, used); + + line.terminal.clear_output(); + } + + line = LinePrinter { + preview: Preview::Count, + ..default_line_printer(&mut term, &fj_one, 0) + }; + + for (available_space, used_space, expected) in vec![(14, 10, r#"{ 1 item }"#)].into_iter() { + let used = line.generate_container_count(&fj_one[0], available_space)?; + assert_eq!( + expected, + line.terminal.output(), + "expected preview with {} available columns (used up {} columns)", + available_space, + UnicodeWidthStr::width(line.terminal.output()), + ); + assert_eq!(used_space, used); + + line.terminal.clear_output(); + } + + Ok(()) + } + #[test] fn test_generate_array_preview() -> fmt::Result { let json = r#"[1, {"x": true}, null, "hello", true]"#; diff --git a/src/options.rs b/src/options.rs index e1da0ce..74338f9 100644 --- a/src/options.rs +++ b/src/options.rs @@ -2,7 +2,7 @@ use std::path::PathBuf; use clap::{ArgAction, Parser, ValueEnum}; -use crate::viewer::Mode; +use crate::viewer::{Mode, Preview}; #[derive(PartialEq, Eq, Copy, Clone, Debug, ValueEnum)] pub enum DataFormat { @@ -29,6 +29,15 @@ pub struct Opt { #[arg(short, long, value_enum, hide_possible_values = true, default_value_t = Mode::Data)] pub mode: Mode, + /// Initial preview of container nodes. In full mode (--preview full; + /// the default), containers will be rendered as much as they can be in + /// the width of the terminal. In count mode (--preview count), only + /// the child node count will be rendered. In none mode (--preview none), + /// no preview will be rendered at all. This can be toggled by pressing + /// 'p'. + #[arg(short, long, value_enum, hide_possible_values = true, default_value_t = Preview::Count)] + pub preview: Preview, + // This godforsaken configuration to get both --line-numbers and --no-line-numbers to // work (with --line-numbers as the default) and --relative-line-numbers and // --no-relative-line-numbers to work (with --no-relative-line-numbers as the default) diff --git a/src/screenwriter.rs b/src/screenwriter.rs index 115fbaa..e5a8908 100644 --- a/src/screenwriter.rs +++ b/src/screenwriter.rs @@ -255,6 +255,7 @@ impl ScreenWriter { let mut line = lp::LinePrinter { mode: viewer.mode, + preview: viewer.get_preview(), terminal: &mut self.terminal, flatjson: &viewer.flatjson, diff --git a/src/viewer.rs b/src/viewer.rs index 98190ea..3b1ac99 100644 --- a/src/viewer.rs +++ b/src/viewer.rs @@ -9,6 +9,13 @@ pub enum Mode { Data, } +#[derive(PartialEq, Eq, Copy, Clone, Debug, ValueEnum)] +pub enum Preview { + Full, + Count, + None, +} + const DEFAULT_SCROLLOFF: u16 = 3; pub struct JsonViewer { @@ -30,6 +37,9 @@ pub struct JsonViewer { // Access the functional value via .scrolloff(). pub scrolloff_setting: u16, pub mode: Mode, + + // Private because it has funny rules re: mode, see set_preview() + preview: Preview, } impl JsonViewer { @@ -43,6 +53,7 @@ impl JsonViewer { dimensions: TTYDimensions::default(), scrolloff_setting: DEFAULT_SCROLLOFF, mode, + preview: Preview::Count, } } } @@ -133,6 +144,7 @@ pub enum Action { DeepExpandNodeAndSiblings, ToggleMode, + TogglePreview, ResizeViewerDimensions(TTYDimensions), } @@ -183,6 +195,7 @@ impl JsonViewer { Action::ExpandNodeAndSiblings => self.expand_node_and_siblings(), Action::DeepExpandNodeAndSiblings => self.deep_expand_node_and_siblings(), Action::ToggleMode => self.toggle_mode(), + Action::TogglePreview => self.toggle_preview(), Action::ResizeViewerDimensions(dims) => self.dimensions = dims, } @@ -232,6 +245,7 @@ impl JsonViewer { Action::ExpandNodeAndSiblings => false, Action::DeepExpandNodeAndSiblings => false, Action::ToggleMode => false, + Action::TogglePreview => false, Action::ResizeViewerDimensions(_) => true, _ => false, } @@ -249,6 +263,7 @@ impl JsonViewer { | Action::MoveFocusedLineToCenter | Action::MoveFocusedLineToBottom | Action::ToggleMode + | Action::TogglePreview | Action::ResizeViewerDimensions(_) ) } @@ -807,6 +822,40 @@ impl JsonViewer { Mode::Line => Mode::Data, Mode::Data => Mode::Line, }; + + // Line mode doesn't use Preview::None + if self.mode == Mode::Line && self.preview == Preview::None { + self.preview = Preview::Count; + } + } + + pub fn set_preview(&mut self, val: Preview) { + if val == Preview::None && self.mode == Mode::Line { + // Emit a warning....? + self.preview = Preview::Count; + } else { + self.preview = val; + } + } + + pub fn get_preview(&self) -> Preview { + self.preview + } + + fn toggle_preview(&mut self) { + if self.mode == Mode::Data { + self.preview = match self.preview { + Preview::Full => Preview::Count, + Preview::Count => Preview::None, + Preview::None => Preview::Full, + } + } else { + self.preview = match self.preview { + Preview::Full => Preview::Count, + Preview::Count => Preview::Full, + Preview::None => Preview::Count, // this shouldn't happen, see toggle_mode() + } + } } fn scrolloff(&self) -> u16 {