Nettoyage du code généré par IA.
Remise à plat des états de l'application (saisie, affichage...)
This commit is contained in:
@@ -11,7 +11,6 @@ use crossterm::{
|
|||||||
use ratatui::{backend::CrosstermBackend, Terminal};
|
use ratatui::{backend::CrosstermBackend, Terminal};
|
||||||
use std::{env, fs, io, io::Read, path::Path, time::Duration};
|
use std::{env, fs, io, io::Read, path::Path, time::Duration};
|
||||||
use ui::App;
|
use ui::App;
|
||||||
use anyhow::anyhow;
|
|
||||||
|
|
||||||
fn run_app(
|
fn run_app(
|
||||||
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
terminal: &mut Terminal<CrosstermBackend<io::Stdout>>,
|
||||||
|
|||||||
@@ -3,8 +3,6 @@ use chrono::NaiveDateTime;
|
|||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct LogEntry {
|
pub struct LogEntry {
|
||||||
pub timestamp: NaiveDateTime,
|
pub timestamp: NaiveDateTime,
|
||||||
pub hostname: String,
|
|
||||||
pub process: String,
|
|
||||||
pub message: String,
|
pub message: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
use crate::models::{LogEntry, SMTPSession};
|
use crate::models::{LogEntry, SMTPSession};
|
||||||
use chrono::{Datelike, NaiveDateTime};
|
use chrono::{Datelike, NaiveDateTime};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
use std::{collections::HashMap, path::Path, time::SystemTime};
|
use std::{collections::HashMap};
|
||||||
use anyhow::Context;
|
|
||||||
|
|
||||||
lazy_static::lazy_static! {
|
lazy_static::lazy_static! {
|
||||||
static ref LOG_LINE_RE: Regex = Regex::new(
|
static ref LOG_LINE_RE: Regex = Regex::new(
|
||||||
@@ -19,10 +18,6 @@ fn get_log_year(file_path: Option<&str>) -> Option<i32> {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn parse_log_file(contents: &str) -> anyhow::Result<Vec<SMTPSession>> {
|
|
||||||
parse_log_file_with_path(contents, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyhow::Result<Vec<SMTPSession>> {
|
pub fn parse_log_file_with_path(contents: &str, file_path: Option<&str>) -> anyhow::Result<Vec<SMTPSession>> {
|
||||||
let mut sessions: HashMap<String, SMTPSession> = HashMap::new();
|
let mut sessions: HashMap<String, SMTPSession> = HashMap::new();
|
||||||
let log_year = get_log_year(file_path).unwrap_or_else(|| chrono::Local::now().year());
|
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 month = &captures["month"];
|
||||||
let day = &captures["day"];
|
let day = &captures["day"];
|
||||||
let time = &captures["time"];
|
let time = &captures["time"];
|
||||||
let hostname = captures["hostname"].to_string();
|
|
||||||
let process = captures["process"].to_string();
|
|
||||||
let message = captures["message"].to_string();
|
let message = captures["message"].to_string();
|
||||||
|
|
||||||
// Create a timestamp string in the format that chrono can parse
|
// 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 {
|
let entry = LogEntry {
|
||||||
timestamp,
|
timestamp,
|
||||||
hostname,
|
|
||||||
process,
|
|
||||||
message: message.clone(),
|
message: message.clone(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
273
src/ui.rs
273
src/ui.rs
@@ -1,10 +1,10 @@
|
|||||||
use crate::models::SMTPSession;
|
use crate::models::SMTPSession;
|
||||||
use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
|
use crossterm::event::{KeyCode, KeyEvent};
|
||||||
use ratatui::{
|
use ratatui::{
|
||||||
prelude::*,
|
prelude::*,
|
||||||
widgets::*,
|
widgets::*,
|
||||||
style::{Color, Modifier, Style},
|
style::{Color, Modifier, Style},
|
||||||
text::{Line, Span, Text},
|
text::{Line, Span},
|
||||||
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
layout::{Alignment, Constraint, Direction, Layout, Rect},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -15,9 +15,9 @@ pub struct App<'a> {
|
|||||||
pub scroll: u16,
|
pub scroll: u16,
|
||||||
pub should_quit: bool,
|
pub should_quit: bool,
|
||||||
filter_type: Option<FilterType>,
|
filter_type: Option<FilterType>,
|
||||||
filter_text: String,
|
pub filter_text: String,
|
||||||
pub log_display_mode: LogDisplayMode,
|
pub log_display_mode: LogDisplayMode,
|
||||||
pub show_help: bool,
|
display_state: DisplayState,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(PartialEq)]
|
#[derive(PartialEq)]
|
||||||
@@ -32,6 +32,13 @@ enum FilterType {
|
|||||||
To,
|
To,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq)]
|
||||||
|
enum DisplayState {
|
||||||
|
Display,
|
||||||
|
Help,
|
||||||
|
FilterInput,
|
||||||
|
}
|
||||||
|
|
||||||
impl<'a> App<'a> {
|
impl<'a> App<'a> {
|
||||||
pub fn new(sessions: &'a [SMTPSession]) -> Self {
|
pub fn new(sessions: &'a [SMTPSession]) -> Self {
|
||||||
let filtered_sessions = (0..sessions.len()).collect();
|
let filtered_sessions = (0..sessions.len()).collect();
|
||||||
@@ -44,7 +51,7 @@ impl<'a> App<'a> {
|
|||||||
filter_type: None,
|
filter_type: None,
|
||||||
filter_text: String::new(),
|
filter_text: String::new(),
|
||||||
log_display_mode: LogDisplayMode::SingleLine,
|
log_display_mode: LogDisplayMode::SingleLine,
|
||||||
show_help: false,
|
display_state: DisplayState::Display,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,121 +86,123 @@ impl<'a> App<'a> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn on_tick(&mut self) {
|
|
||||||
// Update app state if needed
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn on_key(&mut self, key: KeyEvent) {
|
pub fn on_key(&mut self, key: KeyEvent) {
|
||||||
// If help is shown, any key should close it
|
match self.display_state {
|
||||||
if self.show_help {
|
DisplayState::Display => {
|
||||||
self.show_help = false;
|
match key.code {
|
||||||
return;
|
KeyCode::Char('q') => self.should_quit = true,
|
||||||
}
|
KeyCode::Down => {
|
||||||
|
if let Some(selected_idx) = self.selected_session {
|
||||||
match key.code {
|
if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) {
|
||||||
KeyCode::Char('q') => self.should_quit = true,
|
if pos < self.filtered_sessions.len().saturating_sub(1) {
|
||||||
KeyCode::Char('c') if key.modifiers.intersects(KeyModifiers::CONTROL) => self.should_quit = true,
|
self.selected_session = Some(self.filtered_sessions[pos + 1]);
|
||||||
KeyCode::Down => {
|
self.scroll = 0;
|
||||||
if let Some(selected_idx) = self.selected_session {
|
}
|
||||||
if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) {
|
} else if !self.filtered_sessions.is_empty() {
|
||||||
if pos < self.filtered_sessions.len().saturating_sub(1) {
|
self.selected_session = Some(self.filtered_sessions[0]);
|
||||||
self.selected_session = Some(self.filtered_sessions[pos + 1]);
|
}
|
||||||
|
} 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;
|
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() {
|
KeyCode::End => {
|
||||||
self.selected_session = Some(self.filtered_sessions[0]);
|
if let Some(&last_idx) = self.filtered_sessions.last() {
|
||||||
}
|
self.selected_session = Some(last_idx);
|
||||||
}
|
|
||||||
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;
|
self.scroll = 0;
|
||||||
}
|
}
|
||||||
} else if !self.filtered_sessions.is_empty() {
|
|
||||||
self.selected_session = Some(self.filtered_sessions[0]);
|
|
||||||
}
|
}
|
||||||
}
|
KeyCode::PageDown => {
|
||||||
}
|
if let Some(selected_idx) = self.selected_session {
|
||||||
KeyCode::Char('j') => {
|
if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) {
|
||||||
self.scroll = self.scroll.saturating_add(1);
|
let new_pos = (pos + 10).min(self.filtered_sessions.len().saturating_sub(1));
|
||||||
}
|
self.selected_session = Some(self.filtered_sessions[new_pos]);
|
||||||
KeyCode::Char('k') => {
|
self.scroll = 0;
|
||||||
self.scroll = self.scroll.saturating_sub(1);
|
}
|
||||||
}
|
} else if !self.filtered_sessions.is_empty() {
|
||||||
KeyCode::Char('l') => {
|
self.selected_session = Some(self.filtered_sessions[0]);
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
} else if !self.filtered_sessions.is_empty() {
|
KeyCode::PageUp => {
|
||||||
self.selected_session = Some(self.filtered_sessions[0]);
|
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);
|
||||||
KeyCode::PageUp => {
|
self.selected_session = Some(self.filtered_sessions[new_pos]);
|
||||||
if let Some(selected_idx) = self.selected_session {
|
self.scroll = 0;
|
||||||
if let Some(pos) = self.filtered_sessions.iter().position(|&i| i == selected_idx) {
|
}
|
||||||
let new_pos = pos.saturating_sub(10);
|
} else if !self.filtered_sessions.is_empty() {
|
||||||
self.selected_session = Some(self.filtered_sessions[new_pos]);
|
self.selected_session = Some(self.filtered_sessions[0]);
|
||||||
self.scroll = 0;
|
}
|
||||||
}
|
}
|
||||||
} else if !self.filtered_sessions.is_empty() {
|
KeyCode::Char('h') => {
|
||||||
self.selected_session = Some(self.filtered_sessions[0]);
|
self.display_state = DisplayState::Help;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
KeyCode::Char('h') => {
|
DisplayState::Help => {
|
||||||
self.show_help = !self.show_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(" ↑/↓ - Move selection up/down"),
|
||||||
Line::from(" PgUp/PgDn - Move by 10 entries"),
|
Line::from(" PgUp/PgDn - Move by 10 entries"),
|
||||||
Line::from(" Home/End - Jump to first/last entry"),
|
Line::from(" Home/End - Jump to first/last entry"),
|
||||||
Line::from(" j/k - Scroll log view up/down"),
|
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("Filtering:".bold()),
|
Line::from("Filtering:".bold()),
|
||||||
Line::from(" f - Filter by sender"),
|
Line::from(" f - Filter by sender (from)"),
|
||||||
Line::from(" t - Filter by recipient"),
|
Line::from(" t - Filter by recipient (to)"),
|
||||||
Line::from(" r - Reset filter"),
|
Line::from(" r - Reset filter"),
|
||||||
Line::from(" <text> - Type to filter"),
|
|
||||||
Line::from(" Enter - Apply filter"),
|
Line::from(" Enter - Apply filter"),
|
||||||
Line::from(" Esc - Cancel filter"),
|
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("View:".bold()),
|
Line::from("View:".bold()),
|
||||||
Line::from(" l - Toggle log display mode (single/multi-line)"),
|
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(""),
|
||||||
Line::from("Quit:".bold()),
|
Line::from("Quit:".bold()),
|
||||||
Line::from(" q - Quit"),
|
Line::from(" q - Quit"),
|
||||||
Line::from(" Ctrl+c - Quit"),
|
|
||||||
Line::from(""),
|
Line::from(""),
|
||||||
Line::from("Usage:".bold()),
|
Line::from("Usage:".bold()),
|
||||||
Line::from(" postfix-log-viewer [FILE]..."),
|
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) {
|
pub fn ui(f: &mut Frame, app: &App) {
|
||||||
if app.show_help {
|
if app.display_state==DisplayState::Help {
|
||||||
render_help(f, f.size());
|
render_help(f, f.size());
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -252,16 +257,34 @@ pub fn ui(f: &mut Frame, app: &App) {
|
|||||||
|
|
||||||
// Left panel - Session list
|
// Left panel - Session list
|
||||||
// Display filter status
|
// Display filter status
|
||||||
let filter_status = if let Some(filter_type) = &app.filter_type {
|
let filter_status= match app.display_state {
|
||||||
let filter_label = match filter_type {
|
DisplayState::Display => {
|
||||||
FilterType::From => "Filter from",
|
if !app.filtered_sessions.is_empty() {
|
||||||
FilterType::To => "Filter to",
|
if let Some(filter_type) = &app.filter_type {
|
||||||
};
|
let filter_label = match filter_type {
|
||||||
format!("{}: {}", filter_label, app.filter_text)
|
FilterType::From => "from",
|
||||||
} else if !app.filtered_sessions.is_empty() {
|
FilterType::To => "to",
|
||||||
format!("Filter active ({} sessions)", app.filtered_sessions.len())
|
};
|
||||||
} else {
|
format!("Filter {} {} ({} sessions)", filter_label, app.filter_text, app.filtered_sessions.len())
|
||||||
String::from("No active filter")
|
} 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<ListItem> = app
|
let session_list: Vec<ListItem> = app
|
||||||
|
|||||||
Reference in New Issue
Block a user