diff --git a/src/app/clipboard.rs b/src/app/clipboard.rs index 8c8abaf..4eef48f 100644 --- a/src/app/clipboard.rs +++ b/src/app/clipboard.rs @@ -73,6 +73,10 @@ mod tests { status_message: None, status_set_at: None, + // JSON popup defaults + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/app/filters.rs b/src/app/filters.rs index 7971302..7eed89e 100644 --- a/src/app/filters.rs +++ b/src/app/filters.rs @@ -232,6 +232,10 @@ mod tests { status_message: None, status_set_at: None, + // JSON popup defaults + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/app/keymap.rs b/src/app/keymap.rs index ce5492b..b9d5742 100644 --- a/src/app/keymap.rs +++ b/src/app/keymap.rs @@ -21,6 +21,17 @@ impl App { return Ok(()); } + // JSON popup has highest priority once open. + if self.json_popup_open { + match key_event.code { + KeyCode::Esc | KeyCode::Char('q') => { + self.close_json_popup(); + } + _ => {} + } + return Ok(()); + } + match key_event.code { // q should NOT quit while editing or while group search is active KeyCode::Char('q') if !self.editing && !self.group_search_active => { @@ -149,6 +160,19 @@ impl App { self.copy_results_to_clipboard(); } + // Open JSON popup for current Results line (Results pane, not editing) + KeyCode::Char('j') if !self.editing && self.focus == Focus::Results => { + if let Some(line) = self.lines.get(self.results_scroll).cloned() { + // Heuristic: try to find a JSON object/array starting point + let msg_part = line + .find('{') + .or_else(|| line.find('[')) + .map(|idx| &line[idx..]) + .unwrap_or(line.as_str()); + self.open_json_popup(msg_part); + } + } + // Toggle tail mode KeyCode::Char('t') if !self.editing && !self.group_search_active => { self.tail_mode = !self.tail_mode; @@ -252,6 +276,10 @@ mod tests { status_message: None, status_set_at: None, + // JSON popup defaults + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 3ccd552..627b233 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -80,6 +80,10 @@ pub struct App { pub status_message: Option, pub status_set_at: Option, + // JSON popup state for viewing pretty-printed JSON logs + pub json_popup_open: bool, + pub json_popup_content: String, + pub saved_filters: Vec, pub save_filter_popup_open: bool, @@ -90,6 +94,29 @@ pub struct App { } impl App { + /// Open the JSON popup with pretty-printed content, if `raw` parses as JSON. + /// Falls back to the original string on parse/format errors. + pub fn open_json_popup(&mut self, raw: &str) { + // Try to parse as JSON first + if let Ok(value) = serde_json::from_str::(raw) { + if let Ok(pretty) = serde_json::to_string_pretty(&value) { + self.json_popup_content = pretty; + } else { + self.json_popup_content = raw.to_string(); + } + } else { + // Not valid JSON; just show the raw string + self.json_popup_content = raw.to_string(); + } + self.json_popup_open = true; + } + + /// Close the JSON popup, if open. + pub fn close_json_popup(&mut self) { + self.json_popup_open = false; + self.json_popup_content.clear(); + } + pub fn run(&mut self, terminal: &mut DefaultTerminal) -> io::Result<()> { while !self.exit { if self.focus == Focus::Filter && self.editing { @@ -530,6 +557,9 @@ mod tests { status_message: None, status_set_at: None, + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/main.rs b/src/main.rs index e17258c..00a354a 100644 --- a/src/main.rs +++ b/src/main.rs @@ -68,6 +68,8 @@ fn main() -> io::Result<()> { status_message: None, status_set_at: None, + json_popup_open: false, + json_popup_content: String::new(), saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/ui/mod.rs b/src/ui/mod.rs index 6c34673..a19716f 100644 --- a/src/ui/mod.rs +++ b/src/ui/mod.rs @@ -213,6 +213,49 @@ impl Widget for &App { // Call the refactored renderer self.render_results(results_inner, buf); + // JSON popup overlay (on top of main UI) + if self.json_popup_open { + // Centered popup: 70% width and height + let popup_area = Layout::vertical([ + Constraint::Percentage(15), + Constraint::Percentage(70), + Constraint::Percentage(15), + ]) + .split(area)[1]; + + let popup_chunks = Layout::horizontal([ + Constraint::Percentage(15), + Constraint::Percentage(70), + Constraint::Percentage(15), + ]) + .split(popup_area); + + let popup_rect = popup_chunks[1]; + + // Fill popup area with a solid background, similar to other popups + let popup_bg = Style::default().bg(Color::Rgb(10, 10, 10)).fg(Color::White); + for y in popup_rect.y..popup_rect.y + popup_rect.height { + for x in popup_rect.x..popup_rect.x + popup_rect.width { + if let Some(cell) = buf.cell_mut((x, y)) { + cell.set_char(' ').set_style(popup_bg); + } + } + } + + let popup_block = Block::bordered() + .title("JSON") + .style(popup_bg) + .border_style(Style::default().fg(Color::Yellow)); + + let popup_inner = popup_block.inner(popup_rect); + popup_block.render(popup_rect, buf); + + ratatui::widgets::Paragraph::new(self.json_popup_content.as_str()) + .wrap(ratatui::widgets::Wrap { trim: false }) + .style(popup_bg) + .render(popup_inner, buf); + } + let mut row_y = filter_inner.y; let field_style = @@ -558,6 +601,10 @@ mod ui_tests { status_message: None, status_set_at: None, + // JSON popup defaults + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(), diff --git a/src/ui/results.rs b/src/ui/results.rs index 4d6e4fd..8eaa70b 100644 --- a/src/ui/results.rs +++ b/src/ui/results.rs @@ -173,11 +173,15 @@ mod tests { results_scroll: 0, tail_mode: false, - tail_stop: Arc::new(AtomicBool::new(false)), + tail_stop: std::sync::Arc::new(std::sync::atomic::AtomicBool::new(false)), status_message: None, status_set_at: None, + // JSON popup defaults + json_popup_open: false, + json_popup_content: String::new(), + saved_filters: Vec::new(), save_filter_popup_open: false, save_filter_name: String::new(),