diff --git a/src/main.rs b/src/main.rs index edeaa0c..b24b72d 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,6 @@ use crossterm::{ use ratatui::{backend::CrosstermBackend, Terminal}; use std::{env, fs, io, io::Read, path::Path, time::Duration}; use ui::App; -use anyhow::anyhow; fn run_app( terminal: &mut Terminal>, diff --git a/src/models.rs b/src/models.rs index 87c2f9c..f228986 100644 --- a/src/models.rs +++ b/src/models.rs @@ -3,8 +3,6 @@ use chrono::NaiveDateTime; #[derive(Debug, Clone)] pub struct LogEntry { pub timestamp: NaiveDateTime, - pub hostname: String, - pub process: String, pub message: String, } diff --git a/src/parser.rs b/src/parser.rs index d04ebdc..44dfbd6 100644 --- a/src/parser.rs +++ b/src/parser.rs @@ -1,8 +1,7 @@ use crate::models::{LogEntry, SMTPSession}; use chrono::{Datelike, NaiveDateTime}; use regex::Regex; -use std::{collections::HashMap, path::Path, time::SystemTime}; -use anyhow::Context; +use std::{collections::HashMap}; lazy_static::lazy_static! { static ref LOG_LINE_RE: Regex = Regex::new( @@ -19,10 +18,6 @@ fn get_log_year(file_path: Option<&str>) -> Option { }) } -pub fn parse_log_file(contents: &str) -> anyhow::Result> { - parse_log_file_with_path(contents, None) -} - pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyhow::Result> { let mut sessions: HashMap = HashMap::new(); let log_year = get_log_year(file_path).unwrap_or_else(|| chrono::Local::now().year()); @@ -32,8 +27,6 @@ pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyh let month = &captures["month"]; let day = &captures["day"]; let time = &captures["time"]; - let hostname = captures["hostname"].to_string(); - let process = captures["process"].to_string(); let message = captures["message"].to_string(); // Create a timestamp string in the format that chrono can parse @@ -52,8 +45,6 @@ pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyh let entry = LogEntry { timestamp, - hostname, - process, message: message.clone(), }; diff --git a/src/ui.rs b/src/ui.rs index 36ca508..7b6454b 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -1,10 +1,10 @@ use crate::models::SMTPSession; -use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; +use crossterm::event::{KeyCode, KeyEvent}; use ratatui::{ prelude::*, widgets::*, style::{Color, Modifier, Style}, - text::{Line, Span, Text}, + text::{Line, Span}, layout::{Alignment, Constraint, Direction, Layout, Rect}, }; @@ -15,9 +15,9 @@ pub struct App<'a> { pub scroll: u16, pub should_quit: bool, filter_type: Option, - filter_text: String, + pub filter_text: String, pub log_display_mode: LogDisplayMode, - pub show_help: bool, + display_state: DisplayState, } #[derive(PartialEq)] @@ -32,6 +32,13 @@ enum FilterType { To, } +#[derive(PartialEq)] +enum DisplayState { + Display, + Help, + FilterInput, +} + impl<'a> App<'a> { pub fn new(sessions: &'a [SMTPSession]) -> Self { let filtered_sessions = (0..sessions.len()).collect(); @@ -44,7 +51,7 @@ impl<'a> App<'a> { filter_type: None, filter_text: String::new(), log_display_mode: LogDisplayMode::SingleLine, - show_help: false, + display_state: DisplayState::Display, } } @@ -78,122 +85,124 @@ impl<'a> App<'a> { self.selected_session = Some(self.filtered_sessions[0]); } } - - pub fn on_tick(&mut self) { - // Update app state if needed - } pub fn on_key(&mut self, key: KeyEvent) { - // If help is shown, any key should close it - if self.show_help { - self.show_help = false; - return; - } - - match key.code { - KeyCode::Char('q') => self.should_quit = true, - KeyCode::Char('c') if key.modifiers.intersects(KeyModifiers::CONTROL) => self.should_quit = true, - KeyCode::Down => { - if let Some(selected_idx) = self.selected_session { - if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { - if pos < self.filtered_sessions.len().saturating_sub(1) { - self.selected_session = Some(self.filtered_sessions[pos + 1]); + match self.display_state { + DisplayState::Display => { + match key.code { + KeyCode::Char('q') => self.should_quit = true, + KeyCode::Down => { + if let Some(selected_idx) = self.selected_session { + if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { + if pos < self.filtered_sessions.len().saturating_sub(1) { + self.selected_session = Some(self.filtered_sessions[pos + 1]); + self.scroll = 0; + } + } else if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); + } + } else if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); + } + } + KeyCode::Up => { + if let Some(selected_idx) = self.selected_session { + if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { + if pos > 0 { + self.selected_session = Some(self.filtered_sessions[pos - 1]); + self.scroll = 0; + } + } else if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); + } + } + } + KeyCode::Char('l') => { + self.log_display_mode = match self.log_display_mode { + LogDisplayMode::SingleLine => LogDisplayMode::MultiLine, + LogDisplayMode::MultiLine => LogDisplayMode::SingleLine, + }; + } + KeyCode::Char('f') => { + self.filter_type = Some(FilterType::From); + self.filter_text.clear(); + self.display_state = DisplayState::FilterInput; + } + KeyCode::Char('t') => { + self.filter_type = Some(FilterType::To); + self.filter_text.clear(); + self.display_state = DisplayState::FilterInput; + } + KeyCode::Char('r') => { + self.filter_type = None; + self.filter_text.clear(); + self.apply_filters(); + } + KeyCode::Home => { + if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); self.scroll = 0; } - } else if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); } - } else if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); - } - } - KeyCode::Up => { - if let Some(selected_idx) = self.selected_session { - if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { - if pos > 0 { - self.selected_session = Some(self.filtered_sessions[pos - 1]); + KeyCode::End => { + if let Some(&last_idx) = self.filtered_sessions.last() { + self.selected_session = Some(last_idx); self.scroll = 0; } - } else if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); } - } - } - KeyCode::Char('j') => { - self.scroll = self.scroll.saturating_add(1); - } - KeyCode::Char('k') => { - self.scroll = self.scroll.saturating_sub(1); - } - KeyCode::Char('l') => { - self.log_display_mode = match self.log_display_mode { - LogDisplayMode::SingleLine => LogDisplayMode::MultiLine, - LogDisplayMode::MultiLine => LogDisplayMode::SingleLine, - }; - } - KeyCode::Char('f') => { - self.filter_type = Some(FilterType::From); - self.filter_text.clear(); - } - KeyCode::Char('t') => { - self.filter_type = Some(FilterType::To); - self.filter_text.clear(); - } - KeyCode::Char('r') => { - self.filter_type = None; - self.filter_text.clear(); - self.apply_filters(); - } - KeyCode::Enter if self.filter_type.is_some() => { - self.apply_filters(); - self.filter_type = None; - } - KeyCode::Char(c) if self.filter_type.is_some() => { - self.filter_text.push(c); - self.apply_filters(); - } - KeyCode::Backspace if self.filter_type.is_some() => { - self.filter_text.pop(); - self.apply_filters(); - } - KeyCode::Home => { - if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); - self.scroll = 0; - } - } - KeyCode::End => { - if let Some(&last_idx) = self.filtered_sessions.last() { - self.selected_session = Some(last_idx); - self.scroll = 0; - } - } - KeyCode::PageDown => { - if let Some(selected_idx) = self.selected_session { - if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { - let new_pos = (pos + 10).min(self.filtered_sessions.len().saturating_sub(1)); - self.selected_session = Some(self.filtered_sessions[new_pos]); - self.scroll = 0; + KeyCode::PageDown => { + if let Some(selected_idx) = self.selected_session { + if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { + let new_pos = (pos + 10).min(self.filtered_sessions.len().saturating_sub(1)); + self.selected_session = Some(self.filtered_sessions[new_pos]); + self.scroll = 0; + } + } else if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); + } } - } else if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); - } - } - KeyCode::PageUp => { - if let Some(selected_idx) = self.selected_session { - if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { - let new_pos = pos.saturating_sub(10); - self.selected_session = Some(self.filtered_sessions[new_pos]); - self.scroll = 0; + KeyCode::PageUp => { + if let Some(selected_idx) = self.selected_session { + if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) { + let new_pos = pos.saturating_sub(10); + self.selected_session = Some(self.filtered_sessions[new_pos]); + self.scroll = 0; + } + } else if !self.filtered_sessions.is_empty() { + self.selected_session = Some(self.filtered_sessions[0]); + } } - } else if !self.filtered_sessions.is_empty() { - self.selected_session = Some(self.filtered_sessions[0]); + KeyCode::Char('h') => { + self.display_state = DisplayState::Help; + } + _ => {} } } - KeyCode::Char('h') => { - self.show_help = !self.show_help; + DisplayState::Help => { + match key.code { + KeyCode::Char('h') => { + self.display_state = DisplayState::Display; + } + _ => {} + } + } + DisplayState::FilterInput => { + match key.code { + KeyCode::Enter if self.filter_type.is_some() => { + self.apply_filters(); + self.display_state = DisplayState::Display; + } + KeyCode::Char(c) if self.filter_type.is_some() => { + self.filter_text.push(c); + self.apply_filters(); + } + KeyCode::Backspace if self.filter_text.len() > 0 => { + self.filter_text.pop(); + self.apply_filters(); + } + _ => {} + } } - _ => {} } } } @@ -209,15 +218,12 @@ fn render_help(f: &mut Frame, area: Rect) { Line::from(" ↑/↓ - Move selection up/down"), Line::from(" PgUp/PgDn - Move by 10 entries"), Line::from(" Home/End - Jump to first/last entry"), - Line::from(" j/k - Scroll log view up/down"), Line::from(""), Line::from("Filtering:".bold()), - Line::from(" f - Filter by sender"), - Line::from(" t - Filter by recipient"), + Line::from(" f - Filter by sender (from)"), + Line::from(" t - Filter by recipient (to)"), Line::from(" r - Reset filter"), - Line::from(" - Type to filter"), Line::from(" Enter - Apply filter"), - Line::from(" Esc - Cancel filter"), Line::from(""), Line::from("View:".bold()), Line::from(" l - Toggle log display mode (single/multi-line)"), @@ -225,7 +231,6 @@ fn render_help(f: &mut Frame, area: Rect) { Line::from(""), Line::from("Quit:".bold()), Line::from(" q - Quit"), - Line::from(" Ctrl+c - Quit"), Line::from(""), Line::from("Usage:".bold()), Line::from(" postfix-log-viewer [FILE]..."), @@ -240,7 +245,7 @@ fn render_help(f: &mut Frame, area: Rect) { } pub fn ui(f: &mut Frame, app: &App) { - if app.show_help { + if app.display_state==DisplayState::Help { render_help(f, f.size()); return; } @@ -252,16 +257,34 @@ pub fn ui(f: &mut Frame, app: &App) { // Left panel - Session list // Display filter status - let filter_status = if let Some(filter_type) = &app.filter_type { - let filter_label = match filter_type { - FilterType::From => "Filter from", - FilterType::To => "Filter to", - }; - format!("{}: {}", filter_label, app.filter_text) - } else if !app.filtered_sessions.is_empty() { - format!("Filter active ({} sessions)", app.filtered_sessions.len()) - } else { - String::from("No active filter") + let filter_status= match app.display_state { + DisplayState::Display => { + if !app.filtered_sessions.is_empty() { + if let Some(filter_type) = &app.filter_type { + let filter_label = match filter_type { + FilterType::From => "from", + FilterType::To => "to", + }; + format!("Filter {} {} ({} sessions)", filter_label, app.filter_text, app.filtered_sessions.len()) + } else { + String::from("No active filter") + } + } else { + String::from("No active filter") + } + } + DisplayState::FilterInput => { + if let Some(filter_type) = &app.filter_type { + let filter_label = match filter_type { + FilterType::From => "Filter from", + FilterType::To => "Filter to", + }; + format!("{}: {}", filter_label, app.filter_text) + } else { + String::from("") + } + } + _ => String::from(""), }; let session_list: Vec = app