461 lines
16 KiB
Rust
461 lines
16 KiB
Rust
// 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<FileTab>,
|
|
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();
|
|
}
|
|
}
|
|
}
|