diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index dc4e509..ddc2e67 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -30,18 +30,3 @@ jobs: ARCHIVE_TYPES: ${{ matrix.archive_type }} ARCHIVE_NAME: json-lines-viewer-${{ matrix.archive_platform_name }} UPLOAD_MODE: release - publish-crate: - runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v2 - - name: Set up Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - components: rustfmt - - name: Login to crates.io - run: cargo login ${{ secrets.CRATES_TOKEN }} - - name: Publish crate - run: cargo publish --token ${{ secrets.CRATES_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index f2f9ae0..02505b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -36,9 +36,9 @@ checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" [[package]] name = "anstream" -version = "0.6.18" +version = "0.6.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b" +checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933" dependencies = [ "anstyle", "anstyle-parse", @@ -51,33 +51,33 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9" +checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd" [[package]] name = "anstyle-parse" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9" +checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" dependencies = [ "utf8parse", ] [[package]] name = "anstyle-query" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c" +checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9" dependencies = [ "windows-sys", ] [[package]] name = "anstyle-wincon" -version = "3.0.8" +version = "3.0.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6680de5231bd6ee4c6191b8a1325daa282b415391ec9d3a37bd34f2060dc73fa" +checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882" dependencies = [ "anstyle", "once_cell_polyfill", @@ -158,9 +158,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.17.0" +version = "3.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf" +checksum = "793db76d6187cd04dff33004d8e6c9cc4e05cd330500379d2394209271b4aeee" [[package]] name = "bytemuck" @@ -198,9 +198,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.25" +version = "1.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0fc897dc1e865cc67c0e05a836d9d3f1df3cbe442aa4a9473b18e12624a4951" +checksum = "956a5e21988b87f372569b66183b78babf23ebc2e744b733e4350a752c4dafac" dependencies = [ "jobserver", "libc", @@ -271,9 +271,9 @@ checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6" [[package]] name = "colorchoice" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990" +checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" [[package]] name = "compact_str" @@ -564,9 +564,9 @@ checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" [[package]] name = "flate2" -version = "1.1.1" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ced92e76e966ca2fd84c8f7aa01a4aea65b0eb6648d72f7c8f3e2764a67fece" +checksum = "4a3d7db9596fecd151c5f638c0ee5d5bd487b6e0ea232e5dc96d5250f6f94b1d" dependencies = [ "crc32fast", "libz-rs-sys", @@ -622,9 +622,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.15.3" +version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84b26c544d002229e640969970a2e74021aadf6e2f96372b9c58eff97de08eb3" +checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ "allocator-api2", "equivalent", @@ -748,7 +748,7 @@ dependencies = [ [[package]] name = "json-lines-viewer" -version = "0.4.6" +version = "0.5.0" dependencies = [ "anstyle", "anyhow", @@ -802,9 +802,9 @@ dependencies = [ [[package]] name = "liblzma-sys" -version = "0.4.3" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5839bad90c3cc2e0b8c4ed8296b80e86040240f81d46b9c0e9bc8dd51ddd3af1" +checksum = "01b9596486f6d60c3bbe644c0e1be1aa6ccc472ad630fe8927b456973d7cb736" dependencies = [ "cc", "libc", @@ -823,9 +823,9 @@ dependencies = [ [[package]] name = "libz-rs-sys" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6489ca9bd760fe9642d7644e827b0c9add07df89857b0416ee15c1cc1a3b8c5a" +checksum = "172a788537a2221661b480fee8dc5f96c580eb34fa88764d3205dc356c7e4221" dependencies = [ "zlib-rs", ] @@ -1404,9 +1404,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "0.6.8" +version = "0.6.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1" +checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3" dependencies = [ "serde", ] @@ -1483,9 +1483,9 @@ checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d" [[package]] name = "smallvec" -version = "1.15.0" +version = "1.15.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" [[package]] name = "static_assertions" @@ -1675,9 +1675,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" [[package]] name = "toml" -version = "0.8.22" +version = "0.8.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05ae329d1f08c4d17a59bed7ff5b5a769d062e64a62d34a3261b219e62cd5aae" +checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", "serde_spanned", @@ -1687,18 +1687,18 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.6.9" +version = "0.6.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3da5db5a963e24bc68be8b17b6fa82814bb22ee8660f192bb182771d498f09a3" +checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c" dependencies = [ "serde", ] [[package]] name = "toml_edit" -version = "0.22.26" +version = "0.22.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "310068873db2c5b3e7659d2cc35d21855dbafa50d1ce336397c666e3cb08137e" +checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap", "serde", @@ -1710,9 +1710,9 @@ dependencies = [ [[package]] name = "toml_write" -version = "0.1.1" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076" +checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "typenum" @@ -2094,9 +2094,9 @@ dependencies = [ [[package]] name = "zlib-rs" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "868b928d7949e09af2f6086dfc1e01936064cc7a819253bce650d4e2a2d63ba8" +checksum = "626bd9fa9734751fc50d6060752170984d7053f5a39061f524cda68023d4db8a" [[package]] name = "zopfli" diff --git a/Cargo.toml b/Cargo.toml index c2842cf..bd3c7a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "json-lines-viewer" -version = "0.4.6" +version = "0.5.0" edition = "2024" description = "JSON Lines Viewer - Terminal-UI to view JSON line files (e.g. application logs)" authors = ["bitmagier"] diff --git a/planning.md b/planning.md index 85990c7..967ae50 100644 --- a/planning.md +++ b/planning.md @@ -1,8 +1,5 @@ ## TODO's Version 1 -- feature: highlight all search hits on all screens with a style (e.g. underline or background color) - - main screen: mark line - - object screen: mark line - - value details screen: mark text section +- case-insensitive search ## (Version 2): Should be a fork with a different name - e.g. json-viewer - rewrite: generalize viewer to any kind of json and any object depth diff --git a/src/model.rs b/src/model.rs index f88bf94..a54cfb1 100644 --- a/src/model.rs +++ b/src/model.rs @@ -1,6 +1,6 @@ use crate::props::Props; use crate::raw_json_lines::RawJsonLines; -use ratatui::prelude::{Color, Line, Size, Stylize}; +use ratatui::prelude::{Color, Line, Size, Span, Style, Stylize}; use ratatui::style::Styled; use ratatui::text::ToSpan; use ratatui::widgets::{ListItem, ListState}; @@ -43,10 +43,13 @@ impl Default for ModelViewState { #[derive(Clone, Default)] pub struct FindTask { pub search_string: String, - pub found: Option + pub found: Option, } impl FindTask { - pub fn add_search_char(&mut self, c: char) { + pub fn add_search_char( + &mut self, + c: char, + ) { self.search_string.push(c); self.found = None; } @@ -103,9 +106,7 @@ impl<'a> Model<'a> { } } - pub fn has_find_task(&self) -> bool { - self.find_task.is_some() - } + pub fn has_find_task(&self) -> bool { self.find_task.is_some() } pub fn updated( mut self, @@ -147,9 +148,7 @@ impl<'a> Model<'a> { self.find_next(true); (self, None) } - Message::Enter => { - (self, Some(Message::ScrollDown)) - } + Message::Enter => (self, Some(Message::ScrollDown)), Message::Exit => { self.find_task = None; (self, None) @@ -281,7 +280,8 @@ impl<'a> Model<'a> { }, Screen::ValueDetails => match msg { Message::ScrollUp => { - self.view_state.value_screen_vertical_scroll_offset = self.view_state.value_screen_vertical_scroll_offset.saturating_sub(1); + self.view_state.value_screen_vertical_scroll_offset = + self.view_state.value_screen_vertical_scroll_offset.saturating_sub(1); (self, None) } Message::ScrollDown => { @@ -289,7 +289,8 @@ impl<'a> Model<'a> { (self, None) } Message::PageUp => { - self.view_state.value_screen_vertical_scroll_offset = self.view_state.value_screen_vertical_scroll_offset.saturating_sub(self.page_len()); + self.view_state.value_screen_vertical_scroll_offset = + self.view_state.value_screen_vertical_scroll_offset.saturating_sub(self.page_len()); (self, None) } Message::PageDown => { @@ -320,22 +321,46 @@ impl<'a> Model<'a> { self.find_task = None; } + pub fn with_search_hits_marked<'b>( + &self, + text: String, + ) -> Vec> { + if let Some(t) = self.find_task.as_ref() && !t.search_string.is_empty(){ + let mut i = 0; + let mut spans = vec![]; + + while let Some(hit) = text[i..].find(&t.search_string) { + spans.push(Span::from(text[i..i+hit].to_string())); + spans.push(Span::from(text[i+hit..i+hit+t.search_string.len()].to_string()).set_style(Self::find_matches_style())); + i = i+hit+t.search_string.len(); + } + + if i < text.len() { + spans.push(Span::from(text[i..].to_string())); + } + + spans + } else { + vec![Span::from(text)] + } + } + fn render_json_line<'x>( &self, m: &serde_json::Map, ) -> Line<'x> { - fn render_property( - line: &mut Line, - k: &str, - v: &serde_json::Value, - ) { + let render_property = |line: &mut Line, k: &str, v: &serde_json::Value| { if line.iter().len() > 0 { line.push_span(", "); } - line.push_span(k.to_owned().bold()); + for e in self.with_search_hits_marked(k.to_owned()) { + line.push_span(e.bold()); + } line.push_span(":".to_owned()); - line.push_span(format!("{v}")); - } + for e in self.with_search_hits_marked(format!("{v}")) { + line.push_span(e) + } + }; let mut line = Line::default(); let mut num_fields = 0; @@ -366,7 +391,11 @@ impl<'a> Model<'a> { /// returns JSON object lines and keys in rendered order pub fn produce_line_details_screen_content(&self) -> (Vec, Vec) { - let line_idx = self.view_state.main_window_list_state.selected().expect("we should find a a selected line"); + let line_idx = self + .view_state + .main_window_list_state + .selected() + .expect("we should find a a selected line"); self.raw_json_lines.lines[line_idx].produce_rendered_fields_as_list(&self.props.fields_order) } @@ -384,9 +413,7 @@ impl<'a> Model<'a> { format!("{}:{}", source_name, raw_line.line_nr) } - pub fn render_status_line_right(&self) -> String { - self.last_action_result.clone() - } + pub fn render_status_line_right(&self) -> String { self.last_action_result.clone() } pub fn render_find_task_line_left(&self) -> Line { let Some(task) = &self.find_task else { @@ -424,9 +451,7 @@ impl<'a> Model<'a> { } } - pub fn page_len(&self) -> u16 { - self.terminal_size.height.saturating_sub(2) - } + pub fn page_len(&self) -> u16 { self.terminal_size.height.saturating_sub(2) } fn save_settings(&mut self) { self.last_action_result = match self.props.save() { @@ -435,8 +460,10 @@ impl<'a> Model<'a> { }; } - - fn find_next(&mut self, skip_current_line: bool) { + fn find_next( + &mut self, + skip_current_line: bool, + ) { let mut find_task = self.find_task.clone().expect("find task should be set"); if find_task.found.is_none() { find_task.found = Some(false); @@ -445,7 +472,11 @@ impl<'a> Model<'a> { match self.active_screen { Screen::Done => (), Screen::Main => { - let mut start_line_num = self.view_state.main_window_list_state.selected().unwrap_or(self.view_state.main_window_list_state.offset()); + let mut start_line_num = self + .view_state + .main_window_list_state + .selected() + .unwrap_or(self.view_state.main_window_list_state.offset()); if skip_current_line { start_line_num += 1 } @@ -453,12 +484,16 @@ impl<'a> Model<'a> { if line.content.contains(&find_task.search_string) { find_task.found = Some(true); self.view_state.main_window_list_state.select(Some(start_line_num + idx)); - break + break; } } } Screen::ObjectDetails => { - let mut start_line_num = self.view_state.object_detail_list_state.selected().unwrap_or(self.view_state.object_detail_list_state.offset()); + let mut start_line_num = self + .view_state + .object_detail_list_state + .selected() + .unwrap_or(self.view_state.object_detail_list_state.offset()); if skip_current_line { start_line_num += 1 } @@ -488,7 +523,11 @@ impl<'a> Model<'a> { match self.active_screen { Screen::Done => {} Screen::Main => { - let start_line_num = self.view_state.main_window_list_state.selected().unwrap_or(self.view_state.main_window_list_state.offset()); + let start_line_num = self + .view_state + .main_window_list_state + .selected() + .unwrap_or(self.view_state.main_window_list_state.offset()); for (idx, line) in self.raw_json_lines.lines[..start_line_num].iter().rev().enumerate() { if line.content.contains(&find_task.search_string) { find_task.found = Some(true); @@ -498,7 +537,11 @@ impl<'a> Model<'a> { } } Screen::ObjectDetails => { - let start_line_num = self.view_state.object_detail_list_state.selected().unwrap_or(self.view_state.object_detail_list_state.offset()); + let start_line_num = self + .view_state + .object_detail_list_state + .selected() + .unwrap_or(self.view_state.object_detail_list_state.offset()); let (lines, field_names) = self.produce_line_details_screen_content(); for (idx, line) in lines[..start_line_num].iter().rev().enumerate() { if line.contains(&find_task.search_string) { @@ -514,6 +557,10 @@ impl<'a> Model<'a> { } self.find_task = Some(find_task); } + + fn find_matches_style() -> Style { + Style::new().on_yellow() + } } pub struct ModelIntoIter<'a> { @@ -538,9 +585,7 @@ impl<'a> IntoIterator for &'a Model<'a> { type Item = ListItem<'a>; type IntoIter = ModelIntoIter<'a>; - fn into_iter(self) -> Self::IntoIter { - ModelIntoIter { model: self, index: 0 } - } + fn into_iter(self) -> Self::IntoIter { ModelIntoIter { model: self, index: 0 } } } impl<'a> Iterator for ModelIntoIter<'a> { @@ -558,9 +603,7 @@ impl<'a> Iterator for ModelIntoIter<'a> { Some(ListItem::new(line)) } - fn size_hint(&self) -> (usize, Option) { - (0, Some(self.model.raw_json_lines.lines.len() - self.index)) - } + fn size_hint(&self) -> (usize, Option) { (0, Some(self.model.raw_json_lines.lines.len() - self.index)) } fn advance_by( &mut self, diff --git a/src/terminal.rs b/src/terminal.rs index 064b9a2..adedc9d 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -101,6 +101,8 @@ fn render_line_details_screen( ) -> Option { let (block, cursor_position) = produce_screen_border(frame.area(), model); let (list_items, keys_in_rendered_order) = model.produce_line_details_screen_content(); + let list_items = list_items.into_iter() + .map(|e| Line::from(model.with_search_hits_marked(e))); let json_field_list = List::new(list_items) .block(block) .highlight_style(Style::new().underlined())