Compare commits
5
Commits
main
..
52d1df4c2f
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
52d1df4c2f | ||
|
|
bc197d4080 | ||
|
|
f145ac521e | ||
|
|
0dd5962b9d | ||
|
|
a53ced9160 |
@@ -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
|
||||
@@ -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
@@ -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
@@ -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
|
||||
|
||||
@@ -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.
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
Executable
+92
@@ -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>"
|
||||
@@ -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"
|
||||
Executable
+161
@@ -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 +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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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"
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
@@ -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"
|
||||
@@ -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,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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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(¶ms.date_from, ¶ms.date_format).ok()?;
|
||||
let date_to = NaiveDateTime::parse_from_str(¶ms.date_to, ¶ms.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, ¶ms.date_format, true)?;
|
||||
|
||||
// Binary search for last line <= date_to
|
||||
let end_line = binary_search_date(line_index, &mut file_handle, &date_to, ¶ms.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, ®ex_matcher)
|
||||
};
|
||||
|
||||
if should_include {
|
||||
chunk_results.push(FilteredLine { line_number });
|
||||
if params.matches_line(&content, ®ex_matcher) {
|
||||
chunk_results.push(FilteredLine {
|
||||
line_number,
|
||||
content,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -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
@@ -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
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
@@ -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;
|
||||
|
||||
@@ -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};
|
||||
|
||||
+8
-82
@@ -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,38 +27,17 @@ pub fn render_search_panel(
|
||||
config_changed: false,
|
||||
};
|
||||
|
||||
let palette = theme::get_palette();
|
||||
|
||||
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| {
|
||||
egui::TopBottomPanel::bottom("search_panel").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);
|
||||
ui.label("🔍 Filter:");
|
||||
|
||||
ui.add_space(4.0);
|
||||
|
||||
// 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...")
|
||||
let text_edit_width = 200.0;
|
||||
let text_response = ui.add_sized(
|
||||
[text_edit_width, 20.0],
|
||||
egui::TextEdit::singleline(&mut state.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));
|
||||
|
||||
@@ -72,77 +45,30 @@ pub fn render_search_panel(
|
||||
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 {
|
||||
if case_changed || regex_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);
|
||||
ui.label(format!("({} matches)", match_count));
|
||||
}
|
||||
});
|
||||
|
||||
// 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")
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Progress bar
|
||||
if search_state.is_searching() {
|
||||
ui.add_space(6.0);
|
||||
let progress = search_state.get_progress();
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
|
||||
+7
-73
@@ -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));
|
||||
|
||||
// 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()
|
||||
egui::RichText::new(format!("📄 {}", tab.filename())).strong()
|
||||
} else {
|
||||
egui::RichText::new(format!("📄 {}", display_name))
|
||||
egui::RichText::new(format!("📄 {}", tab.filename()))
|
||||
};
|
||||
|
||||
// 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() {
|
||||
if ui.selectable_label(is_active, button_text).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 let Some(tab) = tabs.get(*active_tab_index) {
|
||||
ui.label(format!("({} lines)", tab.line_index.total_lines));
|
||||
}
|
||||
});
|
||||
|
||||
// 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;
|
||||
|
||||
// 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;
|
||||
}
|
||||
} 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);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
+3
-12
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user