use std::path::PathBuf;
use crate::logging::reader::{self, LogEntry, LogLevel};
const PAGE_ROWS: usize = 20;
#[derive(Debug, Clone)]
pub struct LogViewerState {
pub files: Vec<PathBuf>,
pub file_index: usize,
pub entries: Vec<LogEntry>,
pub min_level: LogLevel,
pub search: String,
pub search_active: bool,
pub scroll: usize,
pub viewport_rows: usize,
pub error: Option<String>,
}
impl Default for LogViewerState {
fn default() -> Self {
Self {
files: Vec::new(),
file_index: 0,
entries: Vec::new(),
min_level: LogLevel::Debug,
search: String::new(),
search_active: false,
scroll: 0,
viewport_rows: 0,
error: None,
}
}
}
impl LogViewerState {
pub fn open() -> Self {
let files = reader::available_logs(&reader::log_dir());
let mut state = Self {
file_index: files.len().saturating_sub(1),
files,
..Default::default()
};
state.load();
state
}
pub fn load(&mut self) {
self.entries.clear();
self.error = None;
self.scroll = 0;
let Some(path) = self.files.get(self.file_index) else {
self.error = Some("No log files found. Is file logging enabled?".to_string());
return;
};
match reader::tail_entries(path, reader::TAIL_BYTES) {
Ok(entries) => self.entries = entries,
Err(e) => self.error = Some(format!("Cannot read {}: {e}", path.display())),
}
}
pub fn file_name(&self) -> String {
self.files
.get(self.file_index)
.and_then(|p| p.file_name())
.and_then(|n| n.to_str())
.unwrap_or("(no file)")
.to_string()
}
pub fn visible(&self) -> Vec<&LogEntry> {
self.entries
.iter()
.filter(|e| e.level.at_least(self.min_level) && e.matches(&self.search))
.collect()
}
pub fn content_rows(&self) -> usize {
self.visible().iter().map(|e| e.lines()).sum()
}
pub fn max_scroll(&self) -> usize {
self.content_rows().saturating_sub(self.viewport_rows)
}
pub fn scroll_to_end(&mut self) {
self.scroll = self.max_scroll();
}
pub fn scroll_up(&mut self, rows: usize) {
self.scroll = self.scroll.saturating_sub(rows);
}
pub fn scroll_down(&mut self, rows: usize) {
self.scroll = self.scroll.saturating_add(rows).min(self.max_scroll());
}
pub fn page_up(&mut self) {
self.scroll_up(PAGE_ROWS);
}
pub fn page_down(&mut self) {
self.scroll_down(PAGE_ROWS);
}
pub fn set_level(&mut self, level: LogLevel) {
self.min_level = level;
self.scroll = 0;
}
pub fn step_file(&mut self, delta: isize) {
if self.files.is_empty() {
return;
}
let last = self.files.len() - 1;
let next = match delta.cmp(&0) {
std::cmp::Ordering::Less => self.file_index.saturating_sub(delta.unsigned_abs()),
std::cmp::Ordering::Greater => (self.file_index + delta as usize).min(last),
std::cmp::Ordering::Equal => self.file_index,
};
if next != self.file_index {
self.file_index = next;
self.load();
self.scroll_to_end();
}
}
pub fn push_search(&mut self, c: char) {
self.search.push(c);
self.scroll = 0;
}
pub fn pop_search(&mut self) {
if self.search.is_empty() {
self.search_active = false;
} else {
self.search.pop();
}
self.scroll = 0;
}
pub fn clear_search(&mut self) {
self.search.clear();
self.search_active = false;
self.scroll = 0;
}
}
pub fn level_for_key(c: char) -> Option<LogLevel> {
match c {
'1' | 'e' => Some(LogLevel::Error),
'2' | 'w' => Some(LogLevel::Warn),
'3' | 'i' => Some(LogLevel::Info),
'4' | 'd' => Some(LogLevel::Debug),
'5' | 't' => Some(LogLevel::Trace),
_ => None,
}
}