Compare commits

..
15 Commits
Author SHA1 Message Date
stas cce8a818d1 0.3.0
Build / Build - Linux-x86_64 (push) Has been cancelled
Build / Build - macOS-aarch64 (push) Has been cancelled
Build / Build - macOS-x86_64 (push) Has been cancelled
Build / Build - Windows-x86_64 (push) Has been cancelled
2025-12-09 20:14:21 +01:00
stas edbcbe728f binary search date range 2025-12-09 20:11:50 +01:00
stas b7f7aeeb9a inefficient date range 2025-12-09 19:20:58 +01:00
stas 97caee37cc export filtered results 2025-12-09 18:46:12 +01:00
stas 7c2632a297 QOL shortcuts 2025-12-09 18:39:01 +01:00
stas cc4fad4532 tabs names fix 2025-12-09 18:31:25 +01:00
stas 8360d0d9b4 tabs scroll 2025-12-09 18:28:31 +01:00
stas be60c65ab8 cleaner UI 2025-12-02 20:03:25 +01:00
stas c24d94ca2b cleaner UI 2025-12-02 19:50:12 +01:00
stas 75df916d97 memory optimisation 2025-12-02 19:32:31 +01:00
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
24 changed files with 953 additions and 1823 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 -185
View File
@@ -172,56 +172,6 @@ dependencies = [
"libc", "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]] [[package]]
name = "arboard" name = "arboard"
version = "3.6.1" version = "3.6.1"
@@ -724,46 +674,6 @@ dependencies = [
"windows-link", "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]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "5.4.1" version = "5.4.1"
@@ -783,12 +693,6 @@ dependencies = [
"unicode-width", "unicode-width",
] ]
[[package]]
name = "colorchoice"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
[[package]] [[package]]
name = "com" name = "com"
version = "0.6.0" version = "0.6.0"
@@ -1611,12 +1515,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.5.2" version = "0.5.2"
@@ -1793,38 +1691,6 @@ dependencies = [
"hashbrown 0.16.1", "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]] [[package]]
name = "itoa" name = "itoa"
version = "1.0.15" version = "1.0.15"
@@ -2434,12 +2300,6 @@ version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "once_cell_polyfill"
version = "1.70.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
[[package]] [[package]]
name = "orbclient" name = "orbclient"
version = "0.3.49" version = "0.3.49"
@@ -2846,12 +2706,10 @@ dependencies = [
[[package]] [[package]]
name = "rlogg" name = "rlogg"
version = "0.4.1" version = "0.3.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"clap",
"eframe", "eframe",
"interprocess",
"rayon", "rayon",
"regex", "regex",
"rfd", "rfd",
@@ -2871,15 +2729,6 @@ version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" 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]] [[package]]
name = "rustix" name = "rustix"
version = "0.38.44" version = "0.38.44"
@@ -2952,12 +2801,6 @@ dependencies = [
"tiny-skia", "tiny-skia",
] ]
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.228" version = "1.0.228"
@@ -3137,15 +2980,6 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "spinning"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d4f0e86297cad2658d92a707320d87bf4e6ae1050287f51d19b67ef3f153a7b"
dependencies = [
"lock_api",
]
[[package]] [[package]]
name = "spirv" name = "spirv"
version = "0.3.0+sdk-1.3.268.0" version = "0.3.0+sdk-1.3.268.0"
@@ -3173,12 +3007,6 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731" checksum = "6637bab7722d379c8b41ba849228d680cc12d0a45ba1fa2b48f2a30577a06731"
[[package]]
name = "strsim"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.109" version = "1.0.109"
@@ -3309,12 +3137,6 @@ dependencies = [
"zerovec", "zerovec",
] ]
[[package]]
name = "to_method"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7c4ceeeca15c8384bbc3e011dbd8fccb7f068a440b752b7d9b32ceb0ca0e2e8"
[[package]] [[package]]
name = "toml_datetime" name = "toml_datetime"
version = "0.7.3" version = "0.7.3"
@@ -3457,12 +3279,6 @@ version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
[[package]]
name = "utf8parse"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]] [[package]]
name = "uuid" name = "uuid"
version = "1.18.1" version = "1.18.1"
+1 -6
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "rlogg" name = "rlogg"
version = "0.4.1" version = "0.3.0"
edition = "2024" edition = "2024"
authors = ["Stanislav Pastushenko <staspast1@gmail.com>"] authors = ["Stanislav Pastushenko <staspast1@gmail.com>"]
description = "A fast log file viewer with search, filtering, and highlighting capabilities" description = "A fast log file viewer with search, filtering, and highlighting capabilities"
@@ -18,8 +18,3 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
rayon = "1.10" rayon = "1.10"
chrono = "0.4" 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
-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();
}
}
}
+464 -78
View File
@@ -10,88 +10,21 @@ mod tab_manager;
mod theme; mod theme;
mod types; mod types;
mod ui; mod ui;
mod log_viewer_app;
use eframe::egui; use eframe::egui;
use clap::Parser; use std::sync::Arc;
use std::path::PathBuf;
use crate::log_viewer_app::LogViewerApp;
use config::AppConfig; use config::AppConfig;
use interprocess::local_socket::{LocalSocketListener, LocalSocketStream}; use file_tab::FileTab;
use std::io::{prelude::*, BufReader}; use highlight::HighlightManager;
use std::sync::mpsc; use search::{add_to_history, start_search, SearchParams, SearchState};
use std::thread; use tab_manager::{close_tab, open_file_dialog, IndexingState};
use ui::{
#[derive(Parser)] render_highlight_editor, render_log_view, render_search_panel, render_tabs_panel,
#[command(name = "rlogg")] render_top_menu, LogViewContext, SearchPanelState,
#[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>,
}
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(); fn main() -> eframe::Result {
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 { let options = eframe::NativeOptions {
viewport: egui::ViewportBuilder::default() viewport: egui::ViewportBuilder::default()
.with_inner_size([1200.0, 800.0]) .with_inner_size([1200.0, 800.0])
@@ -102,14 +35,467 @@ fn run_app(cli: Cli, file_receiver: Option<mpsc::Receiver<Vec<PathBuf>>>) -> efr
}; };
let config = AppConfig::load(); let config = AppConfig::load();
let initial_files = cli.files;
eframe::run_native( eframe::run_native(
"RLogg", "RLogg",
options, options,
Box::new(move |cc| { Box::new(move |cc| {
// Apply the modern dark purple theme
theme::apply_theme(&cc.egui_ctx); theme::apply_theme(&cc.egui_ctx);
Ok(Box::new(LogViewerApp::new(config, initial_files, file_receiver))) Ok(Box::new(LogViewerApp::new(config)))
}), }),
) )
} }
struct LogViewerApp {
tabs: Vec<FileTab>,
active_tab_index: usize,
search_panel_state: SearchPanelState,
indexing_state: IndexingState,
search_state: SearchState,
highlight_manager: HighlightManager,
first_frame: bool,
}
impl LogViewerApp {
fn new(config: AppConfig) -> Self {
Self {
tabs: Vec::new(),
active_tab_index: 0,
search_panel_state: SearchPanelState {
query: config.last_search_query,
case_sensitive: config.case_sensitive,
use_regex: config.use_regex,
history: config.search_history,
date_range_enabled: config.date_range_enabled,
date_format: config.date_format,
date_from: config.date_from,
date_to: config.date_to,
},
indexing_state: IndexingState::new(),
search_state: SearchState::new(),
highlight_manager: HighlightManager::new(config.highlight_rules),
first_frame: true,
}
}
fn active_tab(&self) -> Option<&FileTab> {
self.tabs.get(self.active_tab_index)
}
fn active_tab_mut(&mut self) -> Option<&mut FileTab> {
self.tabs.get_mut(self.active_tab_index)
}
fn save_config(&self) {
let config = AppConfig {
search_history: self.search_panel_state.history.clone(),
case_sensitive: self.search_panel_state.case_sensitive,
use_regex: self.search_panel_state.use_regex,
last_search_query: self.search_panel_state.query.clone(),
highlight_rules: self.highlight_manager.rules.clone(),
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
config.save();
}
fn handle_open_file(&mut self) {
if let Some(new_tab) = open_file_dialog(&self.indexing_state) {
self.tabs.push(new_tab);
self.active_tab_index = self.tabs.len() - 1;
}
}
fn handle_close_tab(&mut self, index: usize) {
close_tab(&mut self.tabs, &mut self.active_tab_index, index);
}
fn handle_search(&mut self) {
if let Some(tab) = self.active_tab() {
// Validate date format if date range is enabled
if self.search_panel_state.date_range_enabled {
use chrono::NaiveDateTime;
// Validate format by trying to parse example dates
let test_date = "2025-01-01 00:00:00";
if NaiveDateTime::parse_from_str(test_date, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date format: {}", self.search_panel_state.date_format);
eprintln!("Expected format like: %Y-%m-%d %H:%M:%S");
return;
}
// Validate date_from and date_to can be parsed
if !self.search_panel_state.date_from.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_from, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_from format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
if !self.search_panel_state.date_to.is_empty() {
if NaiveDateTime::parse_from_str(&self.search_panel_state.date_to, &self.search_panel_state.date_format).is_err() {
eprintln!("Invalid date_to format. Expected format: {}", self.search_panel_state.date_format);
return;
}
}
}
let params = SearchParams {
query: self.search_panel_state.query.clone(),
case_sensitive: self.search_panel_state.case_sensitive,
use_regex: self.search_panel_state.use_regex,
date_range_enabled: self.search_panel_state.date_range_enabled,
date_format: self.search_panel_state.date_format.clone(),
date_from: self.search_panel_state.date_from.clone(),
date_to: self.search_panel_state.date_to.clone(),
};
let line_index = Arc::clone(&tab.line_index);
let file_path = tab.file_path.clone();
// Clear current results
if let Some(tab) = self.active_tab_mut() {
tab.filtered_lines.clear();
}
start_search(&self.search_state, params, line_index, file_path);
}
}
fn handle_clear_search(&mut self) {
self.search_panel_state.query.clear();
if let Some(tab) = self.active_tab_mut() {
tab.filtered_lines.clear();
}
}
fn handle_export_filtered(&mut self) {
use std::fs::File;
use std::io::Write;
use std::time::SystemTime;
if let Some(tab) = self.active_tab() {
if tab.filtered_lines.is_empty() {
return;
}
// Generate timestamped filename
let original_path = &tab.file_path;
let file_stem = original_path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("export");
let extension = original_path
.extension()
.and_then(|s| s.to_str())
.unwrap_or("log");
// Generate timestamp
let timestamp = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
// Create new filename: filename_timestamp.extension
let export_filename = format!("{}_{}.{}", file_stem, timestamp, extension);
let export_path = original_path.with_file_name(export_filename);
// Write filtered lines to file
match File::create(&export_path) {
Ok(mut file) => {
// Get the line index and file handle
let line_index = Arc::clone(&tab.line_index);
let file_path = tab.file_path.clone();
// Open the original file for reading
if let Ok(original_file) = File::open(&file_path) {
let mut reader = std::io::BufReader::new(original_file);
// Write each filtered line to the export file
for filtered_line in &tab.filtered_lines {
if let Some(content) = line_index.read_line(&mut reader, filtered_line.line_number) {
writeln!(file, "{}", content).ok();
}
}
// Open the exported file in a new tab
if let Ok(line_index) = crate::line_index::LineIndex::build(&export_path) {
if let Ok(exported_file) = File::open(&export_path) {
let new_tab = crate::file_tab::FileTab::new(
export_path,
line_index,
exported_file,
);
self.tabs.push(new_tab);
self.active_tab_index = self.tabs.len() - 1;
}
}
}
}
Err(e) => {
eprintln!("Failed to create export file: {}", e);
}
}
}
}
fn handle_keyboard_input(&mut self, ctx: &egui::Context) {
if let Some(tab) = self.active_tab_mut() {
tab.page_scroll_direction = ctx.input(|i| {
if i.key_pressed(egui::Key::PageDown) {
Some(1.0)
} else if i.key_pressed(egui::Key::PageUp) {
Some(-1.0)
} else {
None
}
});
}
}
fn update_search_results(&mut self) {
if let Some(filtered) = self.search_state.take_results() {
if let Some(tab) = self.active_tab_mut() {
tab.filtered_lines = filtered;
tab.filtered_scroll_offset = 0;
}
}
}
}
impl eframe::App for LogViewerApp {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.first_frame {
self.first_frame = false;
ctx.send_viewport_cmd(egui::ViewportCommand::Maximized(true));
}
// Update search results if available
self.update_search_results();
// Render top menu
let mut open_file_requested = false;
render_top_menu(
ctx,
&mut self.highlight_manager,
&self.indexing_state,
&mut open_file_requested,
);
if open_file_requested {
self.handle_open_file();
}
// Render highlight editor
let highlight_config_changed = render_highlight_editor(ctx, &mut self.highlight_manager);
if highlight_config_changed {
self.save_config();
}
// Render tabs
let mut close_tab_index = None;
render_tabs_panel(
ctx,
&self.tabs,
&mut self.active_tab_index,
&mut close_tab_index,
);
if let Some(index) = close_tab_index {
self.handle_close_tab(index);
}
// Check for Ctrl+F keyboard shortcut
let ctrl_f_pressed = ctx.input(|i| {
i.modifiers.ctrl && i.key_pressed(egui::Key::F)
});
// Check for Ctrl+Enter to execute search globally
let ctrl_enter_pressed = ctx.input(|i| {
i.modifiers.ctrl && i.key_pressed(egui::Key::Enter)
});
// Render search panel
let match_count = self.active_tab().map(|t| t.filtered_lines.len()).unwrap_or(0);
let search_actions = render_search_panel(
ctx,
&mut self.search_panel_state,
&self.search_state,
match_count,
ctrl_f_pressed,
);
if search_actions.execute_search || ctrl_enter_pressed {
add_to_history(
&mut self.search_panel_state.history,
&self.search_panel_state.query,
);
self.handle_search();
}
if search_actions.clear_search {
self.handle_clear_search();
}
if search_actions.config_changed {
self.save_config();
}
// Render filtered view with improved styling
// Show filtered view if there's a query OR if date range is enabled
let show_filtered = !self.search_panel_state.query.is_empty() || self.search_panel_state.date_range_enabled;
let highlight_rules = self.highlight_manager.rules.clone();
let mut export_clicked = false;
if show_filtered {
if let Some(tab) = self.active_tab_mut() {
if !tab.filtered_lines.is_empty() {
let palette = theme::get_palette();
egui::TopBottomPanel::bottom("filtered_view")
.resizable(true)
.default_height(250.0)
.min_height(150.0)
.show(ctx, |ui| {
// Styled header
let header_frame = egui::Frame::none()
.fill(palette.bg_secondary)
.inner_margin(egui::Margin::symmetric(12.0, 8.0));
header_frame.show(ui, |ui| {
ui.horizontal(|ui| {
let icon = egui::RichText::new("🔍")
.size(16.0);
ui.label(icon);
let title = egui::RichText::new("Search Results")
.size(16.0)
.color(palette.text_primary)
.strong();
ui.label(title);
let count = egui::RichText::new(format!("({} matches)", tab.filtered_lines.len()))
.size(14.0)
.color(palette.accent_bright);
ui.label(count);
ui.add_space(16.0);
// Export button
if ui.button("📤 Export").clicked() {
export_clicked = true;
}
});
});
ui.separator();
render_log_view(
ui,
LogViewContext {
tab,
highlight_rules: &highlight_rules,
show_all: false,
},
);
});
}
}
}
// Handle export after rendering the filtered view
if export_clicked {
self.handle_export_filtered();
}
// Handle keyboard input
self.handle_keyboard_input(ctx);
// Render main view with improved styling
egui::CentralPanel::default().show(ctx, |ui| {
let palette = theme::get_palette();
if let Some(tab) = self.active_tab_mut() {
// Styled header
let header_frame = egui::Frame::none()
.fill(palette.bg_secondary)
.inner_margin(egui::Margin::symmetric(12.0, 8.0));
header_frame.show(ui, |ui| {
ui.horizontal(|ui| {
let icon = egui::RichText::new("📄")
.size(16.0);
ui.label(icon);
let title = egui::RichText::new("Log View")
.size(16.0)
.color(palette.text_primary)
.strong();
ui.label(title);
// Show filename
if let Some(filename) = tab.file_path.file_name() {
ui.add_space(8.0);
let filename_text = egui::RichText::new(format!("{}", filename.to_string_lossy()))
.size(14.0)
.color(palette.text_secondary);
ui.label(filename_text);
}
// Show line count
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let line_count = egui::RichText::new(format!("{} lines", tab.line_index.total_lines))
.size(13.0)
.color(palette.text_muted);
ui.label(line_count);
});
});
});
ui.separator();
render_log_view(
ui,
LogViewContext {
tab,
highlight_rules: &highlight_rules,
show_all: true,
},
);
} else {
ui.centered_and_justified(|ui| {
ui.vertical_centered(|ui| {
ui.add_space(40.0);
let icon = egui::RichText::new("📂")
.size(48.0);
ui.label(icon);
ui.add_space(16.0);
let text = egui::RichText::new("No file loaded")
.size(18.0)
.color(palette.text_secondary);
ui.label(text);
ui.add_space(8.0);
let hint = egui::RichText::new("Click 'Open File' in the menu to get started")
.size(14.0)
.color(palette.text_muted);
ui.label(hint);
});
});
}
});
// Only request repaint if there are ongoing background operations
if self.indexing_state.is_indexing() || self.search_state.is_searching() {
ctx.request_repaint();
}
}
}
+1 -1
View File
@@ -42,7 +42,7 @@ pub fn open_file_dialog(indexing_state: &IndexingState) -> Option<FileTab> {
open_file(path, indexing_state) 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(); indexing_state.start_indexing();
// Background indexing for progress indication // Background indexing for progress indication
-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);
});
});
}
-1
View File
@@ -3,7 +3,6 @@ pub mod log_view;
pub mod search_panel; pub mod search_panel;
pub mod tabs_panel; pub mod tabs_panel;
pub mod top_menu; pub mod top_menu;
pub mod log_panel;
pub use highlight_editor::render_highlight_editor; pub use highlight_editor::render_highlight_editor;
pub use log_view::{render_log_view, LogViewContext}; pub use log_view::{render_log_view, LogViewContext};
+3 -12
View File
@@ -3,23 +3,16 @@ use eframe::egui;
use crate::highlight::HighlightManager; use crate::highlight::HighlightManager;
use crate::tab_manager::IndexingState; use crate::tab_manager::IndexingState;
pub struct TopMenuActions{
pub open_file_requested: bool,
}
pub fn render_top_menu( pub fn render_top_menu(
ctx: &egui::Context, ctx: &egui::Context,
highlight_manager: &mut HighlightManager, highlight_manager: &mut HighlightManager,
indexing_state: &IndexingState, indexing_state: &IndexingState,
on_open_file: &mut bool,
) -> TopMenuActions { ) {
let mut top_menu_actions = TopMenuActions{open_file_requested: false};
egui::TopBottomPanel::top("top_panel").show(ctx, |ui| { egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
ui.horizontal(|ui| { ui.horizontal(|ui| {
if ui.button("📂 Open File").clicked() { if ui.button("📂 Open File").clicked() {
top_menu_actions.open_file_requested = true; *on_open_file = true;
} }
if ui.button("🎨 Highlights").clicked() { if ui.button("🎨 Highlights").clicked() {
@@ -53,6 +46,4 @@ pub fn render_top_menu(
} }
}); });
}); });
top_menu_actions
} }