// Hide console window on Windows in release builds #![cfg_attr(all(target_os = "windows", not(debug_assertions)), windows_subsystem = "windows")] mod config; mod file_tab; mod highlight; mod line_index; mod search; mod tab_manager; mod theme; mod types; mod ui; use eframe::egui; use std::sync::Arc; use config::AppConfig; use file_tab::FileTab; use highlight::HighlightManager; use search::{add_to_history, start_search, SearchParams, SearchState}; use tab_manager::{close_tab, open_file_dialog, IndexingState}; use ui::{ render_highlight_editor, render_log_view, render_search_panel, render_tabs_panel, render_top_menu, LogViewContext, SearchPanelState, }; fn main() -> eframe::Result { let options = eframe::NativeOptions { viewport: egui::ViewportBuilder::default() .with_inner_size([1200.0, 800.0]) .with_min_inner_size([800.0, 600.0]) .with_maximized(true) .with_title("RLogg - Log Viewer"), ..Default::default() }; let config = AppConfig::load(); eframe::run_native( "RLogg", options, Box::new(move |cc| { // Apply the modern dark purple theme theme::apply_theme(&cc.egui_ctx); Ok(Box::new(LogViewerApp::new(config))) }), ) } struct LogViewerApp { tabs: Vec, active_tab_index: usize, search_panel_state: SearchPanelState, indexing_state: IndexingState, search_state: SearchState, highlight_manager: HighlightManager, first_frame: bool, } impl LogViewerApp { fn new(config: AppConfig) -> Self { Self { tabs: Vec::new(), active_tab_index: 0, search_panel_state: SearchPanelState { query: config.last_search_query, case_sensitive: config.case_sensitive, use_regex: config.use_regex, history: config.search_history, }, indexing_state: IndexingState::new(), search_state: SearchState::new(), highlight_manager: HighlightManager::new(config.highlight_rules), first_frame: true, } } fn active_tab(&self) -> Option<&FileTab> { self.tabs.get(self.active_tab_index) } fn active_tab_mut(&mut self) -> Option<&mut FileTab> { self.tabs.get_mut(self.active_tab_index) } fn save_config(&self) { let config = AppConfig { search_history: self.search_panel_state.history.clone(), case_sensitive: self.search_panel_state.case_sensitive, use_regex: self.search_panel_state.use_regex, last_search_query: self.search_panel_state.query.clone(), highlight_rules: self.highlight_manager.rules.clone(), }; config.save(); } fn handle_open_file(&mut self) { if let Some(new_tab) = open_file_dialog(&self.indexing_state) { self.tabs.push(new_tab); self.active_tab_index = self.tabs.len() - 1; } } fn handle_close_tab(&mut self, index: usize) { close_tab(&mut self.tabs, &mut self.active_tab_index, index); } fn handle_search(&mut self) { if let Some(tab) = self.active_tab() { let params = SearchParams { query: self.search_panel_state.query.clone(), case_sensitive: self.search_panel_state.case_sensitive, use_regex: self.search_panel_state.use_regex, }; let line_index = Arc::clone(&tab.line_index); let file_path = tab.file_path.clone(); // Clear current results if let Some(tab) = self.active_tab_mut() { tab.filtered_lines.clear(); } start_search(&self.search_state, params, line_index, file_path); } } fn handle_clear_search(&mut self) { self.search_panel_state.query.clear(); if let Some(tab) = self.active_tab_mut() { tab.filtered_lines.clear(); } } fn handle_keyboard_input(&mut self, ctx: &egui::Context) { if let Some(tab) = self.active_tab_mut() { tab.page_scroll_direction = ctx.input(|i| { if i.key_pressed(egui::Key::PageDown) { Some(1.0) } else if i.key_pressed(egui::Key::PageUp) { Some(-1.0) } else { None } }); } } fn update_search_results(&mut self) { if let Some(filtered) = self.search_state.take_results() { if let Some(tab) = self.active_tab_mut() { tab.filtered_lines = filtered; tab.filtered_scroll_offset = 0; } } } } impl eframe::App for LogViewerApp { fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { if self.first_frame { self.first_frame = false; ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true)); } // Update search results if available self.update_search_results(); // Render top menu let mut open_file_requested = false; render_top_menu( ctx, &mut self.highlight_manager, &self.indexing_state, &mut open_file_requested, ); if open_file_requested { self.handle_open_file(); } // Render highlight editor let highlight_config_changed = render_highlight_editor(ctx, &mut self.highlight_manager); if highlight_config_changed { self.save_config(); } // Render tabs let mut close_tab_index = None; render_tabs_panel( ctx, &self.tabs, &mut self.active_tab_index, &mut close_tab_index, ); if let Some(index) = close_tab_index { self.handle_close_tab(index); } // Render search panel let match_count = self.active_tab().map(|t| t.filtered_lines.len()).unwrap_or(0); let search_actions = render_search_panel( ctx, &mut self.search_panel_state, &self.search_state, match_count, ); if search_actions.execute_search { add_to_history( &mut self.search_panel_state.history, &self.search_panel_state.query, ); self.handle_search(); } if search_actions.clear_search { self.handle_clear_search(); } if search_actions.config_changed { self.save_config(); } // Render filtered view with improved styling let show_filtered = !self.search_panel_state.query.is_empty(); let highlight_rules = self.highlight_manager.rules.clone(); if show_filtered { if let Some(tab) = self.active_tab_mut() { if !tab.filtered_lines.is_empty() { let palette = theme::get_palette(); egui::TopBottomPanel::bottom("filtered_view") .resizable(true) .default_height(250.0) .min_height(150.0) .show(ctx, |ui| { // Styled header let header_frame = egui::Frame::none() .fill(palette.bg_secondary) .inner_margin(egui::Margin::symmetric(12.0, 8.0)); header_frame.show(ui, |ui| { ui.horizontal(|ui| { let icon = egui::RichText::new("🔍") .size(16.0); ui.label(icon); let title = egui::RichText::new("Search Results") .size(16.0) .color(palette.text_primary) .strong(); ui.label(title); let count = egui::RichText::new(format!("({} matches)", tab.filtered_lines.len())) .size(14.0) .color(palette.accent_bright); ui.label(count); }); }); ui.separator(); render_log_view( ui, LogViewContext { tab, highlight_rules: &highlight_rules, show_all: false, }, ); }); } } } // Handle keyboard input self.handle_keyboard_input(ctx); // Render main view with improved styling egui::CentralPanel::default().show(ctx, |ui| { let palette = theme::get_palette(); if let Some(tab) = self.active_tab_mut() { // Styled header let header_frame = egui::Frame::none() .fill(palette.bg_secondary) .inner_margin(egui::Margin::symmetric(12.0, 8.0)); header_frame.show(ui, |ui| { ui.horizontal(|ui| { let icon = egui::RichText::new("📄") .size(16.0); ui.label(icon); let title = egui::RichText::new("Log View") .size(16.0) .color(palette.text_primary) .strong(); ui.label(title); // Show filename if let Some(filename) = tab.file_path.file_name() { ui.add_space(8.0); let filename_text = egui::RichText::new(format!("• {}", filename.to_string_lossy())) .size(14.0) .color(palette.text_secondary); ui.label(filename_text); } // Show line count ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { let line_count = egui::RichText::new(format!("{} lines", tab.line_index.total_lines)) .size(13.0) .color(palette.text_muted); ui.label(line_count); }); }); }); ui.separator(); render_log_view( ui, LogViewContext { tab, highlight_rules: &highlight_rules, show_all: true, }, ); } else { ui.centered_and_justified(|ui| { ui.vertical_centered(|ui| { ui.add_space(40.0); let icon = egui::RichText::new("📂") .size(48.0); ui.label(icon); ui.add_space(16.0); let text = egui::RichText::new("No file loaded") .size(18.0) .color(palette.text_secondary); ui.label(text); ui.add_space(8.0); let hint = egui::RichText::new("Click 'Open File' in the menu to get started") .size(14.0) .color(palette.text_muted); ui.label(hint); }); }); } }); // Only request repaint if there are ongoing background operations if self.indexing_state.is_indexing() || self.search_state.is_searching() { ctx.request_repaint(); } } }