init, basic glogg clone

This commit is contained in:
2025-12-02 18:30:12 +01:00
parent fb9967ea41
commit e60f2bf319
26 changed files with 6512 additions and 0 deletions

5
.cargo/config.toml Normal file
View File

@@ -0,0 +1,5 @@
# Cargo configuration for cross-compilation
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
ar = "x86_64-w64-mingw32-ar"

108
.github/workflows/build.yml vendored Normal file
View File

@@ -0,0 +1,108 @@
name: Build
on:
push:
branches: [ master, main ]
pull_request:
branches: [ master, main ]
release:
types: [ created ]
env:
CARGO_TERM_COLOR: always
jobs:
build:
name: Build - ${{ matrix.platform.name }}
runs-on: ${{ matrix.platform.os }}
strategy:
fail-fast: false
matrix:
platform:
- name: Linux-x86_64
os: ubuntu-latest
target: x86_64-unknown-linux-gnu
bin: rlogg
command: build
- name: Windows-x86_64
os: windows-latest
target: x86_64-pc-windows-msvc
bin: rlogg.exe
command: build
- name: macOS-x86_64
os: macos-latest
target: x86_64-apple-darwin
bin: rlogg
command: build
- name: macOS-aarch64
os: macos-latest
target: aarch64-apple-darwin
bin: rlogg
command: build
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
targets: ${{ matrix.platform.target }}
- name: Cache cargo registry
uses: actions/cache@v4
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v4
with:
path: ~/.cargo/git
key: ${{ runner.os }}-cargo-git-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo build
uses: actions/cache@v4
with:
path: target
key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Install Linux dependencies
if: matrix.platform.os == 'ubuntu-latest'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
- name: Build
run: cargo ${{ matrix.platform.command }} --release --target ${{ matrix.platform.target }}
- name: Strip binary (Linux and macOS)
if: matrix.platform.os != 'windows-latest'
run: strip target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }}
- name: Create artifact directory
run: mkdir -p artifacts
- name: Copy binary to artifacts
shell: bash
run: |
cp target/${{ matrix.platform.target }}/release/${{ matrix.platform.bin }} artifacts/
- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: rlogg-${{ matrix.platform.name }}
path: artifacts/${{ matrix.platform.bin }}
retention-days: 30
- name: Upload to release
if: github.event_name == 'release'
uses: softprops/action-gh-release@v1
with:
files: artifacts/${{ matrix.platform.bin }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

17
.gitignore vendored Normal file
View File

@@ -0,0 +1,17 @@
# Build artifacts
/target
/dist
# Cargo lock file (optional for binaries)
# Cargo.lock
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db

210
BUILD.md Normal file
View File

@@ -0,0 +1,210 @@
# Building RLogg
This document describes how to build RLogg for different platforms.
## Quick Start
**On Linux, you can build for all platforms including Windows:**
```bash
./cross-compile.sh all
```
This uses Docker-based cross-compilation, so no Windows tools needed!
## Prerequisites
- Rust toolchain (install from https://rustup.rs/)
- Platform-specific dependencies (see below)
- For cross-compilation: Docker (for the `cross` tool)
## Platform-Specific Dependencies
### Linux
```bash
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev \
libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
### macOS
No additional dependencies required. Xcode Command Line Tools should be installed.
### Windows
No additional dependencies required. Visual Studio Build Tools or Visual Studio with C++ development tools should be installed.
## Quick Build
### Development Build
```bash
cargo build
```
### Release Build (Optimized)
```bash
cargo build --release
```
The binary will be in `target/release/rlogg` (or `rlogg.exe` on Windows).
## Multi-Platform Builds
### Using Build Scripts
#### Linux/macOS
```bash
./build-release.sh
```
#### Windows
```powershell
.\build-release.ps1
```
The built binaries will be in the `dist/` directory.
### Manual Cross-Compilation
#### Install Target
```bash
# For Linux
rustup target add x86_64-unknown-linux-gnu
# For Windows
rustup target add x86_64-pc-windows-msvc
# For macOS x86_64
rustup target add x86_64-apple-darwin
# For macOS ARM (M1/M2)
rustup target add aarch64-apple-darwin
```
#### Build for Specific Target
```bash
cargo build --release --target <target-triple>
```
Examples:
```bash
# Linux
cargo build --release --target x86_64-unknown-linux-gnu
# Windows
cargo build --release --target x86_64-pc-windows-msvc
# macOS Intel
cargo build --release --target x86_64-apple-darwin
# macOS Apple Silicon
cargo build --release --target aarch64-apple-darwin
```
## GitHub Actions (Automated Builds)
The project includes GitHub Actions workflow that automatically builds for all platforms:
- Linux x86_64
- Windows x86_64
- macOS x86_64 (Intel)
- macOS aarch64 (Apple Silicon)
### Triggering Builds
- **Push to master/main**: Builds all platforms and uploads artifacts
- **Pull Request**: Runs build checks
- **Release**: Builds and attaches binaries to the GitHub release
### Creating a Release
1. Tag your commit:
```bash
git tag -a v0.1.0 -m "Release v0.1.0"
git push origin v0.1.0
```
2. Create a release on GitHub from the tag
3. GitHub Actions will automatically build and attach binaries
## Optimizing Binary Size
To reduce the binary size, you can use these settings in `Cargo.toml`:
```toml
[profile.release]
opt-level = "z" # Optimize for size
lto = true # Enable Link Time Optimization
codegen-units = 1 # Better optimization
strip = true # Strip symbols
```
Or build with:
```bash
cargo build --release
strip target/release/rlogg # On Linux/macOS
```
## Troubleshooting
### Linux: Missing libraries
Make sure all required development libraries are installed:
```bash
sudo apt-get install -y libgtk-3-dev libxcb-render0-dev libxcb-shape0-dev \
libxcb-xfixes0-dev libxkbcommon-dev libssl-dev
```
### macOS: Code signing issues
If you encounter code signing errors, you can sign the binary:
```bash
codesign --force --deep --sign - target/release/rlogg
```
### Windows: MSVC not found
Install Visual Studio Build Tools with C++ development tools, or install full Visual Studio.
### Cross-compilation from Linux to Windows
#### Method 1: Using the cross-compile script (Recommended)
```bash
# Interactive mode
./cross-compile.sh
# Or specify target directly
./cross-compile.sh windows # Windows (MinGW)
./cross-compile.sh all # All platforms
```
**Note**: Only MinGW/GNU target is supported for cross-compilation from Linux. MSVC requires Windows.
#### Method 2: Manual with 'cross'
Install `cross` and build:
```bash
cargo install cross --git https://github.com/cross-rs/cross
cross build --release --target x86_64-pc-windows-msvc
```
The `cross` tool uses Docker to provide a complete cross-compilation environment, so you don't need to install Windows-specific tools.
## Running Tests
```bash
cargo test
```
## Running in Development Mode
```bash
cargo run
```
## Checking Code
```bash
# Check for errors without building
cargo check
# Run linter
cargo clippy
# Format code
cargo fmt
```

160
CROSS_COMPILE.md Normal file
View File

@@ -0,0 +1,160 @@
# Cross-Compilation Guide
Build Windows binaries from Linux using Docker-based cross-compilation.
## Prerequisites
1. **Install Docker**:
```bash
# Ubuntu/Debian
sudo apt-get install docker.io
sudo usermod -aG docker $USER
# Log out and back in for group changes to take effect
```
2. **Verify Docker**:
```bash
docker --version
```
## Quick Start
### Option 1: Interactive Menu
```bash
./cross-compile.sh
```
Then select:
- `1` - Linux native
- `2` - Windows (MinGW)
- `3` - Both platforms
- `4` - Exit
### Option 2: Command Line
```bash
./cross-compile.sh windows # Windows (MinGW/GNU)
./cross-compile.sh all # All platforms
./cross-compile.sh linux # Linux native
```
### Option 3: Make
```bash
make cross-windows # Build for Windows
make cross-all # Build for all platforms
```
## What Gets Built
Binaries are created in the `dist/` directory:
- `rlogg-linux-x86_64` - Linux binary
- `rlogg-windows-x86_64.exe` - Windows binary (MinGW)
## How It Works
The `cross` tool:
1. Automatically installs on first use
2. Uses Docker to run cross-compilation in containers
3. Provides complete toolchains for each target
4. No need to install Windows SDK or MinGW manually
## About Windows Cross-Compilation
### Why MinGW/GNU Only?
When cross-compiling from Linux to Windows, only the **GNU target** (`x86_64-pc-windows-gnu`) is supported:
- **MinGW (GNU)** - Uses open-source MinGW toolchain
- ✅ Fully supported by `cross` from Linux
- ✅ Works great for most Windows applications
- ✅ Smaller binaries
- ✅ No runtime dependencies on Visual C++ redistributables
- **MSVC** - Microsoft Visual C++ toolchain
- ❌ Not available for cross-compilation from Linux
- ❌ Requires proprietary Microsoft tools
- ❌ Can only be built on Windows or via Windows VM
### Compatibility
The MinGW binaries work on **all Windows systems** (Windows 7+) without requiring additional runtime installations. They're fully compatible with standard Windows applications.
## Troubleshooting
### Docker permission denied
```bash
sudo usermod -aG docker $USER
# Log out and back in
```
### cross installation fails
```bash
# Install from source
cargo install cross --git https://github.com/cross-rs/cross
```
### Build fails with linker errors
The `cross` tool handles all linker configuration automatically. If you see linker errors, try:
```bash
# Clean and rebuild
make clean
./cross-compile.sh windows
```
### Very slow first build
The first build downloads the Docker image (~1-2 GB) and compiles dependencies. Subsequent builds are much faster due to caching.
## Alternative: Manual Cross-Compilation (Advanced)
If you don't want to use Docker, you can manually set up cross-compilation:
### For Windows from Linux
```bash
# Install MinGW
sudo apt-get install mingw-w64
# Add Rust target
rustup target add x86_64-pc-windows-gnu
# Configure cargo
mkdir -p ~/.cargo
cat >> ~/.cargo/config.toml << EOF
[target.x86_64-pc-windows-gnu]
linker = "x86_64-w64-mingw32-gcc"
EOF
# Build
cargo build --release --target x86_64-pc-windows-gnu
```
**Note**: This only works for the GNU target, not MSVC.
## Testing Windows Binaries on Linux
Use Wine to test Windows binaries:
```bash
# Install Wine
sudo apt-get install wine64
# Run the Windows binary
wine dist/rlogg-windows-x86_64-gnu.exe
```
## CI/CD Integration
The GitHub Actions workflow automatically cross-compiles for all platforms. Just push to trigger builds:
```bash
git push origin master
```
Artifacts are available in the Actions tab.
## Performance Considerations
Cross-compilation is:
- **Fast**: Similar speed to native compilation
- **Cached**: Dependencies are cached in Docker volumes
- **Isolated**: Doesn't affect your system
Typical build times:
- First build: 2-5 minutes (includes Docker image download)
- Incremental builds: 30-60 seconds

4406
Cargo.lock generated Normal file
View File

File diff suppressed because it is too large Load Diff

18
Cargo.toml Normal file
View File

@@ -0,0 +1,18 @@
[package]
name = "rlogg"
version = "0.1.0"
edition = "2024"
authors = ["Your Name <your.email@example.com>"]
description = "A fast log file viewer with search, filtering, and highlighting capabilities"
license = "MIT OR Apache-2.0"
repository = "https://github.com/yourusername/rlogg"
readme = "README.md"
keywords = ["log", "viewer", "gui", "search", "filter"]
categories = ["command-line-utilities", "gui"]
[dependencies]
eframe = "0.29"
rfd = "0.15"
regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"

15
Cross.toml Normal file
View File

@@ -0,0 +1,15 @@
# Cross-compilation configuration for the 'cross' tool
# Windows GNU target (MinGW) - fully supported
[target.x86_64-pc-windows-gnu]
# Use edge image with newer GLIBC to avoid build script issues
image = "ghcr.io/cross-rs/x86_64-pc-windows-gnu:edge"
# Ensure build scripts run in the container
pre-build = [
"dpkg --add-architecture i386",
]
# Note: MSVC target is not supported by cross from Linux
# MSVC requires proprietary Microsoft tools that only run on Windows
# Use the GNU target for Windows cross-compilation from Linux

58
Makefile Normal file
View File

@@ -0,0 +1,58 @@
.PHONY: help build release clean test run check fmt clippy install dist cross-windows cross-all
help:
@echo "RLogg - Available targets:"
@echo " make build - Build debug version"
@echo " make release - Build release version"
@echo " make dist - Build release for current platform"
@echo " make cross-windows - Cross-compile for Windows (requires Docker)"
@echo " make cross-all - Cross-compile for all platforms (requires Docker)"
@echo " make run - Run in development mode"
@echo " make test - Run tests"
@echo " make check - Check code without building"
@echo " make fmt - Format code"
@echo " make clippy - Run linter"
@echo " make clean - Clean build artifacts"
@echo " make install - Install binary to ~/.cargo/bin"
build:
cargo build
release:
cargo build --release
@echo "Binary built at: target/release/rlogg"
dist:
@echo "Building release binaries for all platforms..."
./build-release.sh
run:
cargo run
test:
cargo test
check:
cargo check
fmt:
cargo fmt
clippy:
cargo clippy -- -D warnings
clean:
cargo clean
rm -rf dist/
install: release
cargo install --path .
@echo "Installed to ~/.cargo/bin/rlogg"
cross-windows:
@echo "Cross-compiling for Windows..."
./cross-compile.sh windows
cross-all:
@echo "Cross-compiling for all platforms..."
./cross-compile.sh all

70
build-release.ps1 Normal file
View File

@@ -0,0 +1,70 @@
# Build script for creating release binaries for Windows
# PowerShell script for Windows users
$ErrorActionPreference = "Stop"
Write-Host "RLogg - Multi-platform Release Builder (Windows)" -ForegroundColor Cyan
Write-Host "=" * 50 -ForegroundColor Cyan
Write-Host ""
# Create dist directory
$DistDir = "dist"
if (-not (Test-Path $DistDir)) {
New-Item -ItemType Directory -Path $DistDir | Out-Null
}
# Function to build for a target
function Build-Target {
param (
[string]$Target,
[string]$Name
)
Write-Host "Building for $Name ($Target)..." -ForegroundColor Yellow
try {
cargo build --release --target $Target
if ($LASTEXITCODE -eq 0) {
Write-Host "✓ Build successful for $Name" -ForegroundColor Green
# Copy binary to dist directory
if ($Target -like "*windows*") {
$SourcePath = "target\$Target\release\rlogg.exe"
$DestPath = "$DistDir\rlogg-$Name.exe"
} else {
$SourcePath = "target\$Target\release\rlogg"
$DestPath = "$DistDir\rlogg-$Name"
}
Copy-Item $SourcePath $DestPath -Force
Write-Host "✓ Binary copied to $DestPath" -ForegroundColor Green
Write-Host ""
return $true
}
} catch {
Write-Host "✗ Build failed for $Name" -ForegroundColor Red
Write-Host $_.Exception.Message -ForegroundColor Red
Write-Host ""
return $false
}
}
# Build for Windows
Write-Host "=== Building for Windows ===" -ForegroundColor Cyan
$success = Build-Target "x86_64-pc-windows-msvc" "windows-x86_64"
if ($success) {
Write-Host ""
Write-Host "=== Build Complete ===" -ForegroundColor Green
Write-Host "Binaries are in the '$DistDir' directory:" -ForegroundColor Green
Get-ChildItem $DistDir | Format-Table Name, Length, LastWriteTime
Write-Host ""
Write-Host "To build for additional platforms, install the target and run:"
Write-Host " rustup target add <target-triple>"
Write-Host " cargo build --release --target <target-triple>"
} else {
Write-Host ""
Write-Host "Build failed!" -ForegroundColor Red
exit 1
}

87
build-release.sh Executable file
View File

@@ -0,0 +1,87 @@
#!/bin/bash
# Build script for creating release binaries for all platforms
set -e
echo "RLogg - Multi-platform Release Builder"
echo "======================================"
echo ""
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
# Create dist directory
DIST_DIR="dist"
mkdir -p "$DIST_DIR"
# Function to build for a target
build_target() {
local target=$1
local name=$2
echo -e "${YELLOW}Building for $name ($target)...${NC}"
if cargo build --release --target "$target"; then
echo -e "${GREEN}✓ Build successful for $name${NC}"
# Copy binary to dist directory
if [[ "$target" == *"windows"* ]]; then
cp "target/$target/release/rlogg.exe" "$DIST_DIR/rlogg-$name.exe"
echo -e "${GREEN}✓ Binary copied to $DIST_DIR/rlogg-$name.exe${NC}"
else
cp "target/$target/release/rlogg" "$DIST_DIR/rlogg-$name"
# Strip binary on Unix-like systems
strip "$DIST_DIR/rlogg-$name" 2>/dev/null || true
echo -e "${GREEN}✓ Binary copied to $DIST_DIR/rlogg-$name${NC}"
fi
echo ""
return 0
else
echo -e "${RED}✗ Build failed for $name${NC}"
echo ""
return 1
fi
}
# Detect current platform
PLATFORM=$(uname -s)
ARCH=$(uname -m)
echo "Current platform: $PLATFORM ($ARCH)"
echo ""
# Build for current platform first
case "$PLATFORM" in
Linux)
echo "=== Building for Linux ==="
build_target "x86_64-unknown-linux-gnu" "linux-x86_64"
;;
Darwin)
echo "=== Building for macOS ==="
if [[ "$ARCH" == "arm64" ]]; then
build_target "aarch64-apple-darwin" "macos-aarch64"
else
build_target "x86_64-apple-darwin" "macos-x86_64"
fi
;;
MINGW* | MSYS* | CYGWIN*)
echo "=== Building for Windows ==="
build_target "x86_64-pc-windows-msvc" "windows-x86_64"
;;
*)
echo -e "${RED}Unknown platform: $PLATFORM${NC}"
exit 1
;;
esac
echo ""
echo -e "${GREEN}=== Build Complete ===${NC}"
echo "Binaries are in the '$DIST_DIR' directory:"
ls -lh "$DIST_DIR"
echo ""
echo "To build for additional platforms, install the target and run:"
echo " rustup target add <target-triple>"
echo " cargo build --release --target <target-triple>"

152
cross-compile.sh Executable file
View File

@@ -0,0 +1,152 @@
#!/bin/bash
# Cross-compilation script for building Windows binaries on Linux
set -e
echo "RLogg - Cross-Platform Builder (Linux → All Platforms)"
echo "======================================================="
echo ""
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Create dist directory
DIST_DIR="dist"
mkdir -p "$DIST_DIR"
# Check if cross is installed
check_cross() {
if ! command -v cross &> /dev/null; then
echo -e "${YELLOW}Installing 'cross' for cross-compilation...${NC}"
cargo install cross --git https://github.com/cross-rs/cross
else
echo -e "${GREEN}✓ 'cross' is already installed${NC}"
fi
}
# Build using cross
build_with_cross() {
local target=$1
local name=$2
echo ""
echo -e "${BLUE}=== Building for $name ===${NC}"
echo -e "${YELLOW}Target: $target${NC}"
if cross build --release --target "$target"; then
echo -e "${GREEN}✓ Build successful for $name${NC}"
# Copy binary to dist directory
if [[ "$target" == *"windows"* ]]; then
cp "target/$target/release/rlogg.exe" "$DIST_DIR/rlogg-$name.exe"
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-$name.exe${NC}"
else
cp "target/$target/release/rlogg" "$DIST_DIR/rlogg-$name"
strip "$DIST_DIR/rlogg-$name" 2>/dev/null || true
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-$name${NC}"
fi
return 0
else
echo -e "${RED}✗ Build failed for $name${NC}"
return 1
fi
}
# Build using regular cargo (for native Linux)
build_native() {
echo ""
echo -e "${BLUE}=== Building for Linux (native) ===${NC}"
if cargo build --release; then
echo -e "${GREEN}✓ Build successful for Linux${NC}"
cp "target/release/rlogg" "$DIST_DIR/rlogg-linux-x86_64"
strip "$DIST_DIR/rlogg-linux-x86_64" 2>/dev/null || true
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-linux-x86_64${NC}"
return 0
else
echo -e "${RED}✗ Build failed for Linux${NC}"
return 1
fi
}
# Main menu
show_menu() {
echo ""
echo "Select targets to build:"
echo " 1) Linux x86_64 (native)"
echo " 2) Windows x86_64 (cross-compile with MinGW)"
echo " 3) Both Linux and Windows"
echo " 4) Exit"
echo ""
read -p "Enter choice [1-4]: " choice
echo ""
}
# Parse command line arguments
if [ $# -eq 0 ]; then
# Interactive mode
while true; do
show_menu
case $choice in
1)
build_native
;;
2)
check_cross
build_with_cross "x86_64-pc-windows-gnu" "windows-x86_64"
;;
3)
build_native
check_cross
build_with_cross "x86_64-pc-windows-gnu" "windows-x86_64"
;;
4)
echo "Exiting..."
break
;;
*)
echo -e "${RED}Invalid choice${NC}"
;;
esac
done
else
# Command line mode
case "$1" in
linux)
build_native
;;
windows|win)
check_cross
build_with_cross "x86_64-pc-windows-gnu" "windows-x86_64"
;;
all)
build_native
check_cross
build_with_cross "x86_64-pc-windows-gnu" "windows-x86_64"
;;
*)
echo "Usage: $0 [linux|windows|all]"
echo ""
echo " linux - Build for Linux (native)"
echo " windows - Cross-compile for Windows (MinGW)"
echo " all - Build for both platforms"
echo ""
echo "Or run without arguments for interactive mode"
exit 1
;;
esac
fi
echo ""
echo -e "${GREEN}=== Build Complete ===${NC}"
if [ -d "$DIST_DIR" ] && [ "$(ls -A $DIST_DIR)" ]; then
echo "Binaries in '$DIST_DIR':"
ls -lh "$DIST_DIR"
else
echo "No binaries were built"
fi

39
src/config.rs Normal file
View File

@@ -0,0 +1,39 @@
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use crate::types::HighlightRule;
#[derive(Serialize, Deserialize, Default)]
pub struct AppConfig {
pub search_history: Vec<String>,
pub case_sensitive: bool,
pub use_regex: bool,
pub last_search_query: String,
#[serde(default)]
pub highlight_rules: Vec<HighlightRule>,
}
impl AppConfig {
fn config_path() -> PathBuf {
let exe_path = std::env::current_exe().unwrap_or_else(|_| PathBuf::from("."));
let exe_dir = exe_path.parent().unwrap_or_else(|| std::path::Path::new("."));
exe_dir.join("rlogg_config.json")
}
pub fn load() -> Self {
let path = Self::config_path();
if let Ok(contents) = fs::read_to_string(&path) {
serde_json::from_str(&contents).unwrap_or_default()
} else {
Self::default()
}
}
pub fn save(&self) {
let path = Self::config_path();
if let Ok(json) = serde_json::to_string_pretty(self) {
let _ = fs::write(&path, json);
}
}
}

47
src/file_tab.rs Normal file
View File

@@ -0,0 +1,47 @@
use std::fs::File;
use std::io::BufReader;
use std::path::PathBuf;
use std::sync::Arc;
use crate::line_index::LineIndex;
use crate::types::FilteredLine;
pub struct FileTab {
pub file_path: PathBuf,
pub line_index: Arc<LineIndex>,
pub file_handle: BufReader<File>,
pub filtered_lines: Vec<FilteredLine>,
pub selected_line: Option<usize>,
pub main_scroll_offset: usize,
pub filtered_scroll_offset: usize,
pub scroll_to_main: bool,
pub page_scroll_direction: Option<f32>,
pub desired_scroll_offset: f32,
pub force_scroll: bool,
}
impl FileTab {
pub fn new(path: PathBuf, index: LineIndex, file: File) -> Self {
Self {
file_path: path,
line_index: Arc::new(index),
file_handle: BufReader::new(file),
filtered_lines: Vec::new(),
selected_line: None,
main_scroll_offset: 0,
filtered_scroll_offset: 0,
scroll_to_main: false,
page_scroll_direction: None,
desired_scroll_offset: 0.0,
force_scroll: false,
}
}
pub fn filename(&self) -> String {
self.file_path
.file_name()
.and_then(|n| n.to_str())
.unwrap_or("Unknown")
.to_string()
}
}

56
src/highlight.rs Normal file
View File

@@ -0,0 +1,56 @@
use crate::types::HighlightRule;
pub struct HighlightManager {
pub rules: Vec<HighlightRule>,
pub show_editor: bool,
pub new_pattern: String,
pub new_color: [u8; 3],
}
impl HighlightManager {
pub fn new(rules: Vec<HighlightRule>) -> Self {
Self {
rules,
show_editor: false,
new_pattern: String::new(),
new_color: [255, 255, 0],
}
}
pub fn toggle_editor(&mut self) {
self.show_editor = !self.show_editor;
}
pub fn add_highlight(&mut self) -> bool {
if self.new_pattern.is_empty() {
return false;
}
self.rules.push(HighlightRule {
pattern: self.new_pattern.clone(),
color: self.new_color,
enabled: true,
});
self.new_pattern.clear();
true
}
pub fn remove_highlight(&mut self, index: usize) -> bool {
if index < self.rules.len() {
self.rules.remove(index);
true
} else {
false
}
}
pub fn toggle_rule(&mut self, index: usize) -> bool {
if let Some(rule) = self.rules.get_mut(index) {
rule.enabled = !rule.enabled;
true
} else {
false
}
}
}

54
src/line_index.rs Normal file
View File

@@ -0,0 +1,54 @@
use std::fs::File;
use std::io::{BufRead, BufReader, Seek, SeekFrom};
use std::path::PathBuf;
pub struct LineIndex {
pub positions: Vec<u64>,
pub total_lines: usize,
}
impl LineIndex {
pub fn build(file_path: &PathBuf) -> Result<Self, std::io::Error> {
let file = File::open(file_path)?;
let mut reader = BufReader::new(file);
let mut positions = vec![0u64];
let mut line = String::new();
let mut current_pos = 0u64;
loop {
let bytes_read = reader.read_line(&mut line)?;
if bytes_read == 0 {
break;
}
current_pos += bytes_read as u64;
positions.push(current_pos);
line.clear();
}
let total_lines = positions.len().saturating_sub(1);
positions.pop();
Ok(Self {
positions,
total_lines,
})
}
pub fn read_line(&self, file: &mut BufReader<File>, line_num: usize) -> Option<String> {
if line_num >= self.total_lines {
return None;
}
let pos = self.positions[line_num];
if file.seek(SeekFrom::Start(pos)).is_err() {
return None;
}
let mut line = String::new();
if file.read_line(&mut line).is_err() {
return None;
}
Some(line.trim_end_matches(['\r', '\n']).to_string())
}
}

275
src/main.rs Normal file
View File

@@ -0,0 +1,275 @@
mod config;
mod file_tab;
mod highlight;
mod line_index;
mod search;
mod tab_manager;
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| 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_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
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() {
egui::TopBottomPanel::bottom("filtered_view")
.resizable(true)
.default_height(200.0)
.show(ctx, |ui| {
ui.heading("Filtered View");
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
egui::CentralPanel::default().show(ctx, |ui| {
ui.heading("Main Log View");
ui.separator();
if let Some(tab) = self.active_tab_mut() {
render_log_view(
ui,
LogViewContext {
tab,
highlight_rules: &highlight_rules,
show_all: true,
},
);
} else {
ui.centered_and_justified(|ui| {
ui.label("Click 'Open File' to load a log file");
});
}
});
// Only request repaint if there are ongoing background operations
if self.indexing_state.is_indexing() || self.search_state.is_searching() {
ctx.request_repaint();
}
}
}

146
src/search.rs Normal file
View File

@@ -0,0 +1,146 @@
use regex::Regex;
use std::fs::File;
use std::io::BufReader;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::line_index::LineIndex;
use crate::types::FilteredLine;
pub const MAX_SEARCH_HISTORY: usize = 50;
pub struct SearchState {
pub searching: Arc<Mutex<bool>>,
pub progress: Arc<Mutex<f32>>,
pub results: Arc<Mutex<Option<Vec<FilteredLine>>>>,
}
impl SearchState {
pub fn new() -> Self {
Self {
searching: Arc::new(Mutex::new(false)),
progress: Arc::new(Mutex::new(0.0)),
results: Arc::new(Mutex::new(None)),
}
}
pub fn is_searching(&self) -> bool {
*self.searching.lock().unwrap()
}
pub fn get_progress(&self) -> f32 {
*self.progress.lock().unwrap()
}
pub fn take_results(&self) -> Option<Vec<FilteredLine>> {
self.results.lock().unwrap().take()
}
}
pub struct SearchParams {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
}
impl SearchParams {
fn build_regex_matcher(&self) -> Option<Regex> {
if !self.use_regex {
return None;
}
let pattern = if self.case_sensitive {
self.query.clone()
} else {
format!("(?i){}", self.query)
};
Regex::new(&pattern).ok()
}
fn matches_line(&self, content: &str, regex_matcher: &Option<Regex>) -> bool {
if let Some(regex) = regex_matcher {
regex.is_match(content)
} else if self.use_regex {
false
} else if self.case_sensitive {
content.contains(&self.query)
} else {
content.to_lowercase().contains(&self.query.to_lowercase())
}
}
}
pub fn start_search(
search_state: &SearchState,
params: SearchParams,
line_index: Arc<LineIndex>,
file_path: std::path::PathBuf,
) {
if search_state.is_searching() {
return;
}
let searching = Arc::clone(&search_state.searching);
let progress = Arc::clone(&search_state.progress);
let results = Arc::clone(&search_state.results);
*searching.lock().unwrap() = true;
*progress.lock().unwrap() = 0.0;
*results.lock().unwrap() = None;
thread::spawn(move || {
let filtered = search_lines(&params, &line_index, &file_path, &progress);
*results.lock().unwrap() = Some(filtered);
*progress.lock().unwrap() = 1.0;
*searching.lock().unwrap() = false;
});
}
fn search_lines(
params: &SearchParams,
line_index: &LineIndex,
file_path: &std::path::Path,
progress: &Arc<Mutex<f32>>,
) -> Vec<FilteredLine> {
let mut filtered = Vec::new();
let regex_matcher = params.build_regex_matcher();
if let Ok(file) = File::open(file_path) {
let mut file_handle = BufReader::new(file);
let total_lines = line_index.total_lines;
for line_num in 0..total_lines {
if let Some(content) = line_index.read_line(&mut file_handle, line_num) {
if params.matches_line(&content, &regex_matcher) {
filtered.push(FilteredLine {
line_number: line_num,
content,
});
}
}
if line_num % 1000 == 0 {
*progress.lock().unwrap() = line_num as f32 / total_lines as f32;
}
}
}
filtered
}
pub fn add_to_history(history: &mut Vec<String>, query: &str) {
if query.is_empty() {
return;
}
if let Some(pos) = history.iter().position(|x| x == query) {
history.remove(pos);
}
history.insert(0, query.to_string());
if history.len() > MAX_SEARCH_HISTORY {
history.truncate(MAX_SEARCH_HISTORY);
}
}

77
src/tab_manager.rs Normal file
View File

@@ -0,0 +1,77 @@
use std::fs::File;
use std::path::PathBuf;
use std::sync::{Arc, Mutex};
use std::thread;
use crate::file_tab::FileTab;
use crate::line_index::LineIndex;
pub struct IndexingState {
pub indexing: Arc<Mutex<bool>>,
pub progress: Arc<Mutex<f32>>,
}
impl IndexingState {
pub fn new() -> Self {
Self {
indexing: Arc::new(Mutex::new(false)),
progress: Arc::new(Mutex::new(0.0)),
}
}
pub fn is_indexing(&self) -> bool {
*self.indexing.lock().unwrap()
}
fn start_indexing(&self) {
*self.indexing.lock().unwrap() = true;
*self.progress.lock().unwrap() = 0.0;
}
fn finish_indexing(&self) {
*self.indexing.lock().unwrap() = false;
}
}
pub fn open_file_dialog(indexing_state: &IndexingState) -> Option<FileTab> {
let path = rfd::FileDialog::new()
.add_filter("Log Files", &["log", "txt"])
.add_filter("All Files", &["*"])
.pick_file()?;
open_file(path, indexing_state)
}
fn open_file(path: PathBuf, indexing_state: &IndexingState) -> Option<FileTab> {
indexing_state.start_indexing();
// Background indexing for progress indication
let indexing = Arc::clone(&indexing_state.indexing);
let progress = Arc::clone(&indexing_state.progress);
let path_clone = path.clone();
thread::spawn(move || {
if let Ok(_index) = LineIndex::build(&path_clone) {
*progress.lock().unwrap() = 1.0;
}
*indexing.lock().unwrap() = false;
});
// Build index and create tab
let index = LineIndex::build(&path).ok()?;
let file = File::open(&path).ok()?;
let tab = FileTab::new(path, index, file);
indexing_state.finish_indexing();
Some(tab)
}
pub fn close_tab(tabs: &mut Vec<FileTab>, active_index: &mut usize, index: usize) {
if index < tabs.len() {
tabs.remove(index);
if *active_index >= tabs.len() && !tabs.is_empty() {
*active_index = tabs.len() - 1;
}
}
}

14
src/types.rs Normal file
View File

@@ -0,0 +1,14 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Clone)]
pub struct HighlightRule {
pub pattern: String,
pub color: [u8; 3],
pub enabled: bool,
}
#[derive(Clone)]
pub struct FilteredLine {
pub line_number: usize,
pub content: String,
}

View File

@@ -0,0 +1,57 @@
use eframe::egui;
use crate::highlight::HighlightManager;
pub fn render_highlight_editor(ctx: &egui::Context, manager: &mut HighlightManager) -> bool {
if !manager.show_editor {
return false;
}
let mut config_changed = false;
egui::Window::new("Highlight Manager")
.collapsible(false)
.show(ctx, |ui| {
ui.heading("Add New Highlight");
ui.horizontal(|ui| {
ui.label("Pattern:");
ui.text_edit_singleline(&mut manager.new_pattern);
});
ui.horizontal(|ui| {
ui.label("Color:");
ui.color_edit_button_srgb(&mut manager.new_color);
});
if ui.button("Add Highlight").clicked() && manager.add_highlight() {
config_changed = true;
}
ui.separator();
ui.heading("Existing Highlights");
let mut to_delete = None;
for (idx, rule) in manager.rules.iter_mut().enumerate() {
ui.horizontal(|ui| {
if ui.checkbox(&mut rule.enabled, "").changed() {
config_changed = true;
}
ui.label(&rule.pattern);
if ui.color_edit_button_srgb(&mut rule.color).changed() {
config_changed = true;
}
if ui.button("🗑").clicked() {
to_delete = Some(idx);
}
});
}
if let Some(idx) = to_delete {
manager.remove_highlight(idx);
config_changed = true;
}
});
config_changed
}

241
src/ui/log_view.rs Normal file
View File

@@ -0,0 +1,241 @@
use eframe::egui;
use crate::file_tab::FileTab;
use crate::types::HighlightRule;
pub struct LogViewContext<'a> {
pub tab: &'a mut FileTab,
pub highlight_rules: &'a [HighlightRule],
pub show_all: bool,
}
pub fn render_log_view(ui: &mut egui::Ui, ctx: LogViewContext) {
let LogViewContext {
tab,
highlight_rules,
show_all,
} = ctx;
let total_lines = if show_all {
tab.line_index.total_lines
} else {
tab.filtered_lines.len()
};
if total_lines == 0 {
ui.centered_and_justified(|ui| {
if show_all {
ui.label("Click 'Open File' to load a log file");
} else {
ui.label("No matching lines");
}
});
return;
}
let line_height = ui.text_style_height(&egui::TextStyle::Monospace);
// Account for: frame inner margins (2.0 vertical) + horizontal layout spacing + frame outer spacing
// The actual rendered height is approximately line_height + 6.0
let row_height = line_height + 2.0;
let scroll_id_str = if show_all {
"main_view_scroll"
} else {
"filtered_view_scroll"
};
let scroll_id = egui::Id::new(scroll_id_str);
// Only handle scroll-to-line and page-scroll for the main view
if show_all {
handle_scroll_to_line(ui, &scroll_id, tab, row_height);
handle_page_scroll(ui, &scroll_id, tab, row_height, total_lines);
}
let mut scroll_area = egui::ScrollArea::both()
.auto_shrink([false; 2])
.id_salt(scroll_id);
if show_all && tab.force_scroll {
scroll_area = scroll_area.vertical_scroll_offset(tab.desired_scroll_offset);
tab.force_scroll = false;
}
let scroll_output = scroll_area.show_rows(ui, row_height, total_lines, |ui, row_range| {
render_visible_lines(ui, tab, highlight_rules, show_all, row_range);
});
// Update the scroll offset from the actual scroll state
if show_all {
let actual_offset = scroll_output.state.offset.y;
// eprintln!("Actual scroll offset after render: {}", actual_offset);
if (actual_offset - tab.desired_scroll_offset).abs() > 1.0 {
eprintln!(
"SYNC: Updating desired_scroll_offset from {} to {}",
tab.desired_scroll_offset, actual_offset
);
}
tab.desired_scroll_offset = actual_offset;
}
}
fn handle_scroll_to_line(
_ui: &mut egui::Ui,
_scroll_id: &egui::Id,
tab: &mut FileTab,
row_height: f32,
) {
if tab.scroll_to_main {
let target_row = tab.main_scroll_offset;
let adgusted_row_height = row_height + 3f32;
let scroll_offset = (target_row as f32) * adgusted_row_height;
eprintln!("=== SCROLL TO LINE ===");
eprintln!(" main_scroll_offset (selected line): {}", tab.main_scroll_offset);
eprintln!(" target_row (with -5 context): {}", target_row);
eprintln!(" row_height: {}", row_height);
eprintln!(" calculated scroll_offset: {}", scroll_offset);
eprintln!(" force_scroll: true");
tab.desired_scroll_offset = scroll_offset;
tab.force_scroll = true;
tab.scroll_to_main = false;
}
}
fn handle_page_scroll(
ui: &mut egui::Ui,
_scroll_id: &egui::Id,
tab: &mut FileTab,
row_height: f32,
total_lines: usize,
) {
if let Some(direction) = tab.page_scroll_direction.take() {
let viewport_height = ui.available_height();
let rows_per_page = (viewport_height / row_height).floor().max(1.0);
let scroll_delta = direction * rows_per_page * row_height;
let max_offset = (total_lines as f32 * row_height - viewport_height).max(0.0);
let new_offset = (tab.desired_scroll_offset + scroll_delta).clamp(0.0, max_offset);
eprintln!(
"Page scroll: current_offset={}, scroll_delta={}, new_offset={}",
tab.desired_scroll_offset, scroll_delta, new_offset
);
tab.desired_scroll_offset = new_offset;
tab.force_scroll = true;
}
}
fn render_visible_lines(
ui: &mut egui::Ui,
tab: &mut FileTab,
highlight_rules: &[HighlightRule],
show_all: bool,
row_range: std::ops::Range<usize>,
) {
ui.style_mut().spacing.item_spacing.y = 0.0;
for display_idx in row_range {
let (line_num, content) = get_line_content(tab, show_all, display_idx);
let is_selected = tab.selected_line == Some(line_num);
render_line(
ui,
&content,
line_num,
is_selected,
highlight_rules,
|clicked| {
if clicked {
tab.selected_line = Some(line_num);
if !show_all {
tab.main_scroll_offset = line_num;
tab.scroll_to_main = true;
}
}
},
);
}
}
fn get_line_content(tab: &mut FileTab, show_all: bool, display_idx: usize) -> (usize, String) {
if show_all {
let content = tab
.line_index
.read_line(&mut tab.file_handle, display_idx)
.unwrap_or_default();
(display_idx, content)
} else {
if display_idx < tab.filtered_lines.len() {
let filtered = &tab.filtered_lines[display_idx];
(filtered.line_number, filtered.content.clone())
} else {
(0, String::new())
}
}
}
fn render_line<F>(
ui: &mut egui::Ui,
content: &str,
line_num: usize,
is_selected: bool,
highlight_rules: &[HighlightRule],
on_click: F,
) where
F: FnOnce(bool),
{
let highlight_color = highlight_rules
.iter()
.find(|rule| rule.enabled && content.contains(&rule.pattern))
.map(|rule| egui::Color32::from_rgb(rule.color[0], rule.color[1], rule.color[2]));
let bg_color = if is_selected {
egui::Color32::from_rgb(70, 130, 180)
} else if let Some(color) = highlight_color {
color
} else {
egui::Color32::TRANSPARENT
};
let frame = egui::Frame::none()
.fill(bg_color)
.inner_margin(egui::Margin::symmetric(2.0, 1.0));
frame.show(ui, |ui| {
ui.horizontal(|ui| {
let line_num_text = egui::RichText::new(format!("{:6} ", line_num + 1))
.monospace()
.color(if is_selected {
egui::Color32::WHITE
} else {
egui::Color32::DARK_GRAY
});
let line_num_response = ui.label(line_num_text);
let text = egui::RichText::new(content).monospace().color(
if is_selected {
egui::Color32::WHITE
} else {
ui.style().visuals.text_color()
},
);
let text_response = ui
.scope(|ui| {
ui.style_mut().visuals.selection.bg_fill =
egui::Color32::from_rgb(255, 180, 50);
ui.style_mut().visuals.selection.stroke.color =
egui::Color32::from_rgb(200, 140, 30);
ui.add(egui::Label::new(text).selectable(true))
})
.inner;
let clicked =
line_num_response.clicked() || (text_response.clicked() && !text_response.has_focus());
on_click(clicked);
})
});
}

11
src/ui/mod.rs Normal file
View File

@@ -0,0 +1,11 @@
pub mod highlight_editor;
pub mod log_view;
pub mod search_panel;
pub mod tabs_panel;
pub mod top_menu;
pub use highlight_editor::render_highlight_editor;
pub use log_view::{render_log_view, LogViewContext};
pub use search_panel::{render_search_panel, SearchPanelState};
pub use tabs_panel::render_tabs_panel;
pub use top_menu::render_top_menu;

99
src/ui/search_panel.rs Normal file
View File

@@ -0,0 +1,99 @@
use eframe::egui;
use crate::search::SearchState;
pub struct SearchPanelState {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
pub history: Vec<String>,
}
pub struct SearchPanelActions {
pub execute_search: bool,
pub clear_search: bool,
pub config_changed: bool,
}
pub fn render_search_panel(
ctx: &egui::Context,
state: &mut SearchPanelState,
search_state: &SearchState,
match_count: usize,
) -> SearchPanelActions {
let mut actions = SearchPanelActions {
execute_search: false,
clear_search: false,
config_changed: false,
};
egui::TopBottomPanel::bottom("search_panel").show(ctx, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("🔍 Filter:");
let text_edit_width = 200.0;
let text_response = ui.add_sized(
[text_edit_width, 20.0],
egui::TextEdit::singleline(&mut state.query),
);
let enter_pressed =
text_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
if !state.history.is_empty() {
render_history_dropdown(ui, state);
}
let case_changed = ui
.checkbox(&mut state.case_sensitive, "Case sensitive")
.changed();
let regex_changed = ui.checkbox(&mut state.use_regex, "Regex").changed();
if case_changed || regex_changed {
actions.config_changed = true;
}
if ui.button("Search").clicked() || enter_pressed {
actions.execute_search = true;
actions.config_changed = true;
}
if !state.query.is_empty() {
if ui.button("✖ Clear").clicked() {
actions.clear_search = true;
}
ui.label(format!("({} matches)", match_count));
}
});
if search_state.is_searching() {
let progress = search_state.get_progress();
ui.horizontal(|ui| {
ui.add(
egui::ProgressBar::new(progress)
.text(format!("Searching... {:.0}%", progress * 100.0))
.animate(true),
);
});
}
});
});
actions
}
fn render_history_dropdown(ui: &mut egui::Ui, state: &mut SearchPanelState) {
egui::ComboBox::from_id_salt("search_history_dropdown")
.selected_text("")
.width(30.0)
.show_ui(ui, |ui| {
ui.set_min_width(300.0);
for history_item in &state.history.clone() {
if ui.selectable_label(false, history_item).clicked() {
state.query = history_item.clone();
}
}
});
}

41
src/ui/tabs_panel.rs Normal file
View File

@@ -0,0 +1,41 @@
use eframe::egui;
use crate::file_tab::FileTab;
pub fn render_tabs_panel(
ctx: &egui::Context,
tabs: &[FileTab],
active_tab_index: &mut usize,
on_close_tab: &mut Option<usize>,
) {
if tabs.is_empty() {
return;
}
egui::TopBottomPanel::top("tabs_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
for (idx, tab) in tabs.iter().enumerate() {
let is_active = idx == *active_tab_index;
let button_text = if is_active {
egui::RichText::new(format!("📄 {}", tab.filename())).strong()
} else {
egui::RichText::new(format!("📄 {}", tab.filename()))
};
if ui.selectable_label(is_active, button_text).clicked() {
*active_tab_index = idx;
}
if ui.small_button("").clicked() {
*on_close_tab = Some(idx);
}
ui.separator();
}
if let Some(tab) = tabs.get(*active_tab_index) {
ui.label(format!("({} lines)", tab.line_index.total_lines));
}
});
});
}

49
src/ui/top_menu.rs Normal file
View File

@@ -0,0 +1,49 @@
use eframe::egui;
use crate::highlight::HighlightManager;
use crate::tab_manager::IndexingState;
pub fn render_top_menu(
ctx: &egui::Context,
highlight_manager: &mut HighlightManager,
indexing_state: &IndexingState,
on_open_file: &mut bool,
) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("📂 Open File").clicked() {
*on_open_file = true;
}
if ui.button("🎨 Highlights").clicked() {
highlight_manager.toggle_editor();
}
ui.separator();
let mut to_toggle = None;
for (idx, rule) in highlight_manager.rules.iter().enumerate() {
let color = egui::Color32::from_rgb(rule.color[0], rule.color[1], rule.color[2]);
let button_text = egui::RichText::new(&rule.pattern)
.background_color(color)
.color(egui::Color32::BLACK);
if ui.selectable_label(rule.enabled, button_text).clicked() {
to_toggle = Some(idx);
}
}
if let Some(idx) = to_toggle {
highlight_manager.toggle_rule(idx);
// Signal that config should be saved
}
ui.separator();
if indexing_state.is_indexing() {
ui.spinner();
ui.label("Indexing...");
}
});
});
}