Compare commits

...

2 Commits

Author SHA1 Message Date
5f8facbd46 build fix?
All checks were successful
Build / Build - linux (push) Successful in 8m44s
2025-12-11 19:43:47 +01:00
3d3ca9a81b start to clean up vibecode 2025-12-11 19:32:38 +01:00
9 changed files with 558 additions and 477 deletions

View File

@@ -38,22 +38,36 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate dependency hash
id: deps_hash
run: |
# Create a hash of dependencies only (excludes package version)
# This prevents cache invalidation on version bumps
{
# Get all dependencies from Cargo.toml (skip package.version)
grep -A 9999 '^\[dependencies' Cargo.toml 2>/dev/null || true
grep -A 9999 '^\[dev-dependencies' Cargo.toml 2>/dev/null || true
grep -A 9999 '^\[build-dependencies' Cargo.toml 2>/dev/null || true
# Include Cargo.lock for exact dependency versions
cat Cargo.lock 2>/dev/null || true
} | sha256sum | cut -d' ' -f1 > /tmp/deps_hash.txt
echo "hash=$(cat /tmp/deps_hash.txt)" >> $GITHUB_OUTPUT
- name: Cache Cargo Dependencies
uses: actions/cache@v3
uses: https://gitea.com/actions/cache@v3
with:
# Path to cache: The 'target' directory contains all compiled libraries
path: |
~/.cargo/bin/
~/.cargo/registry/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
# Key: Changes if OS, Rust toolchain, or dependencies change
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ matrix.target }}
# Restore Keys: Use for finding a cache from a slightly older lock file
# Key: Changes only when dependencies change, not when version changes
key: ${{ runner.os }}-cargo-${{ steps.deps_hash.outputs.hash }}-${{ matrix.target }}
# Restore Keys: Fallback to any cache for this target
restore-keys: |
${{ runner.os }}-cargo-${{ matrix.target }}-
${{ runner.os }}-cargo-${{ steps.deps_hash.outputs.hash }}-
${{ runner.os }}-cargo-
env:
GHA_TAR_PATH: /usr/bin/tar
- name: Setup cross-compilation tools
if: matrix.setup_cmd != ''

2
Cargo.lock generated
View File

@@ -2706,7 +2706,7 @@ dependencies = [
[[package]]
name = "rlogg"
version = "0.3.0"
version = "0.3.1"
dependencies = [
"chrono",
"eframe",

View File

@@ -1,6 +1,6 @@
[package]
name = "rlogg"
version = "0.3.0"
version = "0.3.1"
edition = "2024"
authors = ["Stanislav Pastushenko <staspast1@gmail.com>"]
description = "A fast log file viewer with search, filtering, and highlighting capabilities"

View File

@@ -1,3 +1,8 @@
FROM rust:latest
RUN apt update && \
apt install nodejs npm -y
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nodejs \
npm \
tar \
gzip \
&& rm -rf /var/lib/apt/lists/*

359
src/log_viewer_app.rs Normal file
View File

@@ -0,0 +1,359 @@
use std::sync::Arc;
use eframe::egui;
use crate::config::AppConfig;
use crate::file_tab::FileTab;
use crate::highlight::HighlightManager;
use crate::search::{add_to_history, start_search, SearchParams, SearchState};
use crate::tab_manager::{close_tab, open_file_dialog, IndexingState};
use crate::ui::{render_highlight_editor, render_search_panel, render_tabs_panel, render_top_menu, SearchPanelState};
use crate::ui::log_panel::{render_filter_panel, render_main_log_panel};
pub 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,
}
struct KeyAction {
focus_search_input: bool,
execute_search: bool
}
impl KeyAction {
pub fn new() -> Self {
Self{
focus_search_input: false,
execute_search: false,
}
}
}
impl LogViewerApp {
pub 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,
date_range_enabled: config.date_range_enabled,
date_format: config.date_format,
date_from: config.date_from,
date_to: config.date_to,
},
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(),
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
config.save();
}
fn handle_open_file(&mut self, open_file_requested: bool) {
if open_file_requested && 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() {
// Validate date format if date range is enabled
if self.search_panel_state.date_range_enabled {
use chrono::NaiveDateTime;
// Validate format by trying to parse example dates
let test_date = "2025-01-01 00:00:00";
if NaiveDateTime::parse_from_str(test_date, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date format: {}", self.search_panel_state.date_format);
eprintln!("Expected format like: %Y-%m-%d %H:%M:%S");
return;
}
// Validate date_from and date_to can be parsed
if !self.search_panel_state.date_from.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_from, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_from format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
if !self.search_panel_state.date_to.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_to, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_to format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
}
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,
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
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;
}
}
}
fn handle_first_frame(&mut self, ctx: &egui::Context) {
if self.first_frame {
self.first_frame = false;
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
}
}
}
fn handle_key_inputs(ctx: &egui::Context) -> KeyAction {
let mut key_action = KeyAction::new();
key_action.focus_search_input = ctx.input(|i| {
i.modifiers.ctrl && i.key_pressed(egui::Key::F)
});
// Check for Ctrl+Enter to execute search globally
key_action.execute_search = ctx.input(|i| {
i.modifiers.ctrl && i.key_pressed(egui::Key::Enter)
});
key_action
}
impl eframe::App for LogViewerApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
self.handle_first_frame(ctx);
// Update search results if available
self.update_search_results();
// Render top menu
let top_menu_actions = render_top_menu(
ctx,
&mut self.highlight_manager,
&self.indexing_state
);
self.handle_open_file(top_menu_actions.open_file_requested);
// 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);
}
let keyboard_action = handle_key_inputs(ctx);
// 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,
keyboard_action.focus_search_input,
);
if search_actions.execute_search || keyboard_action.focus_search_input {
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
// Show filtered view if there's a query OR if date range is enabled
let show_filtered = !self.search_panel_state.query.is_empty() || self.search_panel_state.date_range_enabled;
let highlight_rules = self.highlight_manager.rules.clone();
let mut export_clicked = false;
if show_filtered {
if let Some(tab) = self.active_tab_mut() {
render_filter_panel(tab, ctx, &highlight_rules, &mut export_clicked);
}
}
// 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
render_main_log_panel(ctx, &highlight_rules, self.active_tab_mut());
// Only request repaint if there are ongoing background operations
if self.indexing_state.is_indexing() || self.search_state.is_searching() {
ctx.request_repaint();
}
}
}

View File

@@ -10,19 +10,12 @@ mod tab_manager;
mod theme;
mod types;
mod ui;
mod log_viewer_app;
use eframe::egui;
use std::sync::Arc;
use crate::log_viewer_app::LogViewerApp;
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 {
@@ -40,462 +33,10 @@ fn main() -> eframe::Result {
"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,
date_range_enabled: config.date_range_enabled,
date_format: config.date_format,
date_from: config.date_from,
date_to: config.date_to,
},
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(),
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.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() {
// Validate date format if date range is enabled
if self.search_panel_state.date_range_enabled {
use chrono::NaiveDateTime;
// Validate format by trying to parse example dates
let test_date = "2025-01-01 00:00:00";
if NaiveDateTime::parse_from_str(test_date, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date format: {}", self.search_panel_state.date_format);
eprintln!("Expected format like: %Y-%m-%d %H:%M:%S");
return;
}
// Validate date_from and date_to can be parsed
if !self.search_panel_state.date_from.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_from, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_from format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
if !self.search_panel_state.date_to.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_to, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_to format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
}
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,
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
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
// Show filtered view if there's a query OR if date range is enabled
let show_filtered = !self.search_panel_state.query.is_empty() || self.search_panel_state.date_range_enabled;
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();
}
}
}

152
src/ui/log_panel.rs Normal file
View File

@@ -0,0 +1,152 @@
use eframe::egui;
use crate::file_tab::FileTab;
use crate::theme;
use crate::theme::ColorPalette;
use crate::types::HighlightRule;
use crate::ui::{render_log_view, LogViewContext};
pub fn render_filter_panel(tab: &mut FileTab, ctx: &egui::Context, highlight_rules: &Vec<HighlightRule>, export_clicked: &mut bool){
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
render_filter_panel_header(ui, &palette, tab, export_clicked);
ui.separator();
render_log_view(
ui,
LogViewContext {
tab,
highlight_rules,
show_all: false,
},
);
});
}
}
pub fn render_main_log_panel(ctx: &egui::Context, highlight_rules: &Vec<HighlightRule>, active_tab: Option<&mut FileTab>) {
egui::CentralPanel::default().show(ctx, |ui| {
let palette = theme::get_palette();
if let Some(tab) = active_tab {
// Styled header
render_file_header(ui, palette, tab);
ui.separator();
render_log_view(
ui,
LogViewContext {
tab,
highlight_rules: &highlight_rules,
show_all: true,
},
);
} else {
render_no_file_opened_view(ui, palette);
}
});
}
fn render_filter_panel_header(ui: &mut egui::Ui, palette: &ColorPalette, tab: &FileTab, export_clicked: &mut bool){
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;
}
});
});
}
fn render_file_header(ui: &mut egui::Ui, palette: ColorPalette, tab: &FileTab) {
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);
});
});
});
}
fn render_no_file_opened_view(ui: &mut egui::Ui, palette: ColorPalette) {
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);
});
});
}

View File

@@ -3,6 +3,7 @@ pub mod log_view;
pub mod search_panel;
pub mod tabs_panel;
pub mod top_menu;
pub mod log_panel;
pub use highlight_editor::render_highlight_editor;
pub use log_view::{render_log_view, LogViewContext};

View File

@@ -3,16 +3,23 @@ use eframe::egui;
use crate::highlight::HighlightManager;
use crate::tab_manager::IndexingState;
pub struct TopMenuActions{
pub open_file_requested: bool,
}
pub fn render_top_menu(
ctx: &egui::Context,
highlight_manager: &mut HighlightManager,
indexing_state: &IndexingState,
on_open_file: &mut bool,
) {
) -> TopMenuActions {
let mut top_menu_actions = TopMenuActions{open_file_requested: false};
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("📂 Open File").clicked() {
*on_open_file = true;
top_menu_actions.open_file_requested = true;
}
if ui.button("🎨 Highlights").clicked() {
@@ -46,4 +53,6 @@ pub fn render_top_menu(
}
});
});
top_menu_actions
}