Compare commits

..
5 Commits
Author SHA1 Message Date
stas 52d1df4c2f build fixes 2025-12-02 19:25:19 +01:00
stas bc197d4080 fix pageUp/pageDown scrolling 2025-12-02 19:03:40 +01:00
stas f145ac521e fix windows console 2025-12-02 19:03:23 +01:00
stas 0dd5962b9d goddamn it is fast now 2025-12-02 18:41:12 +01:00
stas a53ced9160 init, basic glogg clone 2025-12-02 18:30:12 +01:00
31 changed files with 826 additions and 2456 deletions
-92
View File
@@ -1,92 +0,0 @@
name: Build
on:
push:
branches:
- main
workflow_dispatch:
jobs:
build:
name: Build - ${{ matrix.platform }}
runs-on: gitea-runner
container:
image: ${{ matrix.docker_image }}
volumes:
- /tmp/gitea-sccache:/tmp/sccache
strategy:
matrix:
include:
- platform: linux
docker_image: gitea.staspast.click/stas/rust-node-builder:v5
target: x86_64-unknown-linux-gnu
artifact_name: rlogg-linux-x86_64
binary_extension: ""
setup_cmd: ""
- platform: windows
docker_image: gitea.staspast.click/stas/rust-node-builder:v5
target: x86_64-pc-windows-gnu
artifact_name: rlogg-windows-x86_64
binary_extension: ".exe"
setup_cmd: ""
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Configure sccache
run: |
# Configure sccache cache directory (persists on runner between builds)
mkdir -p /tmp/sccache
echo "SCCACHE_DIR=/tmp/sccache" >> $GITHUB_ENV
echo "RUSTC_WRAPPER=sccache" >> $GITHUB_ENV
- name: Cache dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-${{ matrix.target }}-cargo-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
${{ runner.os }}-${{ matrix.target }}-cargo-
- name: Setup cross-compilation tools
if: matrix.setup_cmd != ''
run: ${{ matrix.setup_cmd }}
- name: Add Rust target
run: rustup target add ${{ matrix.target }}
- name: Build release binary
run: |
if [ "${{ matrix.platform }}" = "macos" ]; then
cargo zigbuild --release --target ${{ matrix.target }}
elif [ "${{ matrix.platform }}" = "linux" ]; then
cargo build --release
else
# Cross-compile for other targets (e.g., Windows)
cargo build --release --target ${{ matrix.target }}
fi
# Show sccache statistics
echo "=== sccache stats ==="
sccache --show-stats
- name: Prepare artifact
shell: sh
run: |
mkdir -p dist
if [ "${{ matrix.platform }}" = "linux" ]; then
cp target/release/rlogg${{ matrix.binary_extension }} dist/${{ matrix.artifact_name }}${{ matrix.binary_extension }}
else
cp target/${{ matrix.target }}/release/rlogg${{ matrix.binary_extension }} dist/${{ matrix.artifact_name }}${{ matrix.binary_extension }}
fi
- name: Upload artifact
uses: actions/upload-artifact@v3
with:
name: ${{ matrix.artifact_name }}
path: dist/${{ matrix.artifact_name }}${{ matrix.binary_extension }}
retention-days: 14
+160
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
Generated
+1 -223
View File
@@ -172,56 +172,6 @@ dependencies = [
"libc",
]
[[package]]
name = "anstream"
version = "0.6.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a"
dependencies = [
"anstyle",
"anstyle-parse",
"anstyle-query",
"anstyle-wincon",
"colorchoice",
"is_terminal_polyfill",
"utf8parse",
]
[[package]]
name = "anstyle"
version = "1.0.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78"
[[package]]
name = "anstyle-parse"
version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
dependencies = [
"utf8parse",
]
[[package]]
name = "anstyle-query"
version = "1.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
dependencies = [
"windows-sys 0.61.2",
]
[[package]]
name = "anstyle-wincon"
version = "3.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
dependencies = [
"anstyle",
"once_cell_polyfill",
"windows-sys 0.61.2",
]
[[package]]
name = "arboard"
version = "3.6.1"
@@ -711,59 +661,6 @@ dependencies = [
"libc",
]
[[package]]
name = "chrono"
version = "0.4.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "145052bdd345b87320e369255277e3fb5152762ad123a901ef5c262dd38fe8d2"
dependencies = [
"iana-time-zone",
"js-sys",
"num-traits",
"wasm-bindgen",
"windows-link",
]
[[package]]
name = "clap"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e6ff9dcd79cff5cd969a17a545d79e84ab086e444102a591e288a8aa3ce394"
dependencies = [
"clap_builder",
"clap_derive",
]
[[package]]
name = "clap_builder"
version = "4.5.54"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa42cf4d2b7a41bc8f663a7cab4031ebafa1bf3875705bfaf8466dc60ab52c00"
dependencies = [
"anstream",
"anstyle",
"clap_lex",
"strsim",
]
[[package]]
name = "clap_derive"
version = "4.5.49"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a0b5487afeab2deb2ff4e03a807ad1a03ac532ff5a2cee5d86884440c7f7671"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn 2.0.111",
]
[[package]]
name = "clap_lex"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
[[package]]
name = "clipboard-win"
version = "5.4.1"
@@ -783,12 +680,6 @@ dependencies = [
"unicode-width",
]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]]
name = "com"
version = "0.6.0"
@@ -1611,12 +1502,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "hermit-abi"
version = "0.5.2"
@@ -1635,30 +1520,6 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df"
[[package]]
name = "iana-time-zone"
version = "0.1.64"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33e57f83510bb73707521ebaffa789ec8caf86f9657cad665b092b581d40e9fb"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"log",
"wasm-bindgen",
"windows-core 0.58.0",
]
[[package]]
name = "iana-time-zone-haiku"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
dependencies = [
"cc",
]
[[package]]
name = "icu_collections"
version = "2.1.1"
@@ -1793,38 +1654,6 @@ dependencies = [
"hashbrown 0.16.1",
]
[[package]]
name = "interprocess"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f2533f3be42fffe3b5e63b71aeca416c1c3bc33e4e27be018521e76b1f38fb"
dependencies = [
"blocking",
"cfg-if",
"futures-core",
"futures-io",
"intmap",
"libc",
"once_cell",
"rustc_version",
"spinning",
"thiserror 1.0.69",
"to_method",
"winapi",
]
[[package]]
name = "intmap"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae52f28f45ac2bc96edb7714de995cffc174a395fb0abf5bff453587c980d7b9"
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
[[package]]
name = "itoa"
version = "1.0.15"
@@ -2434,12 +2263,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]]
name = "orbclient"
version = "0.3.49"
@@ -2846,12 +2669,9 @@ dependencies = [
[[package]]
name = "rlogg"
version = "0.4.1"
version = "0.1.0"
dependencies = [
"chrono",
"clap",
"eframe",
"interprocess",
"rayon",
"regex",
"rfd",
@@ -2871,15 +2691,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "rustix"
version = "0.38.44"
@@ -2952,12 +2763,6 @@ dependencies = [
"tiny-skia",
]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
@@ -3137,15 +2942,6 @@ dependencies = [
"serde",
]
[[package]]
name = "spinning"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b"
dependencies = [
"lock_api",
]
[[package]]
name = "spirv"
version = "0.3.0+sdk-1.3.268.0"
@@ -3173,12 +2969,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]]
name = "syn"
version = "1.0.109"
@@ -3309,12 +3099,6 @@ dependencies = [
"zerovec",
]
[[package]]
name = "to_method"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
[[package]]
name = "toml_datetime"
version = "0.7.3"
@@ -3457,12 +3241,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
version = "1.18.1"
+1 -7
View File
@@ -1,6 +1,6 @@
[package]
name = "rlogg"
version = "0.4.1"
version = "0.1.0"
edition = "2024"
authors = ["Stanislav Pastushenko <staspast1@gmail.com>"]
description = "A fast log file viewer with search, filtering, and highlighting capabilities"
@@ -17,9 +17,3 @@ regex = "1.11"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
rayon = "1.10"
chrono = "0.4"
clap = { version = "4.5", features = ["derive"] }
interprocess = "1.2.1"
[profile.release]
debug = false
-18
View File
@@ -1,18 +0,0 @@
MIT License
Copyright (c) 2025 stas
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and
associated documentation files (the "Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO
EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
USE OR OTHER DEALINGS IN THE SOFTWARE.
-247
View File
@@ -1,247 +0,0 @@
# RLogg - Fast Log File Viewer
A fast, native log file viewer with search, filtering, and highlighting capabilities built with Rust and egui.
## Features
- **Fast Performance**: Handle large log files (GB+) with efficient line indexing
- **Advanced Search**: Regex support, case-sensitive/insensitive search, date range filtering
- **Syntax Highlighting**: Custom highlight rules with regex patterns
- **Multiple Tabs**: Open and view multiple log files simultaneously
- **Export Results**: Export filtered/searched results to new files
- **Cross-Platform**: Native builds for Linux, Windows, and macOS
- **Command-Line Interface**: Open files directly from terminal or OS file associations
## Installation
### From Source
1. **Clone the repository:**
```bash
git clone https://github.com/yourusername/rlogg.git
cd rlogg
```
2. **Build in release mode:**
```bash
cargo build --release
```
3. **The binary will be available at:**
```
target/release/rlogg
```
### Linux
#### Quick Install (User-Local)
```bash
cargo build --release
cd packaging/linux
./install.sh
```
This will:
- Install `rlogg` to `~/.local/bin/`
- Set up file associations for `.log` files
- Configure desktop integration
Make sure `~/.local/bin` is in your PATH:
```bash
export PATH="$HOME/.local/bin:$PATH"
```
#### System-Wide Install
```bash
sudo cp target/release/rlogg /usr/local/bin/
sudo cp packaging/linux/rlogg.desktop /usr/share/applications/
sudo cp packaging/linux/text-x-log.xml /usr/share/mime/packages/
sudo update-desktop-database /usr/share/applications
sudo update-mime-database /usr/share/mime
```
### Windows
1. **Build the project:**
```powershell
cargo build --release
```
2. **Run the installation script:**
```powershell
cd packaging\windows
powershell -ExecutionPolicy Bypass -File install.ps1
```
This will:
- Install RLogg to `%LOCALAPPDATA%\Programs\RLogg`
- Create file associations for `.log` files
- Add "Open with RLogg" to the context menu
## Usage
### Graphical Interface
**Launch without arguments:**
```bash
rlogg
```
Then use the "Open File" button to select log files.
### Command-Line Interface
**Open a single file:**
```bash
rlogg /path/to/file.log
```
**Open multiple files (each in a separate tab):**
```bash
rlogg file1.log file2.log file3.log
```
**Open all log files in a directory:**
```bash
rlogg /var/log/*.log
```
**Get help:**
```bash
rlogg --help
```
**Check version:**
```bash
rlogg --version
```
### File Associations
After installation, you can:
- **Double-click** `.log` files to open them in RLogg
- **Right-click** → "Open With" → RLogg
- **Drag and drop** multiple log files onto the RLogg icon
See [docs/FILE_ASSOCIATIONS.md](docs/FILE_ASSOCIATIONS.md) for detailed setup and troubleshooting.
## Features Guide
### Search
1. Enter search query in the search panel
2. Choose case-sensitive or regex mode
3. Optionally enable date range filtering
4. Results are highlighted and filtered in real-time
### Highlighting
1. Click "Highlight Rules" to open the editor
2. Add custom regex patterns with colors
3. Rules are applied to all open files
4. Perfect for highlighting errors, warnings, etc.
### Tabs
- Open multiple files simultaneously
- Each file maintains its own search/filter state
- Close tabs with the × button
- Switch between tabs with mouse or keyboard
### Export
- Export filtered/searched results to a new file
- Preserves line formatting
- Useful for extracting specific log entries
## Building
### Prerequisites
- Rust 1.70 or later
- Cargo
### Build Commands
**Development build:**
```bash
cargo build
```
**Release build (optimized):**
```bash
cargo build --release
```
**Run directly:**
```bash
cargo run -- /path/to/file.log
```
**Run tests:**
```bash
cargo test
```
See [BUILD.md](BUILD.md) for detailed build instructions.
## Packaging
See [packaging/README.md](packaging/README.md) for instructions on creating distribution packages:
- Linux: `.deb`, `.rpm`, AppImage, tarballs
- Windows: Installers, ZIP archives
## Configuration
RLogg stores configuration in `rlogg_config.json` next to the executable:
- Search history
- Highlight rules
- UI preferences
- Date format settings
## Troubleshooting
### Linux
**File associations not working:**
```bash
update-desktop-database ~/.local/share/applications
update-mime-database ~/.local/share/mime
```
**Binary not found:**
Ensure `~/.local/bin` is in your PATH.
### Windows
**File associations not working:**
1. Restart File Explorer
2. Check registry entries (see [docs/FILE_ASSOCIATIONS.md](docs/FILE_ASSOCIATIONS.md))
**Permission errors:**
Run PowerShell as Administrator or install to user directory.
For more troubleshooting, see [docs/FILE_ASSOCIATIONS.md](docs/FILE_ASSOCIATIONS.md).
## Contributing
Contributions are welcome! Please:
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Run tests and formatting
5. Submit a pull request
## License
MIT OR Apache-2.0
## Acknowledgments
Built with:
- [eframe](https://github.com/emilk/egui/tree/master/crates/eframe) - GUI framework
- [egui](https://github.com/emilk/egui) - Immediate mode GUI library
- [rfd](https://github.com/PolyMeilex/rfd) - Native file dialogs
- [clap](https://github.com/clap-rs/clap) - Command-line argument parsing
+70
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
}
+92
View File
@@ -0,0 +1,92 @@
#!/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
# Extract version from Cargo.toml
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "Version: $VERSION"
echo ""
# 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 with version
if [[ "$target" == *"windows"* ]]; then
cp "target/$target/release/rlogg.exe" "$DIST_DIR/rlogg-$VERSION-$name.exe"
echo -e "${GREEN}✓ Binary copied to $DIST_DIR/rlogg-$VERSION-$name.exe${NC}"
else
cp "target/$target/release/rlogg" "$DIST_DIR/rlogg-$VERSION-$name"
# Strip binary on Unix-like systems
strip "$DIST_DIR/rlogg-$VERSION-$name" 2>/dev/null || true
echo -e "${GREEN}✓ Binary copied to $DIST_DIR/rlogg-$VERSION-$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>"
-82
View File
@@ -1,82 +0,0 @@
#!/bin/bash
# Unified build script for RLogg
# Usage: ./build.sh [linux|windows]
# If no argument is provided, builds for both Linux and Windows.
set -e
# Configuration
DIST_DIR="dist"
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
LINUX_TARGET="x86_64-unknown-linux-gnu"
WINDOWS_TARGET="x86_64-pc-windows-gnu"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
echo "RLogg Builder v$VERSION"
echo "======================"
mkdir -p "$DIST_DIR"
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
fi
}
build_linux() {
echo -e "${BLUE}=== Building for Linux ($LINUX_TARGET) ===${NC}"
if cargo build --release --target "$LINUX_TARGET"; then
cp "target/$LINUX_TARGET/release/rlogg" "$DIST_DIR/rlogg-$VERSION-linux-x86_64"
strip "$DIST_DIR/rlogg-$VERSION-linux-x86_64" 2>/dev/null || true
echo -e "${GREEN}✓ Linux build success: $DIST_DIR/rlogg-$VERSION-linux-x86_64${NC}"
else
echo -e "${RED}✗ Linux build failed${NC}"
return 1
fi
}
build_windows() {
echo -e "${BLUE}=== Building for Windows ($WINDOWS_TARGET) ===${NC}"
check_cross
# Clean specific target to avoid conflicts
# cargo clean --release --target "$WINDOWS_TARGET" 2>/dev/null || true
if cross build --release --target "$WINDOWS_TARGET"; then
cp "target/$WINDOWS_TARGET/release/rlogg.exe" "$DIST_DIR/rlogg-$VERSION-windows-x86_64.exe"
echo -e "${GREEN}✓ Windows build success: $DIST_DIR/rlogg-$VERSION-windows-x86_64.exe${NC}"
else
echo -e "${RED}✗ Windows build failed${NC}"
return 1
fi
}
# Main logic
if [ $# -eq 0 ]; then
echo "No target specified, building for ALL platforms..."
build_linux
build_windows
else
case "$1" in
linux)
build_linux
;;
windows|win)
build_windows
;;
*)
echo "Usage: $0 [linux|windows]"
exit 1
;;
esac
fi
echo ""
echo -e "${GREEN}Build process completed.${NC}"
ls -lh "$DIST_DIR"
+161
View File
@@ -0,0 +1,161 @@
#!/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
# Extract version from Cargo.toml
VERSION=$(grep -m1 '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "Version: $VERSION"
echo ""
# 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}"
# Clean build artifacts to avoid GLIBC mismatch with build scripts
echo -e "${YELLOW}Cleaning build artifacts...${NC}"
cargo clean --release --target "$target"
if cross build --release --target "$target"; then
echo -e "${GREEN}✓ Build successful for $name${NC}"
# Copy binary to dist directory with version
if [[ "$target" == *"windows"* ]]; then
cp "target/$target/release/rlogg.exe" "$DIST_DIR/rlogg-$VERSION-$name.exe"
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-$VERSION-$name.exe${NC}"
else
cp "target/$target/release/rlogg" "$DIST_DIR/rlogg-$VERSION-$name"
strip "$DIST_DIR/rlogg-$VERSION-$name" 2>/dev/null || true
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-$VERSION-$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-$VERSION-linux-x86_64"
strip "$DIST_DIR/rlogg-$VERSION-linux-x86_64" 2>/dev/null || true
echo -e "${GREEN}✓ Binary: $DIST_DIR/rlogg-$VERSION-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
-1
View File
@@ -1 +0,0 @@
docker build -t gitea.staspast.click/stas/rust-node-builder:v5 -f linux-build.Dockerfile . && docker push gitea.staspast.click/stas/rust-node-builder:v5
-26
View File
@@ -1,26 +0,0 @@
FROM rust:latest
# Install system dependencies and tools
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nodejs \
npm \
tar \
gzip \
curl \
mingw-w64 \
&& rm -rf /var/lib/apt/lists/* && \
# Check if /bin/tar is BusyBox and replace it with GNU tar
if /bin/tar --version 2>&1 | grep -q "BusyBox"; then \
cp /usr/bin/tar /bin/tar.gnu && \
rm -f /bin/tar && \
mv /bin/tar.gnu /bin/tar; \
fi
# Install sccache for Rust compilation caching
RUN SCCACHE_VERSION=0.7.4 && \
SCCACHE_URL="https://github.com/mozilla/sccache/releases/download/v${SCCACHE_VERSION}/sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl.tar.gz" && \
curl -L "$SCCACHE_URL" | tar xz && \
chmod +x sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache && \
mv sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl/sccache /usr/local/bin/ && \
rm -rf sccache-v${SCCACHE_VERSION}-x86_64-unknown-linux-musl
-301
View File
@@ -1,301 +0,0 @@
# Packaging Guide
This directory contains packaging files and installation scripts for RLogg on different platforms.
## Directory Structure
```
packaging/
├── linux/
│ ├── rlogg.desktop # XDG desktop entry file
│ ├── text-x-log.xml # MIME type definition for .log files
│ └── install.sh # Linux installation script
├── windows/
│ ├── file-association.reg # Windows registry file for manual setup
│ └── install.ps1 # PowerShell installation script
└── README.md # This file
```
## Building for Distribution
### Linux
1. **Build the release binary:**
```bash
cargo build --release
```
2. **Test the installation script:**
```bash
cd packaging/linux
./install.sh
```
3. **Create a distribution package:**
```bash
# Create a tarball
cd target/release
tar -czf rlogg-linux-x64.tar.gz rlogg ../../packaging/linux/*
```
### Windows
1. **Build the release binary:**
```powershell
cargo build --release
```
2. **Test the installation script:**
```powershell
cd packaging\windows
powershell -ExecutionPolicy Bypass -File install.ps1
```
3. **Create a distribution package:**
```powershell
# Create a ZIP file
Compress-Archive -Path target\release\rlogg.exe, packaging\windows\* -DestinationPath rlogg-windows-x64.zip
```
## Distribution Packages
### Linux Packages
#### DEB Package (Debian/Ubuntu)
Create a `debian/` directory structure for building `.deb` packages:
```bash
mkdir -p debian/usr/local/bin
mkdir -p debian/usr/share/applications
mkdir -p debian/usr/share/mime/packages
cp target/release/rlogg debian/usr/local/bin/
cp packaging/linux/rlogg.desktop debian/usr/share/applications/
cp packaging/linux/text-x-log.xml debian/usr/share/mime/packages/
# Create DEBIAN control file
mkdir -p debian/DEBIAN
cat > debian/DEBIAN/control <<EOF
Package: rlogg
Version: 0.3.1
Section: utils
Priority: optional
Architecture: amd64
Maintainer: Your Name <your.email@example.com>
Description: Fast log file viewer
A fast log file viewer with search, filtering, and highlighting capabilities
EOF
# Build the package
dpkg-deb --build debian rlogg_0.3.1_amd64.deb
```
#### AppImage
For universal Linux distribution:
```bash
# Install linuxdeploy
wget https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
chmod +x linuxdeploy-x86_64.AppImage
# Create AppDir structure
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/share/applications
mkdir -p AppDir/usr/share/mime/packages
cp target/release/rlogg AppDir/usr/bin/
cp packaging/linux/rlogg.desktop AppDir/usr/share/applications/
cp packaging/linux/text-x-log.xml AppDir/usr/share/mime/packages/
# Build AppImage
./linuxdeploy-x86_64.AppImage --appdir AppDir --output appimage
```
### Windows Packages
#### NSIS Installer
Install NSIS and create an installer script:
```nsis
!define APPNAME "RLogg"
!define COMPANYNAME "Your Company"
!define DESCRIPTION "Fast log file viewer"
!define VERSIONMAJOR 0
!define VERSIONMINOR 3
!define VERSIONBUILD 1
RequestExecutionLevel user
Name "${APPNAME}"
Icon "path\to\icon.ico"
OutFile "rlogg-setup.exe"
InstallDir "$LOCALAPPDATA\Programs\${APPNAME}"
Section "Install"
SetOutPath $INSTDIR
File "target\release\rlogg.exe"
WriteUninstaller "$INSTDIR\Uninstall.exe"
# Create file association
WriteRegStr HKCU "Software\Classes\.log" "" "RLogg.LogFile"
WriteRegStr HKCU "Software\Classes\RLogg.LogFile\shell\open\command" "" '"$INSTDIR\rlogg.exe" "%1"'
SectionEnd
Section "Uninstall"
Delete "$INSTDIR\rlogg.exe"
Delete "$INSTDIR\Uninstall.exe"
RMDir "$INSTDIR"
DeleteRegKey HKCU "Software\Classes\.log"
DeleteRegKey HKCU "Software\Classes\RLogg.LogFile"
SectionEnd
```
## Testing File Associations
### Linux Testing
1. **Create a test log file:**
```bash
echo "Test log entry" > /tmp/test.log
```
2. **Test command-line opening:**
```bash
rlogg /tmp/test.log
```
3. **Test XDG opening:**
```bash
xdg-open /tmp/test.log
```
4. **Test in file manager:**
- Navigate to `/tmp/` in your file manager
- Double-click `test.log`
- Right-click → Open With → RLogg
5. **Test multiple files:**
```bash
echo "Log 1" > /tmp/test1.log
echo "Log 2" > /tmp/test2.log
rlogg /tmp/test1.log /tmp/test2.log
```
### Windows Testing
1. **Create a test log file:**
```powershell
"Test log entry" | Out-File -FilePath "$env:TEMP\test.log"
```
2. **Test command-line opening:**
```powershell
& "$env:LOCALAPPDATA\Programs\RLogg\rlogg.exe" "$env:TEMP\test.log"
```
3. **Test in File Explorer:**
- Open File Explorer
- Navigate to `%TEMP%`
- Double-click `test.log`
- Right-click → Open with → RLogg
4. **Test multiple files:**
```powershell
"Log 1" | Out-File -FilePath "$env:TEMP\test1.log"
"Log 2" | Out-File -FilePath "$env:TEMP\test2.log"
& "$env:LOCALAPPDATA\Programs\RLogg\rlogg.exe" "$env:TEMP\test1.log" "$env:TEMP\test2.log"
```
## Platform-Specific Notes
### Linux
- **Desktop environments tested:**
- GNOME 40+
- KDE Plasma 5.20+
- XFCE 4.16+
- i3/sway (via xdg-utils)
- **Dependencies:**
- `xdg-utils` for `update-desktop-database` and `update-mime-database`
- Usually pre-installed on most distributions
- **Wayland compatibility:**
- Tested and working on Wayland sessions
- File associations work the same as X11
### Windows
- **Tested on:**
- Windows 10 (version 1909+)
- Windows 11
- **Registry location:**
- User-local: `HKEY_CURRENT_USER\Software\Classes\`
- No admin rights required
- **Antivirus:**
- Some antivirus software may flag unsigned executables
- Consider code signing for production releases
## CI/CD Integration
### GitHub Actions Example
```yaml
name: Build and Package
on:
release:
types: [created]
jobs:
build-linux:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --release
- name: Package
run: |
cd target/release
tar -czf rlogg-linux-x64.tar.gz rlogg ../../packaging/linux/*
- name: Upload
uses: actions/upload-artifact@v3
with:
name: rlogg-linux
path: target/release/rlogg-linux-x64.tar.gz
build-windows:
runs-on: windows-latest
steps:
- uses: actions/checkout@v3
- name: Build
run: cargo build --release
- name: Package
run: |
Compress-Archive -Path target\release\rlogg.exe, packaging\windows\* -DestinationPath rlogg-windows-x64.zip
- name: Upload
uses: actions/upload-artifact@v3
with:
name: rlogg-windows
path: rlogg-windows-x64.zip
```
## Troubleshooting
See [docs/FILE_ASSOCIATIONS.md](../docs/FILE_ASSOCIATIONS.md) for detailed troubleshooting guides.
## Contributing
When adding new packaging formats or improving existing ones:
1. Test on the target platform
2. Update this README with clear instructions
3. Add troubleshooting steps if needed
4. Submit a pull request with test results
-78
View File
@@ -1,78 +0,0 @@
#!/bin/bash
# Install rlogg and create file association for .log files
# This script installs to user-local directories (~/.local/) and does not require sudo
set -e
echo "Installing RLogg Log Viewer..."
echo ""
# Determine the directory where this script is located
SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
# Check if rlogg binary exists
if [ ! -f "$SCRIPT_DIR/../../target/release/rlogg" ]; then
echo "Error: rlogg binary not found at $SCRIPT_DIR/../../target/release/rlogg"
echo "Please build the project first with: cargo build --release"
exit 1
fi
# User-local installation (no sudo required)
echo "Installing binary to ~/.local/bin/..."
mkdir -p ~/.local/bin
cp "$SCRIPT_DIR/../../target/release/rlogg" ~/.local/bin/
chmod +x ~/.local/bin/rlogg
# Install desktop file with correct path
echo "Installing desktop file..."
mkdir -p ~/.local/share/applications
# Create desktop file with absolute path to the binary
cat > ~/.local/share/applications/rlogg.desktop <<EOF
[Desktop Entry]
Version=1.0
Type=Application
Name=RLogg
GenericName=Log Viewer
Comment=Fast log file viewer with search, filtering, and highlighting
Exec=$HOME/.local/bin/rlogg %F
Icon=rlogg
Terminal=false
Categories=Development;Utility;
MimeType=text/x-log;
Keywords=log;viewer;search;filter;
EOF
# Update MIME database
echo "Installing MIME type definition..."
mkdir -p ~/.local/share/mime/packages
cp "$SCRIPT_DIR/text-x-log.xml" ~/.local/share/mime/packages/
# Update desktop and MIME databases
echo "Updating databases..."
if command -v update-desktop-database &> /dev/null; then
update-desktop-database ~/.local/share/applications 2>/dev/null || true
fi
if command -v update-mime-database &> /dev/null; then
update-mime-database ~/.local/share/mime 2>/dev/null || true
fi
echo ""
echo "Installation complete!"
echo ""
echo "RLogg has been installed to: ~/.local/bin/rlogg"
echo ""
echo "IMPORTANT: Make sure ~/.local/bin is in your PATH."
echo "Add this line to your ~/.bashrc or ~/.zshrc if it's not already there:"
echo ' export PATH="$HOME/.local/bin:$PATH"'
echo ""
echo "File associations have been configured for .log files."
echo "You may need to log out and log back in for file associations to take effect."
echo ""
echo "To uninstall, run:"
echo " rm ~/.local/bin/rlogg"
echo " rm ~/.local/share/applications/rlogg.desktop"
echo " rm ~/.local/share/mime/packages/text-x-log.xml"
echo " update-desktop-database ~/.local/share/applications"
echo " update-mime-database ~/.local/share/mime"
-14
View File
@@ -1,14 +0,0 @@
[Desktop Entry]
Version=1.0
Type=Application
Name=RLogg
GenericName=Log Viewer
Comment=Fast log file viewer with search, filtering, and highlighting
# NOTE: Use absolute path for Exec to work in GUI file managers
# The install.sh script will automatically set this to $HOME/.local/bin/rlogg
Exec=/usr/local/bin/rlogg %F
Icon=rlogg
Terminal=false
Categories=Development;Utility;
MimeType=text/x-log;
Keywords=log;viewer;search;filter;
-7
View File
@@ -1,7 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<mime-info xmlns="http://www.freedesktop.org/standards/shared-mime-info">
<mime-type type="text/x-log">
<comment>Log file</comment>
<glob pattern="*.log"/>
</mime-type>
</mime-info>
-19
View File
@@ -1,19 +0,0 @@
Windows Registry Editor Version 5.00
; Associate .log files with RLogg
; Edit the path below if you installed RLogg to a different location
[HKEY_CURRENT_USER\Software\Classes\.log]
@="RLogg.LogFile"
[HKEY_CURRENT_USER\Software\Classes\RLogg.LogFile]
@="Log File"
[HKEY_CURRENT_USER\Software\Classes\RLogg.LogFile\DefaultIcon]
@="\"C:\\Program Files\\RLogg\\rlogg.exe\",0"
[HKEY_CURRENT_USER\Software\Classes\RLogg.LogFile\shell\open\command]
@="\"C:\\Program Files\\RLogg\\rlogg.exe\" \"%1\""
[HKEY_CURRENT_USER\Software\Classes\RLogg.LogFile\shell\open]
@="Open with RLogg"
-63
View File
@@ -1,63 +0,0 @@
# RLogg Installation Script for Windows
# Run with: powershell -ExecutionPolicy Bypass -File install.ps1
param(
[string]$InstallPath = "$env:LOCALAPPDATA\Programs\RLogg"
)
Write-Host "Installing RLogg Log Viewer..." -ForegroundColor Cyan
Write-Host ""
# Determine the directory where this script is located
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
$BinaryPath = Join-Path (Split-Path (Split-Path $ScriptDir -Parent) -Parent) "target\release\rlogg.exe"
# Check if rlogg.exe exists
if (-not (Test-Path $BinaryPath)) {
Write-Host "Error: rlogg.exe not found at $BinaryPath" -ForegroundColor Red
Write-Host "Please build the project first with: cargo build --release" -ForegroundColor Yellow
exit 1
}
# Create directory
Write-Host "Installing binary to $InstallPath..." -ForegroundColor Green
New-Item -ItemType Directory -Force -Path $InstallPath | Out-Null
# Copy binary
Copy-Item $BinaryPath $InstallPath\
# Create registry entries for .log file association
Write-Host "Configuring file associations..." -ForegroundColor Green
$regPath = "HKCU:\Software\Classes\.log"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "RLogg.LogFile"
$regPath = "HKCU:\Software\Classes\RLogg.LogFile"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "Log File"
$regPath = "HKCU:\Software\Classes\RLogg.LogFile\DefaultIcon"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "`"$InstallPath\rlogg.exe`",0"
$regPath = "HKCU:\Software\Classes\RLogg.LogFile\shell\open\command"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "`"$InstallPath\rlogg.exe`" `"%1`""
$regPath = "HKCU:\Software\Classes\RLogg.LogFile\shell\open"
New-Item -Path $regPath -Force | Out-Null
Set-ItemProperty -Path $regPath -Name "(default)" -Value "Open with RLogg"
Write-Host ""
Write-Host "Installation complete!" -ForegroundColor Green
Write-Host ""
Write-Host "RLogg is installed at: $InstallPath\rlogg.exe" -ForegroundColor Cyan
Write-Host "File associations have been configured for .log files." -ForegroundColor Cyan
Write-Host ""
Write-Host "You can now double-click .log files to open them in RLogg." -ForegroundColor Yellow
Write-Host ""
Write-Host "To uninstall, run:" -ForegroundColor Yellow
Write-Host " Remove-Item -Recurse `"$InstallPath`"" -ForegroundColor Gray
Write-Host " Remove-Item -Recurse HKCU:\Software\Classes\.log" -ForegroundColor Gray
Write-Host " Remove-Item -Recurse HKCU:\Software\Classes\RLogg.LogFile" -ForegroundColor Gray
-12
View File
@@ -12,18 +12,6 @@ pub struct AppConfig {
pub last_search_query: String,
#[serde(default)]
pub highlight_rules: Vec<HighlightRule>,
#[serde(default)]
pub date_range_enabled: bool,
#[serde(default = "default_date_format")]
pub date_format: String,
#[serde(default)]
pub date_from: String,
#[serde(default)]
pub date_to: String,
}
fn default_date_format() -> String {
String::from("%Y-%m-%d %H:%M:%S")
}
impl AppConfig {
-440
View File
@@ -1,440 +0,0 @@
use std::sync::Arc;
use std::path::PathBuf;
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, 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,
file_open_errors: Vec<String>,
file_receiver: Option<std::sync::mpsc::Receiver<Vec<PathBuf>>>,
}
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, initial_files: Vec<PathBuf>, file_receiver: Option<std::sync::mpsc::Receiver<Vec<PathBuf>>>) -> Self {
let indexing_state = IndexingState::new();
let mut tabs = Vec::new();
let mut file_open_errors = Vec::new();
// Open initial files from command line
for file_path in initial_files {
if file_path.exists() {
if file_path.is_file() {
if let Some(tab) = open_file(file_path.clone(), &indexing_state) {
tabs.push(tab);
} else {
file_open_errors.push(format!("Failed to open: {}", file_path.display()));
}
} else {
file_open_errors.push(format!("Not a file: {}", file_path.display()));
}
} else {
file_open_errors.push(format!("File not found: {}", file_path.display()));
}
}
Self {
tabs,
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,
search_state: SearchState::new(),
highlight_manager: HighlightManager::new(config.highlight_rules),
first_frame: true,
file_open_errors,
file_receiver,
}
}
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));
}
// Display file opening errors if any
if !self.file_open_errors.is_empty() {
egui::Window::new("File Opening Errors")
.collapsible(false)
.resizable(false)
.anchor(egui::Align2::CENTER_CENTER, [0.0, 0.0])
.show(ctx, |ui| {
ui.label("The following files could not be opened:");
ui.add_space(10.0);
egui::ScrollArea::vertical()
.max_height(300.0)
.show(ui, |ui| {
for error in &self.file_open_errors {
ui.label(error);
}
});
ui.add_space(10.0);
if ui.button("OK").clicked() {
self.file_open_errors.clear();
}
});
}
}
}
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) {
// Poll for new files from other instances
if let Some(receiver) = &self.file_receiver {
while let Ok(files) = receiver.try_recv() {
for file_path in files {
if file_path.exists() {
if file_path.is_file() {
if let Some(tab) = open_file(file_path.clone(), &self.indexing_state) {
self.tabs.push(tab);
self.active_tab_index = self.tabs.len() - 1;
} else {
// We probably don't want to disrupt the user with a modal for this background action,
// but maybe we could log it or flash a message. For now, let's add it to errors.
self.file_open_errors.push(format!("Failed to open: {}", file_path.display()));
}
}
}
}
// If we received files and opened them, we should request a repaint to show the new tabs immediately
ctx.request_repaint();
// If there were errors, we might want to show the error window again?
// The error window logic is in handle_first_frame/update, strictly speaking it only shows if errors exist.
// However, handle_first_frame is only called once. We should check if we should show errors here or if the existing logic handles it.
// The existing logic checks !self.file_open_errors.is_empty() in handle_first_frame, but checking lines 275-298 reveals
// that the window display code is INSIDE handle_first_frame? No, wait.
// Let's check handle_first_frame implementation again.
}
}
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();
}
}
}
+245 -82
View File
@@ -7,91 +7,23 @@ mod highlight;
mod line_index;
mod search;
mod tab_manager;
mod theme;
mod types;
mod ui;
mod log_viewer_app;
use eframe::egui;
use clap::Parser;
use std::path::PathBuf;
use std::sync::Arc;
use crate::log_viewer_app::LogViewerApp;
use config::AppConfig;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream};
use std::io::{prelude::*, BufReader};
use std::sync::mpsc;
use std::thread;
#[derive(Parser)]
#[command(name = "rlogg")]
#[command(version, about = "A fast log file viewer with search, filtering, and highlighting")]
struct Cli {
/// Log files to open on startup
#[arg(value_name = "FILES")]
files: Vec<PathBuf>,
}
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 {
// Parse command-line arguments
let cli = Cli::parse();
// Single instance check
#[cfg(windows)]
let name = "rlogg_ipc";
#[cfg(not(windows))]
let name = "/tmp/rlogg.sock";
// Try to connect to existing instance
if let Ok(mut conn) = LocalSocketStream::connect(name) {
if !cli.files.is_empty() {
let files_json = serde_json::to_string(&cli.files).unwrap();
if let Err(e) = conn.write_all(files_json.as_bytes()) {
eprintln!("Failed to send files to existing instance: {}", e);
}
}
return Ok(());
}
// If connection failed, we are the first instance.
// Create a listener.
// Cleanup socket on Unix
#[cfg(not(windows))]
if std::path::Path::new(name).exists() {
std::fs::remove_file(name).ok();
}
let listener = match LocalSocketListener::bind(name) {
Ok(l) => l,
Err(error) => {
eprintln!("Failed to bind to socket: {}", error);
// Fallback to running without single-instance support if binding fails
return run_app(cli, None);
}
};
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
for mut conn in listener.incoming().filter_map(|x| x.ok()) {
let tx = tx.clone();
thread::spawn(move || {
let mut reader = BufReader::new(&mut conn);
let mut buffer = String::new();
// We assume one connection per batch of files, validation is relaxed for simplicity
if let Ok(_) = reader.read_to_string(&mut buffer) {
if let Ok(files) = serde_json::from_str::<Vec<PathBuf>>(&buffer) {
tx.send(files).ok();
}
}
});
}
});
run_app(cli, Some(rx))
}
fn run_app(cli: Cli, file_receiver: Option<mpsc::Receiver<Vec<PathBuf>>>) -> eframe::Result {
let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default()
.with_inner_size([1200.0, 800.0])
@@ -102,14 +34,245 @@ fn run_app(cli: Cli, file_receiver: Option<mpsc::Receiver<Vec<PathBuf>>>) -> efr
};
let config = AppConfig::load();
let initial_files = cli.files;
eframe::run_native(
"RLogg",
options,
Box::new(move |cc| {
theme::apply_theme(&cc.egui_ctx);
Ok(Box::new(LogViewerApp::new(config, initial_files, file_receiver)))
}),
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();
}
}
}
+13 -163
View File
@@ -5,7 +5,6 @@ use std::io::BufReader;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::thread;
use chrono::NaiveDateTime;
use crate::line_index::LineIndex;
use crate::types::FilteredLine;
@@ -44,10 +43,6 @@ pub struct SearchParams {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
pub date_range_enabled: bool,
pub date_format: String,
pub date_from: String,
pub date_to: String,
}
impl SearchParams {
@@ -104,136 +99,6 @@ pub fn start_search(
});
}
fn format_to_regex(format: &str) -> Option<String> {
// Convert chrono format to regex pattern
let mut regex = format.to_string();
regex = regex.replace("%Y", r"\d{4}"); // 4-digit year
regex = regex.replace("%m", r"\d{2}"); // 2-digit month
regex = regex.replace("%d", r"\d{2}"); // 2-digit day
regex = regex.replace("%H", r"\d{2}"); // 2-digit hour
regex = regex.replace("%M", r"\d{2}"); // 2-digit minute
regex = regex.replace("%S", r"\d{2}"); // 2-digit second
regex = regex.replace("%I", r"\d{2}"); // 2-digit hour (12h)
regex = regex.replace("%p", r"(AM|PM)"); // AM/PM
Some(regex)
}
fn extract_and_parse_date(content: &str, format: &str) -> Option<NaiveDateTime> {
// Convert format to regex pattern
let pattern = format_to_regex(format)?;
let regex = Regex::new(&pattern).ok()?;
let date_str = regex.captures(content)?.get(0)?.as_str();
// Parse using the provided format
NaiveDateTime::parse_from_str(date_str, format).ok()
}
fn find_line_with_date(
line_index: &LineIndex,
file_handle: &mut BufReader<File>,
start_line: usize,
format: &str,
move_down: bool,
) -> Option<(usize, NaiveDateTime)> {
let max_search = 1000; // Search up to 10 lines
let range: Box<dyn Iterator<Item = usize>> = if move_down {
Box::new(start_line..std::cmp::min(start_line + max_search, line_index.total_lines))
} else {
Box::new((start_line.saturating_sub(max_search)..=start_line).rev())
};
for line_num in range {
if let Some(content) = line_index.read_line(file_handle, line_num) {
if let Some(date) = extract_and_parse_date(&content, format) {
return Some((line_num, date));
}
}
}
None
}
fn binary_search_date(
line_index: &LineIndex,
file_handle: &mut BufReader<File>,
target_date: &NaiveDateTime,
format: &str,
find_first: bool, // true = find first occurrence, false = find last
) -> Option<usize> {
let mut left = 0;
let mut right = line_index.total_lines;
let mut result = None;
while left < right {
let mid = (left + right) / 2;
// Find a line with a date starting from mid, moving down
match find_line_with_date(line_index, file_handle, mid, format, true) {
Some((line_with_date, date)) => {
if find_first {
// Finding first line >= target_date
if date >= *target_date {
result = Some(line_with_date);
right = mid;
} else {
left = mid + 1;
}
} else {
// Finding last line <= target_date
if date <= *target_date {
result = Some(line_with_date);
// Ensure we make progress in the binary search
left = mid + 1;
} else {
right = mid;
}
}
}
None => {
// Can't find a date near mid, try searching in a larger range
// or skip this section
if find_first {
// When finding first, if we can't find a date, try the right half
left = mid + 1;
} else {
// When finding last, if we can't find a date, try the left half
right = mid;
}
}
}
}
result
}
fn find_date_range(
params: &SearchParams,
line_index: &LineIndex,
file_path: &Path,
) -> Option<(usize, usize)> {
if !params.date_range_enabled || params.date_from.is_empty() || params.date_to.is_empty() {
return None;
}
// Parse the target dates using the provided format
let date_from = NaiveDateTime::parse_from_str(&params.date_from, &params.date_format).ok()?;
let date_to = NaiveDateTime::parse_from_str(&params.date_to, &params.date_format).ok()?;
let file = File::open(file_path).ok()?;
let mut file_handle = BufReader::new(file);
// Binary search for first line >= date_from
let start_line = binary_search_date(line_index, &mut file_handle, &date_from, &params.date_format, true)?;
// Binary search for last line <= date_to
let end_line = binary_search_date(line_index, &mut file_handle, &date_to, &params.date_format, false)?;
if start_line < end_line {
Some((start_line, end_line + 1)) // +1 to include the end line
} else {
None
}
}
fn search_lines(
params: &SearchParams,
line_index: &LineIndex,
@@ -245,28 +110,17 @@ fn search_lines(
return Vec::new();
}
// Determine the line range to search (all lines or date range)
let (search_start, search_end) = if let Some((start, end)) = find_date_range(params, line_index, file_path) {
(start, end)
} else {
(0, total_lines)
};
let lines_to_search = search_end - search_start;
if lines_to_search == 0 {
return Vec::new();
}
// Determine optimal chunk size based on lines to search
// Determine optimal chunk size based on total lines
// Aim for enough chunks to utilize all cores, but not too many to avoid overhead
let num_threads = rayon::current_num_threads();
let min_chunk_size = 1000;
let chunk_size = (lines_to_search / (num_threads * 4)).max(min_chunk_size);
let min_chunk_size = 1000; // Process at least 1000 lines per chunk
let chunk_size = (total_lines / (num_threads * 4)).max(min_chunk_size);
// Split line numbers into chunks within the search range
let chunks: Vec<(usize, usize)> = (search_start..search_end)
// Split line numbers into chunks
let chunks: Vec<(usize, usize)> = (0..total_lines)
.step_by(chunk_size)
.map(|start| {
let end = (start + chunk_size).min(search_end);
let end = (start + chunk_size).min(total_lines);
(start, end)
})
.collect();
@@ -287,17 +141,13 @@ fn search_lines(
// Read lines in this chunk efficiently (one seek, sequential reads)
let lines = line_index.read_line_range(&mut file_handle, *start, *end);
// Process each line - only store line numbers, not content
// Process each line
for (line_number, content) in lines {
// If date range is enabled and query is empty, include all lines in range
let should_include = if params.date_range_enabled && params.query.is_empty() {
true
} else {
params.matches_line(&content, &regex_matcher)
};
if should_include {
chunk_results.push(FilteredLine { line_number });
if params.matches_line(&content, &regex_matcher) {
chunk_results.push(FilteredLine {
line_number,
content,
});
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub fn open_file_dialog(indexing_state: &IndexingState) -> Option<FileTab> {
open_file(path, indexing_state)
}
pub fn open_file(path: PathBuf, indexing_state: &IndexingState) -> Option<FileTab> {
fn open_file(path: PathBuf, indexing_state: &IndexingState) -> Option<FileTab> {
indexing_state.start_indexing();
// Background indexing for progress indication
-178
View File
@@ -1,178 +0,0 @@
use eframe::egui;
/// Modern dark purple color palette
pub struct ColorPalette {
// Background colors
pub bg_primary: egui::Color32, // Main background
pub bg_secondary: egui::Color32, // Secondary panels
pub bg_tertiary: egui::Color32, // Elevated elements
// Accent colors
pub accent_primary: egui::Color32, // Primary purple accent
pub accent_secondary: egui::Color32, // Secondary accent
pub accent_bright: egui::Color32, // Bright highlights
// Text colors
pub text_primary: egui::Color32,
pub text_secondary: egui::Color32,
pub text_muted: egui::Color32,
// UI element colors
pub selection: egui::Color32,
pub line_number: egui::Color32,
pub border: egui::Color32,
}
impl ColorPalette {
pub fn dark_purple() -> Self {
Self {
// Deep purple-gray backgrounds
bg_primary: egui::Color32::from_rgb(24, 20, 32), // #18141F
bg_secondary: egui::Color32::from_rgb(32, 26, 42), // #201A2A
bg_tertiary: egui::Color32::from_rgb(42, 35, 54), // #2A2336
// Purple accents - modern and vibrant
accent_primary: egui::Color32::from_rgb(138, 98, 208), // #8A62D0
accent_secondary: egui::Color32::from_rgb(108, 68, 178), // #6C44B2
accent_bright: egui::Color32::from_rgb(168, 128, 238), // #A880EE
// Text colors with good contrast
text_primary: egui::Color32::from_rgb(230, 230, 240), // #E6E6F0
text_secondary: egui::Color32::from_rgb(190, 190, 210), // #BEBED2
text_muted: egui::Color32::from_rgb(140, 140, 165), // #8C8CA5
// UI elements
selection: egui::Color32::from_rgb(108, 68, 178), // #6C44B2
line_number: egui::Color32::from_rgb(110, 100, 130), // #6E6482
border: egui::Color32::from_rgb(60, 50, 75), // #3C324B
}
}
}
/// Apply the dark purple theme to the egui context
pub fn apply_theme(ctx: &egui::Context) {
let palette = ColorPalette::dark_purple();
let mut style = (*ctx.style()).clone();
// === Spacing and sizing for better UX ===
style.spacing.item_spacing = egui::vec2(8.0, 6.0);
style.spacing.button_padding = egui::vec2(12.0, 6.0);
style.spacing.menu_margin = egui::Margin::same(8.0);
style.spacing.indent = 20.0;
style.spacing.scroll = egui::style::ScrollStyle {
bar_width: 10.0,
handle_min_length: 20.0,
bar_inner_margin: 2.0,
bar_outer_margin: 0.0,
..Default::default()
};
// === Color scheme ===
let visuals = &mut style.visuals;
// Dark mode
visuals.dark_mode = true;
// Window and panel backgrounds
visuals.window_fill = palette.bg_primary;
visuals.panel_fill = palette.bg_primary;
visuals.faint_bg_color = palette.bg_secondary;
visuals.extreme_bg_color = palette.bg_tertiary;
// Text colors (using override_text_color to set custom text color)
visuals.override_text_color = Some(palette.text_primary);
// Widget colors
visuals.widgets.noninteractive.bg_fill = palette.bg_secondary;
visuals.widgets.noninteractive.weak_bg_fill = palette.bg_secondary;
visuals.widgets.noninteractive.bg_stroke = egui::Stroke::new(1.0, palette.border);
visuals.widgets.noninteractive.fg_stroke = egui::Stroke::new(1.0, palette.text_secondary);
// Inactive/hovered widgets
visuals.widgets.inactive.bg_fill = palette.bg_secondary;
visuals.widgets.inactive.weak_bg_fill = palette.bg_secondary;
visuals.widgets.inactive.bg_stroke = egui::Stroke::new(1.0, palette.border);
visuals.widgets.inactive.fg_stroke = egui::Stroke::new(1.0, palette.text_primary);
visuals.widgets.hovered.bg_fill = palette.bg_tertiary;
visuals.widgets.hovered.weak_bg_fill = palette.bg_tertiary;
visuals.widgets.hovered.bg_stroke = egui::Stroke::new(1.5, palette.accent_primary);
visuals.widgets.hovered.fg_stroke = egui::Stroke::new(1.5, palette.text_primary);
// Active/clicked widgets
visuals.widgets.active.bg_fill = palette.accent_secondary;
visuals.widgets.active.weak_bg_fill = palette.accent_secondary;
visuals.widgets.active.bg_stroke = egui::Stroke::new(2.0, palette.accent_bright);
visuals.widgets.active.fg_stroke = egui::Stroke::new(2.0, palette.text_primary);
visuals.widgets.open.bg_fill = palette.accent_secondary;
visuals.widgets.open.weak_bg_fill = palette.accent_secondary;
visuals.widgets.open.bg_stroke = egui::Stroke::new(1.5, palette.accent_primary);
visuals.widgets.open.fg_stroke = egui::Stroke::new(1.5, palette.text_primary);
// Selection
visuals.selection.bg_fill = palette.selection;
visuals.selection.stroke = egui::Stroke::new(1.0, palette.accent_bright);
// Hyperlinks
visuals.hyperlink_color = palette.accent_bright;
// Window styling
visuals.window_rounding = egui::Rounding::same(8.0);
visuals.window_shadow = egui::epaint::Shadow {
offset: egui::vec2(0.0, 8.0),
blur: 20.0,
spread: 0.0,
color: egui::Color32::from_black_alpha(80),
};
visuals.window_stroke = egui::Stroke::new(1.0, palette.border);
// Popup styling
visuals.popup_shadow = egui::epaint::Shadow {
offset: egui::vec2(0.0, 4.0),
blur: 16.0,
spread: 0.0,
color: egui::Color32::from_black_alpha(100),
};
// Resize handle
visuals.resize_corner_size = 12.0;
// Menu rounding
visuals.menu_rounding = egui::Rounding::same(6.0);
// Indent guide
visuals.indent_has_left_vline = true;
visuals.striped = true;
// Borders and separators
visuals.window_stroke = egui::Stroke::new(1.0, palette.border);
// === Text styles with better readability ===
style.text_styles.insert(
egui::TextStyle::Body,
egui::FontId::proportional(14.0),
);
style.text_styles.insert(
egui::TextStyle::Button,
egui::FontId::proportional(14.0),
);
style.text_styles.insert(
egui::TextStyle::Heading,
egui::FontId::proportional(18.0),
);
style.text_styles.insert(
egui::TextStyle::Monospace,
egui::FontId::monospace(13.0),
);
// Apply the style
ctx.set_style(style);
}
/// Get the color palette for use in custom rendering
pub fn get_palette() -> ColorPalette {
ColorPalette::dark_purple()
}
+1 -2
View File
@@ -7,9 +7,8 @@ pub struct HighlightRule {
pub enabled: bool,
}
// Filtered lines now only store line numbers to save memory
// Content is loaded on-demand when rendering
#[derive(Clone)]
pub struct FilteredLine {
pub line_number: usize,
pub content: String,
}
-152
View File
@@ -1,152 +0,0 @@
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);
});
});
}
+15 -32
View File
@@ -1,7 +1,6 @@
use eframe::egui;
use crate::file_tab::FileTab;
use crate::theme;
use crate::types::HighlightRule;
pub struct LogViewContext<'a> {
@@ -87,7 +86,7 @@ fn handle_scroll_to_line(
) {
if tab.scroll_to_main {
let target_row = tab.main_scroll_offset;
let adgusted_row_height = row_height + 6f32;
let adgusted_row_height = row_height + 3f32;
let scroll_offset = (target_row as f32) * adgusted_row_height;
eprintln!("=== SCROLL TO LINE ===");
@@ -111,7 +110,7 @@ fn handle_page_scroll(
total_lines: usize,
) {
if let Some(direction) = tab.page_scroll_direction.take() {
let row_height_offset = row_height + 6f32;
let row_height_offset = row_height + 3f32;
let viewport_height = ui.available_height();
let rows_per_page = (viewport_height / row_height_offset).floor().max(1.0);
let scroll_delta = direction * rows_per_page * row_height_offset;
@@ -162,21 +161,15 @@ fn render_visible_lines(
fn get_line_content(tab: &mut FileTab, show_all: bool, display_idx: usize) -> (usize, String) {
if show_all {
// Main view: read line by display index
let content = tab
.line_index
.read_line(&mut tab.file_handle, display_idx)
.unwrap_or_default();
(display_idx, content)
} else {
// Filtered view: get line number from filtered list, then read content on-demand
if display_idx < tab.filtered_lines.len() {
let line_number = tab.filtered_lines[display_idx].line_number;
let content = tab
.line_index
.read_line(&mut tab.file_handle, line_number)
.unwrap_or_default();
(line_number, content)
let filtered = &tab.filtered_lines[display_idx];
(filtered.line_number, filtered.content.clone())
} else {
(0, String::new())
}
@@ -193,59 +186,49 @@ fn render_line<F>(
) where
F: FnOnce(bool),
{
let palette = theme::get_palette();
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]));
// Improved color scheme with better visual hierarchy
let bg_color = if is_selected {
palette.selection
egui::Color32::from_rgb(70, 130, 180)
} else if let Some(color) = highlight_color {
color
} else {
egui::Color32::TRANSPARENT
};
// Better padding and margins for improved readability
let frame = egui::Frame::none()
.fill(bg_color)
.inner_margin(egui::Margin::symmetric(8.0, 3.0));
.inner_margin(egui::Margin::symmetric(2.0, 1.0));
frame.show(ui, |ui| {
ui.horizontal(|ui| {
// Line numbers with better styling
let line_num_text = egui::RichText::new(format!("{:6}", line_num + 1))
let line_num_text = egui::RichText::new(format!("{:6} ", line_num + 1))
.monospace()
.color(if is_selected {
palette.text_primary
egui::Color32::WHITE
} else {
palette.line_number
egui::Color32::DARK_GRAY
});
let line_num_response = ui.label(line_num_text);
// Separator between line number and content
ui.add_space(4.0);
ui.separator();
ui.add_space(4.0);
// Content text with improved styling
let text = egui::RichText::new(content).monospace().color(
if is_selected {
palette.text_primary
egui::Color32::WHITE
} else {
palette.text_primary
ui.style().visuals.text_color()
},
);
let text_response = ui
.scope(|ui| {
// Better text selection colors
ui.style_mut().visuals.selection.bg_fill = palette.accent_bright;
ui.style_mut().visuals.selection.stroke.color = palette.accent_primary;
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;
-1
View File
@@ -3,7 +3,6 @@ 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};
+45 -119
View File
@@ -1,17 +1,12 @@
use eframe::egui;
use crate::search::SearchState;
use crate::theme;
pub struct SearchPanelState {
pub query: String,
pub case_sensitive: bool,
pub use_regex: bool,
pub history: Vec<String>,
pub date_range_enabled: bool,
pub date_format: String,
pub date_from: String,
pub date_to: String,
}
pub struct SearchPanelActions {
@@ -25,7 +20,6 @@ pub fn render_search_panel(
state: &mut SearchPanelState,
search_state: &SearchState,
match_count: usize,
request_focus: bool,
) -> SearchPanelActions {
let mut actions = SearchPanelActions {
execute_search: false,
@@ -33,127 +27,59 @@ pub fn render_search_panel(
config_changed: false,
};
let palette = theme::get_palette();
egui::TopBottomPanel::bottom("search_panel").show(ctx, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
ui.label("🔍 Filter:");
egui::TopBottomPanel::bottom("search_panel")
.frame(egui::Frame::none()
.fill(palette.bg_secondary)
.inner_margin(egui::Margin::symmetric(12.0, 10.0)))
.show(ctx, |ui| {
ui.vertical(|ui| {
ui.horizontal(|ui| {
// Filter label with icon
let label = egui::RichText::new("🔍 Filter:")
.size(14.0)
.color(palette.text_primary);
ui.label(label);
let text_edit_width = 200.0;
let text_response = ui.add_sized(
[text_edit_width, 20.0],
egui::TextEdit::singleline(&mut state.query),
);
ui.add_space(4.0);
let enter_pressed =
text_response.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter));
// Text input with proper height matching buttons and stable ID
let text_edit_width = 300.0;
let search_input_id = egui::Id::new("search_input_field");
let text_response = ui.add(
egui::TextEdit::singleline(&mut state.query)
.id(search_input_id)
.desired_width(text_edit_width)
.hint_text("Enter search query...")
);
// Request focus if Ctrl+F was pressed
if request_focus {
text_response.request_focus();
}
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);
}
ui.add_space(8.0);
// Checkboxes
let case_changed = ui
.checkbox(&mut state.case_sensitive, "Case sensitive")
.changed();
let regex_changed = ui.checkbox(&mut state.use_regex, "Regex").changed();
let date_range_changed = ui.checkbox(&mut state.date_range_enabled, "Date range").changed();
if case_changed || regex_changed || date_range_changed {
actions.config_changed = true;
}
ui.add_space(8.0);
// Search button
if ui.button("Search").clicked() || enter_pressed {
actions.execute_search = true;
actions.config_changed = true;
}
// Clear button and match count
if !state.query.is_empty() {
if ui.button("✖ Clear").clicked() {
actions.clear_search = true;
}
ui.add_space(8.0);
let count_text = egui::RichText::new(format!("{} matches", match_count))
.color(palette.accent_bright)
.size(13.0);
ui.label(count_text);
}
});
// Date range fields (show when date_range_enabled is true)
if state.date_range_enabled {
ui.add_space(6.0);
ui.horizontal(|ui| {
ui.label("Format:");
ui.add(
egui::TextEdit::singleline(&mut state.date_format)
.desired_width(200.0)
.hint_text("%Y-%m-%d %H:%M:%S")
);
});
ui.add_space(4.0);
ui.horizontal(|ui| {
ui.label("From:");
ui.add(
egui::TextEdit::singleline(&mut state.date_from)
.desired_width(180.0)
.hint_text("2025-01-01 00:00:00")
);
ui.add_space(8.0);
ui.label("To:");
ui.add(
egui::TextEdit::singleline(&mut state.date_to)
.desired_width(180.0)
.hint_text("2025-01-01 01:00:00")
);
});
if !state.history.is_empty() {
render_history_dropdown(ui, state);
}
// Progress bar
if search_state.is_searching() {
ui.add_space(6.0);
let progress = search_state.get_progress();
ui.horizontal(|ui| {
ui.add(
egui::ProgressBar::new(progress)
.text(format!("Searching... {:.0}%", progress * 100.0))
.animate(true),
);
});
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
}
+18 -84
View File
@@ -13,95 +13,29 @@ pub fn render_tabs_panel(
}
egui::TopBottomPanel::top("tabs_panel").show(ctx, |ui| {
// Get or initialize scroll offset from persistent storage
let scroll_id = egui::Id::new("tabs_scroll_offset");
let mut scroll_offset: f32 = ui.ctx().data_mut(|d| d.get_persisted(scroll_id).unwrap_or(0.0));
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()))
};
// Use horizontal ScrollArea for tabs with hidden scrollbar
let mut scroll_area = egui::ScrollArea::horizontal()
.auto_shrink([false; 2])
.scroll_bar_visibility(egui::scroll_area::ScrollBarVisibility::AlwaysHidden);
// Apply the stored scroll offset
scroll_area = scroll_area.horizontal_scroll_offset(scroll_offset);
let scroll_output = scroll_area.show(ui, |ui| {
ui.horizontal(|ui| {
for (idx, tab) in tabs.iter().enumerate() {
let is_active = idx == *active_tab_index;
let filename = tab.filename();
// Truncate filename if too long (max 20 characters)
let display_name = if filename.len() > 20 {
format!("{}...", &filename[..17])
} else {
filename.clone()
};
let button_text = if is_active {
egui::RichText::new(format!("📄 {}", display_name)).strong()
} else {
egui::RichText::new(format!("📄 {}", display_name))
};
// Fixed width for tab label (150 pixels)
let tab_response = ui.add_sized(
[150.0, ui.available_height()],
egui::SelectableLabel::new(is_active, button_text)
);
if tab_response.clicked() {
*active_tab_index = idx;
}
// Show full filename on hover
if filename.len() > 20 {
tab_response.on_hover_text(&filename);
}
if ui.small_button("").clicked() {
*on_close_tab = Some(idx);
}
ui.separator();
if ui.selectable_label(is_active, button_text).clicked() {
*active_tab_index = idx;
}
})
});
// Check if content overflows (tabs exceed screen width)
let content_width = scroll_output.content_size.x;
let viewport_width = scroll_output.inner_rect.width();
let content_overflows = content_width > viewport_width;
if ui.small_button("").clicked() {
*on_close_tab = Some(idx);
}
// Handle mouse wheel scrolling when hovering over tabs and content overflows
let mut should_update_offset = false;
if content_overflows && ui.rect_contains_pointer(scroll_output.inner_rect) {
// Get raw scroll delta outside of any closures
let raw_scroll_y = ui.input(|i| i.raw_scroll_delta.y);
// Check for raw mouse wheel events
if raw_scroll_y != 0.0 {
// Use vertical scroll (mouse wheel) for horizontal scrolling
let scroll_amount = -raw_scroll_y * 2.0;
scroll_offset = (scroll_offset + scroll_amount).max(0.0);
// Clamp to valid range
let max_offset = (content_width - viewport_width).max(0.0);
scroll_offset = scroll_offset.min(max_offset);
should_update_offset = true;
ui.separator();
}
} else {
// Update offset from ScrollArea state (in case of other interactions)
scroll_offset = scroll_output.state.offset.x;
should_update_offset = true;
}
// Store the offset outside of any input closures
if should_update_offset {
ui.ctx().data_mut(|d| {
d.insert_persisted(scroll_id, scroll_offset);
});
}
if let Some(tab) = tabs.get(*active_tab_index) {
ui.label(format!("({} lines)", tab.line_index.total_lines));
}
});
});
}
+3 -12
View File
@@ -3,23 +3,16 @@ 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,
) -> TopMenuActions {
let mut top_menu_actions = TopMenuActions{open_file_requested: false};
on_open_file: &mut bool,
) {
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| {
if ui.button("📂 Open File").clicked() {
top_menu_actions.open_file_requested = true;
*on_open_file = true;
}
if ui.button("🎨 Highlights").clicked() {
@@ -53,6 +46,4 @@ pub fn render_top_menu(
}
});
});
top_menu_actions
}