// 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_export_filtered(&mut self) { use std::fs::File; use std::io::Write; use std::time::SystemTime; if let Some(tab) = self.active_tab() { if tab.filtered_lines.is_empty() { return; } // Generate timestamped filename let original_path = &tab.file_path; let file_stem = original_path .file_stem() .and_then(|s| s.to_str()) .unwrap_or("export"); let extension = original_path .extension() .and_then(|s| s.to_str()) .unwrap_or("log"); // Generate timestamp let timestamp = SystemTime::now() .duration_since(SystemTime::UNIX_EPOCH) .unwrap() .as_secs(); // Create new filename: filename_timestamp.extension let export_filename = format!("{}_{}.{}", file_stem, timestamp, extension); let export_path = original_path.with_file_name(export_filename); // Write filtered lines to file match File::create(&export_path) { Ok(mut file) => { // Get the line index and file handle let line_index = Arc::clone(&tab.line_index); let file_path = tab.file_path.clone(); // Open the original file for reading if let Ok(original_file) = File::open(&file_path) { let mut reader = std::io::BufReader::new(original_file); // Write each filtered line to the export file for filtered_line in &tab.filtered_lines { if let Some(content) = line_index.read_line(&mut reader, filtered_line.line_number) { writeln!(file, "{}", content).ok(); } } // Open the exported file in a new tab if let Ok(line_index) = crate::line_index::LineIndex::build(&export_path) { if let Ok(exported_file) = File::open(&export_path) { let new_tab = crate::file_tab::FileTab::new( export_path, line_index, exported_file, ); self.tabs.push(new_tab); self.active_tab_index = self.tabs.len() - 1; } } } } Err(e) => { eprintln!("Failed to create export file: {}", e); } } } } 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); } // Check for Ctrl+F keyboard shortcut let ctrl_f_pressed = ctx.input(|i| { i.modifiers.ctrl && i.key_pressed(egui::Key::F) }); // Check for Ctrl+Enter to execute search globally let ctrl_enter_pressed = ctx.input(|i| { i.modifiers.ctrl && i.key_pressed(egui::Key::Enter) }); // 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, ctrl_f_pressed, ); if search_actions.execute_search || ctrl_enter_pressed { 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(); let mut export_clicked = false; 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.add_space(16.0); // Export button if ui.button("📤 Export").clicked() { export_clicked = true; } }); }); ui.separator(); render_log_view( ui, LogViewContext { tab, highlight_rules: &highlight_rules, show_all: false, }, ); }); } } } // Handle export after rendering the filtered view if export_clicked { self.handle_export_filtered(); } // 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(); } } }