feat: Add a lot more features
CI / build-ubuntu (, default) (push) Has been cancelled
CI / build-ubuntu (--features networking, networking) (push) Has been cancelled
CI / build-ubuntu (--no-default-features, no-default-features) (push) Has been cancelled
CI / build-debian (push) Has been cancelled
CI / build-fedora (push) Has been cancelled
CI / build-arch (push) Has been cancelled
CI / build-opensuse (push) Has been cancelled
CI / build-ubuntu (, default) (push) Has been cancelled
CI / build-ubuntu (--features networking, networking) (push) Has been cancelled
CI / build-ubuntu (--no-default-features, no-default-features) (push) Has been cancelled
CI / build-debian (push) Has been cancelled
CI / build-fedora (push) Has been cancelled
CI / build-arch (push) Has been cancelled
CI / build-opensuse (push) Has been cancelled
- Add new system.rs module for system monitoring via D-Bus: - Battery status (UPower) - Media player controls and metadata (MPRIS) - WiFi/Bluetooth status (NetworkManager) - Keyboard layout tracking - Add media key support (XF86 Play/Pause/Next/Prev) - Add function keys F1-F3 for Suspend/Reboot/PowerOff - Replace deprecated timer.rs with async system management - Add GitHub Actions CI/CD workflows (CI + Release) - Update dependencies and add optional networking feature
This commit is contained in:
@@ -0,0 +1,108 @@
|
|||||||
|
name: CI
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master", "main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["master", "main"]
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-ubuntu:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- features: "default"
|
||||||
|
args: ""
|
||||||
|
- features: "networking"
|
||||||
|
args: "--features networking"
|
||||||
|
- features: "no-default-features"
|
||||||
|
args: "--no-default-features"
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev
|
||||||
|
- name: Build (${{ matrix.features }})
|
||||||
|
run: cargo build --verbose ${{ matrix.args }}
|
||||||
|
- name: Run tests
|
||||||
|
run: cargo test --verbose
|
||||||
|
|
||||||
|
build-debian:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: debian:stable-slim
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev \
|
||||||
|
cargo rustc
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
build-fedora:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: fedora:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
dnf install -y \
|
||||||
|
llvm clang libclang-devel \
|
||||||
|
pkg-config \
|
||||||
|
glib2-devel cairo-devel pango-devel atk-devel \
|
||||||
|
pam-devel \
|
||||||
|
wayland-devel libxkbcommon-devel \
|
||||||
|
cargo rust
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
build-arch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: archlinux:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
pacman -Sy --noconfirm \
|
||||||
|
llvm clang pkgconf \
|
||||||
|
glib2 cairo pango atk \
|
||||||
|
pam \
|
||||||
|
wayland libxkbcommon \
|
||||||
|
rust cargo
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
|
build-opensuse:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: opensuse/tumbleweed:latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
zypper install -y \
|
||||||
|
llvm clang \
|
||||||
|
pkg-config \
|
||||||
|
glib2-devel cairo-devel pango-devel atk-devel \
|
||||||
|
pam-devel \
|
||||||
|
wayland-devel libxkbcommon-devel \
|
||||||
|
rust cargo
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --verbose
|
||||||
|
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-ubuntu:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- variant: "default"
|
||||||
|
args: "--no-default-features"
|
||||||
|
- variant: "networking"
|
||||||
|
args: "--features networking"
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.variant }}
|
||||||
|
run: cargo build --release ${{ matrix.args }}
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rustlock-${{ matrix.variant }}-ubuntu
|
||||||
|
path: target/release/rustlock
|
||||||
|
|
||||||
|
build-debian:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: debian:stable-slim
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libpam0g-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev \
|
||||||
|
cargo rust
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rustlock-default-debian
|
||||||
|
path: target/release/rustlock
|
||||||
|
|
||||||
|
build-fedora:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: fedora:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
dnf install -y \
|
||||||
|
llvm clang libclang-devel \
|
||||||
|
pkg-config \
|
||||||
|
glib2-devel cairo-devel pango-devel atk-devel \
|
||||||
|
pam-devel \
|
||||||
|
wayland-devel libxkbcommon-devel \
|
||||||
|
rust cargo
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rustlock-default-fedora
|
||||||
|
path: target/release/rustlock
|
||||||
|
|
||||||
|
build-arch:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
container: archlinux:latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install system dependencies
|
||||||
|
run: |
|
||||||
|
pacman -Sy --noconfirm \
|
||||||
|
llvm clang pkgconf \
|
||||||
|
glib2 cairo pango atk \
|
||||||
|
pam \
|
||||||
|
wayland libxkbcommon \
|
||||||
|
rust cargo
|
||||||
|
|
||||||
|
- name: Build
|
||||||
|
run: cargo build --release
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rustlock-default-arch
|
||||||
|
path: target/release/rustlock
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [build-ubuntu, build-debian, build-fedora, build-arch]
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Download all artifacts
|
||||||
|
uses: actions/download-artifact@v4
|
||||||
|
with:
|
||||||
|
path: artifacts
|
||||||
|
|
||||||
|
- name: Determine tag
|
||||||
|
id: tag
|
||||||
|
run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Create Release
|
||||||
|
uses: softprops/action-gh-release@v1
|
||||||
|
with:
|
||||||
|
tag_name: ${{ github.ref_name }}
|
||||||
|
name: RustLock v${{ steps.tag.outputs.VERSION }}
|
||||||
|
draft: true
|
||||||
|
prerelease: ${{ contains(github.ref_name, 'alpha') || contains(github.ref_name, 'beta') }}
|
||||||
|
files: |
|
||||||
|
artifacts/rustlock-default-ubuntu/rustlock
|
||||||
|
artifacts/rustlock-networking-ubuntu/rustlock
|
||||||
|
artifacts/rustlock-default-debian/rustlock
|
||||||
|
artifacts/rustlock-default-fedora/rustlock
|
||||||
|
artifacts/rustlock-default-arch/rustlock
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
Generated
+1971
-596
File diff suppressed because it is too large
Load Diff
+39
-15
@@ -4,23 +4,47 @@ version = "0.1.0"
|
|||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
|
smithay-client-toolkit = { version = "0.19", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||||
wayland-client = "0.31"
|
wayland-client = { version = "0.31" }
|
||||||
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
wayland-protocols = { version = "0.32", features = ["client"] }
|
||||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||||
anyhow = "1.0"
|
anyhow = "1.0"
|
||||||
futures = "0.3"
|
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||||
cairo-rs = { version = "0.20", features = ["png"] }
|
cairo-rs = { version = "0.20", default-features = false, features = ["png"] }
|
||||||
image = "0.25"
|
gdk-pixbuf = { version = "0.20", default-features = false, features = ["v2_40"] }
|
||||||
fastblur = "0.1"
|
gio = { version = "0.20" }
|
||||||
xkbcommon = "0.7"
|
pangocairo = { version = "0.20" }
|
||||||
pam-client = "0.5"
|
clap = { version = "4.4", default-features = false, features = ["derive", "std", "help", "usage", "error-context"] }
|
||||||
secstr = "0.5"
|
toml = { version = "1.0", default-features = false, features = ["parse", "display", "serde"] }
|
||||||
clap = { version = "4.5", features = ["derive"] }
|
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||||
toml = "1.0"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
|
||||||
zeroize = "1.7"
|
zeroize = "1.7"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
env_logger = "0.11"
|
env_logger = { version = "0.11", default-features = false, features = ["color", "humantime"] }
|
||||||
chrono = "0.4"
|
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||||
users = "0.11"
|
users = "0.11"
|
||||||
|
zbus = { version = "3.15", default-features = false, features = ["tokio"] }
|
||||||
|
mpris = "2.0"
|
||||||
|
upower_dbus = "0.3"
|
||||||
|
tokio = { version = "1.0", default-features = false, features = ["rt", "rt-multi-thread", "macros", "time", "sync"] }
|
||||||
|
num-traits = { version = "0.2" }
|
||||||
|
rand = { version = "0.8" }
|
||||||
|
reqwest = { version = "0.11", default-features = false, features = ["blocking", "rustls-tls"], optional = true }
|
||||||
|
image = { version = "0.25", default-features = false, features = ["png", "jpeg"] }
|
||||||
|
fastblur = "0.1"
|
||||||
|
pam-client = "0.5"
|
||||||
|
thiserror = "1.0"
|
||||||
|
bytemuck = "1.14"
|
||||||
|
calloop = "0.13"
|
||||||
|
xkbcommon = "0.7"
|
||||||
|
calloop-wayland-source = "0.3"
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = ["networking"]
|
||||||
|
networking = ["dep:reqwest", "tokio/net"]
|
||||||
|
|
||||||
|
[profile.release]
|
||||||
|
opt-level = "z" # Optimize for size
|
||||||
|
lto = true # Enable Link Time Optimization
|
||||||
|
codegen-units = 1 # Reduce parallel code gen to allow better size optimization
|
||||||
|
panic = "abort" # Remove heavy stack unwinding code
|
||||||
|
strip = true # Automatically strip symbols and debug info
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
[](https://github.com/yourusername/rustlock/blob/main/LICENSE)
|
[](https://github.com/yourusername/rustlock/blob/main/LICENSE)
|
||||||
[](https://github.com/yourusername/rustlock/releases)
|
[](https://github.com/yourusername/rustlock/releases)
|
||||||
[](https://github.com/yourusername/rustlock/commits/main)
|
|
||||||
|
|
||||||
A high-performance Wayland screen locker written in Rust, inspired by `swaylock-effects`.
|
A high-performance Wayland screen locker written in Rust, inspired by `swaylock-effects`.
|
||||||
|
|
||||||
@@ -10,26 +9,40 @@ A high-performance Wayland screen locker written in Rust, inspired by `swaylock-
|
|||||||
|
|
||||||
## ✨ Features
|
## ✨ Features
|
||||||
|
|
||||||
- ⚡ **Performance**: Written in safe Rust with minimal dependencies
|
- ⚡ **Performance**: Written in safe Rust, optimized binary size (~2.4MB without networking, ~4MB with)
|
||||||
- 🎨 **Visual Effects**:
|
- 🎨 **Visual Effects**:
|
||||||
- Gaussian blur (configurable radius and passes)
|
- Gaussian blur (configurable radius and passes)
|
||||||
- Vignette effect (configurable base and factor)
|
- Vignette effect (configurable base and factor)
|
||||||
|
- Pixelate, Swirl, and Melting effects
|
||||||
- Smooth fade-in animation
|
- Smooth fade-in animation
|
||||||
- 🔐 **Password Indicator**:
|
- 🔐 **Password Indicator**:
|
||||||
- Circular ring with configurable radius and thickness
|
- Circular ring with configurable radius and thickness
|
||||||
- Dynamic key highlight segments that rotate with each keystroke
|
- Dynamic key highlight segments that rotate with each keystroke
|
||||||
- Customizable colors (ring, inside, separator, highlight)
|
- Caps lock indicator
|
||||||
- 🕐 **Information Display**:
|
- 🕐 **Information Display**:
|
||||||
- Centered clock (HH:MM format)
|
- Centered clock (HH:MM format)
|
||||||
- Full date
|
- Full date
|
||||||
- System uptime
|
- System uptime
|
||||||
|
- 📻 **Media & System Status** (optional):
|
||||||
|
- MPRIS media player integration with album art
|
||||||
|
- Battery percentage and charging status
|
||||||
|
- WiFi SSID and signal strength
|
||||||
|
- Bluetooth connected devices
|
||||||
|
- Keyboard layout indicator
|
||||||
|
- 🔑 **Session Management**:
|
||||||
|
- F1: Suspend
|
||||||
|
- F2: Reboot
|
||||||
|
- F3: Power Off
|
||||||
- 📸 **Screenshot Support**:
|
- 📸 **Screenshot Support**:
|
||||||
- Captures desktop background before locking
|
- Captures desktop background before locking
|
||||||
- Applies visual effects to background
|
- Custom background image support
|
||||||
- 🔑 **Authentication**:
|
- 🔐 **Authentication**:
|
||||||
- PAM-based authentication
|
- PAM-based authentication
|
||||||
- Configurable grace period (any key press within N seconds unlocks without password)
|
- Configurable grace period (any key press within N seconds unlocks without password)
|
||||||
- 📝 **Logging**: Verbose debug logs written to `~/.rustlock.log`
|
- 🎯 **Customization**:
|
||||||
|
- Custom icons for WiFi, Bluetooth, Battery
|
||||||
|
- Theme presets (dark, light, nord, dracula)
|
||||||
|
- Configuration via config file or CLI
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -61,67 +74,119 @@ rustlock \
|
|||||||
--fade-in 0.2
|
--fade-in 0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
|
### Session Controls
|
||||||
|
|
||||||
|
When locked, use function keys to control the system:
|
||||||
|
- **F1**: Suspend to RAM
|
||||||
|
- **F2**: Reboot
|
||||||
|
- **F3**: Power Off
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ⚙️ Configuration
|
## ⚙️ Configuration
|
||||||
|
|
||||||
Options can be provided via command line or a configuration file at `~/.config/rustlock/config.toml`. CLI arguments take precedence.
|
Options can be provided via command line or a configuration file at `~/.config/rustlock/config.toml`. CLI arguments take precedence over config file, which takes precedence over theme defaults.
|
||||||
|
|
||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Description |
|
| Option | Description |
|
||||||
|--------|-------------|
|
|--------|-------------|
|
||||||
|
| **General** | |
|
||||||
| `--screenshots` | Capture desktop background before locking |
|
| `--screenshots` | Capture desktop background before locking |
|
||||||
|
| `--image <PATH>` | Use custom background image instead of screenshot |
|
||||||
| `--clock` | Display centered clock and date |
|
| `--clock` | Display centered clock and date |
|
||||||
| `--indicator` | Show password entry ring (default: true) |
|
| `--indicator` | Show password entry ring (default: true) |
|
||||||
| `--indicator-radius <N>` | Ring radius in pixels (default: 100) |
|
| `--indicator-radius <N>` | Ring radius in pixels (default: 100) |
|
||||||
| `--indicator-thickness <N>` | Ring thickness in pixels (default: 7) |
|
| `--indicator-thickness <N>` | Ring thickness in pixels (default: 7) |
|
||||||
|
| **Effects** | |
|
||||||
| `--effect-blur <R>x<P>` | Gaussian blur: radius x passes (e.g., `7x5`) |
|
| `--effect-blur <R>x<P>` | Gaussian blur: radius x passes (e.g., `7x5`) |
|
||||||
|
| `--effect-pixelate` | Pixelate effect |
|
||||||
|
| `--effect-swirl` | Swirl distortion effect |
|
||||||
|
| `--effect-melting` | Melting distortion effect |
|
||||||
| `--effect-vignette <B>:<F>` | Vignette: base:factor (e.g., `0.5:0.5`) |
|
| `--effect-vignette <B>:<F>` | Vignette: base:factor (e.g., `0.5:0.5`) |
|
||||||
|
| **Colors** | |
|
||||||
| `--ring-color <RRGGBB[AA]>` | Outer ring color (hex, optional alpha) |
|
| `--ring-color <RRGGBB[AA]>` | Outer ring color (hex, optional alpha) |
|
||||||
| `--key-hl-color <RRGGBB[AA]>` | Key highlight segment color |
|
| `--key-hl-color <RRGGBB[AA]>` | Key highlight segment color |
|
||||||
| `--line-color <RRGGBB[AA]>` | Separator line color |
|
| `--line-color <RRGGBB[AA]>` | Separator line color |
|
||||||
| `--inside-color <RRGGBB[AA]>` | Inner circle color |
|
| `--inside-color <RRGGBB[AA]>` | Inner circle color |
|
||||||
| `--separator-color <RRGGBB[AA]>` | Ring separator color |
|
| `--separator-color <RRGGBB[AA]>` | Ring separator color |
|
||||||
|
| **Display Options** | |
|
||||||
|
| `--show-media` | Show MPRIS media info (default: true) |
|
||||||
|
| `--show-battery` | Show battery status (default: true) |
|
||||||
|
| `--show-network` | Show WiFi status (default: true) |
|
||||||
|
| `--show-bluetooth` | Show Bluetooth status (default: true) |
|
||||||
|
| `--show-keyboard-layout` | Show keyboard layout indicator (default: true) |
|
||||||
|
| `--show-album-art` | Show album art (default: true) |
|
||||||
|
| **Custom Icons** | |
|
||||||
|
| `--wifi-icon <PATH>` | Custom WiFi icon (PNG/SVG) |
|
||||||
|
| `--bluetooth-icon <PATH>` | Custom Bluetooth icon (PNG/SVG) |
|
||||||
|
| `--battery-icon <PATH>` | Custom battery icon (PNG/SVG) |
|
||||||
|
| **Other** | |
|
||||||
| `--grace <SECONDS>` | Grace period in seconds (default: 2) |
|
| `--grace <SECONDS>` | Grace period in seconds (default: 2) |
|
||||||
| `--fade-in <SECONDS>` | Fade-in animation duration (default: 0.2) |
|
| `--fade-in <SECONDS>` | Fade-in animation duration (default: 0.2) |
|
||||||
| `--pam-service <NAME>` | PAM service name (default: "rustlock") |
|
| `--pam-service <NAME>` | PAM service name (default: "rustlock") |
|
||||||
| `--config <PATH>` | Path to config file |
|
| `--config <PATH>` | Path to config file |
|
||||||
|
| `--theme <NAME>` | Theme preset: dark, light, nord, dracula |
|
||||||
| `--debug` | Enable debug logging |
|
| `--debug` | Enable debug logging |
|
||||||
| `--log-file` | Write logs to `~/.rustlock.log` |
|
| `--log-file` | Write logs to `~/.rustlock.log` |
|
||||||
| `--temp-screenshot` | Enable peek feature (press 'p' to temporarily show background) |
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 📦 Installation
|
## 📦 Installation
|
||||||
|
|
||||||
|
### Using Nix (Recommended)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
nix-shell -p rustlock
|
||||||
|
```
|
||||||
|
|
||||||
|
Or with flakes:
|
||||||
|
```bash
|
||||||
|
nix run github:yourusername/rustlock
|
||||||
|
```
|
||||||
|
|
||||||
|
### From Source
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cargo build --release
|
cargo build --release
|
||||||
```
|
```
|
||||||
|
|
||||||
The binary will be available at `target/release/rustlock`.
|
The binary will be available at `target/release/rustlock`.
|
||||||
|
|
||||||
|
### Build Options
|
||||||
|
|
||||||
|
- **With networking** (default): Includes reqwest for album art fetching
|
||||||
|
```bash
|
||||||
|
cargo build --release --features networking
|
||||||
|
```
|
||||||
|
|
||||||
|
- **Without networking**: Smaller binary (~2.4MB)
|
||||||
|
```bash
|
||||||
|
cargo build --release --no-default-features
|
||||||
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## ✅ Completed
|
## ✅ Completed
|
||||||
|
|
||||||
- [x] PAM-based authentication
|
- [x] PAM-based authentication
|
||||||
- [x] Grace period (any key unlocks within N seconds)
|
- [x] Grace period (any key unlocks within N seconds)
|
||||||
- [x] Screenshot capture with blur/vignette effects
|
- [x] Screenshot capture with blur/vignette/pixelate/swirl/melting effects
|
||||||
- [x] Configuration file support (`~/.config/rustlock/config.toml`)
|
- [x] Configuration file support (`~/.config/rustlock/config.toml`) with schema validation
|
||||||
- [x] Debug logging to `~/.rustlock.log`
|
- [x] Debug logging to `~/.rustlock.log`
|
||||||
- [x] Clock and date display
|
- [x] Clock and date display
|
||||||
- [x] Password indicator ring with rotating highlights
|
- [x] Password indicator ring with rotating highlights
|
||||||
|
- [x] Dynamic screen resolution detection
|
||||||
## 🚧 TODO
|
- [x] Full multi-monitor support with different resolutions
|
||||||
|
- [x] Theme/profile support with presets (dark, light, nord, dracula)
|
||||||
- [ ] Don't hardcode screen resolution (detect dynamically)
|
- [x] Wayland protocol stability fixes
|
||||||
- [ ] Add multi-monitor support (handle multiple outputs)
|
- [x] Media control integration (MPRIS support with Album Art)
|
||||||
- [ ] Automatically install PAM configuration file
|
- [x] Battery, WiFi, and Bluetooth status indicators
|
||||||
- [ ] Configuration file schema validation
|
- [x] Custom background image support
|
||||||
- [ ] Additional visual effects (pixelate, swirl, etc.)
|
- [x] Custom icons for status indicators
|
||||||
- [ ] Theme/profile support with presets
|
- [x] Keyboard layout indicator
|
||||||
- [ ] Wayland protocol stability updates
|
- [x] Session management (F1-F3 keys)
|
||||||
|
- [x] Caps lock indicator
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,10 @@ pkgs.rustPlatform.buildRustPackage {
|
|||||||
cairo
|
cairo
|
||||||
pam
|
pam
|
||||||
gdk-pixbuf
|
gdk-pixbuf
|
||||||
|
librsvg
|
||||||
|
pango
|
||||||
libxkbcommon
|
libxkbcommon
|
||||||
|
dbus
|
||||||
];
|
];
|
||||||
|
|
||||||
nativeBuildInputs = [
|
nativeBuildInputs = [
|
||||||
|
|||||||
+24
-12
@@ -35,7 +35,7 @@ impl pam_client::ConversationHandler for LockConversation {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channel::Channel<bool>) {
|
pub fn create_and_run_auth_loop() -> Option<(channel::Sender<Zeroizing<String>>, channel::Channel<bool>)> {
|
||||||
let username = get_current_username()
|
let username = get_current_username()
|
||||||
.expect("Failed to get username")
|
.expect("Failed to get username")
|
||||||
.to_str()
|
.to_str()
|
||||||
@@ -43,9 +43,16 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channe
|
|||||||
.to_string();
|
.to_string();
|
||||||
|
|
||||||
let conversation = LockConversation { password: None };
|
let conversation = LockConversation { password: None };
|
||||||
let _context = Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
|
match Context::new(SERVICE_NAME, Some(username.as_str()), conversation) {
|
||||||
.expect("Failed to initialize PAM context");
|
Ok(_) => {
|
||||||
debug!("Prepared to authenticate user '{}'", username);
|
debug!("Prepared to authenticate user '{}'", username);
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Failed to initialize PAM context: {:?}", err);
|
||||||
|
error!("Ensure that the PAM service '{}' is correctly configured.", SERVICE_NAME);
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let (auth_req_send, auth_req_recv) = channel::channel::<Zeroizing<String>>();
|
let (auth_req_send, auth_req_recv) = channel::channel::<Zeroizing<String>>();
|
||||||
let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
|
let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
|
||||||
@@ -59,15 +66,20 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channe
|
|||||||
let conversation = LockConversation {
|
let conversation = LockConversation {
|
||||||
password: Some(password),
|
password: Some(password),
|
||||||
};
|
};
|
||||||
let mut context =
|
match Context::new(SERVICE_NAME, Some(username.as_str()), conversation) {
|
||||||
Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
|
Ok(mut context) => {
|
||||||
.expect("Failed to initialize PAM context");
|
match context.authenticate(Flag::NONE) {
|
||||||
match context.authenticate(Flag::NONE) {
|
Ok(()) => {
|
||||||
Ok(()) => {
|
auth_res_send.send(true).unwrap();
|
||||||
auth_res_send.send(true).unwrap();
|
}
|
||||||
|
Err(err) => {
|
||||||
|
error!("Pam authenticate failed with {:?}", err);
|
||||||
|
auth_res_send.send(false).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!("Pam authenticate failed with {:?}", err);
|
error!("Failed to re-initialize PAM context: {:?}", err);
|
||||||
auth_res_send.send(false).unwrap();
|
auth_res_send.send(false).unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,5 +93,5 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channe
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
(auth_req_send, auth_res_recv)
|
Some((auth_req_send, auth_res_recv))
|
||||||
}
|
}
|
||||||
|
|||||||
+189
-11
@@ -6,13 +6,13 @@ use std::path::PathBuf;
|
|||||||
#[derive(Parser, Debug, Clone, Serialize, Deserialize)]
|
#[derive(Parser, Debug, Clone, Serialize, Deserialize)]
|
||||||
#[command(author, version, about, long_about = None)]
|
#[command(author, version, about, long_about = None)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
#[arg(long)]
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||||
pub screenshots: bool,
|
pub screenshots: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||||
pub clock: bool,
|
pub clock: bool,
|
||||||
|
|
||||||
#[arg(long, default_value = "true")]
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
pub indicator: bool,
|
pub indicator: bool,
|
||||||
|
|
||||||
#[arg(long, default_value = "100")]
|
#[arg(long, default_value = "100")]
|
||||||
@@ -27,6 +27,15 @@ pub struct Config {
|
|||||||
#[arg(long, value_parser = util::parse_vignette_effect)]
|
#[arg(long, value_parser = util::parse_vignette_effect)]
|
||||||
pub effect_vignette: Option<(f32, f32)>,
|
pub effect_vignette: Option<(f32, f32)>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub effect_pixelate: Option<u32>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub effect_swirl: Option<f32>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub effect_melting: Option<f32>,
|
||||||
|
|
||||||
#[arg(long, default_value = "785412", value_parser = util::parse_hex_color)]
|
#[arg(long, default_value = "785412", value_parser = util::parse_hex_color)]
|
||||||
pub ring_color: (f64, f64, f64, f64),
|
pub ring_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
@@ -61,17 +70,65 @@ pub struct Config {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub log_file: bool,
|
pub log_file: bool,
|
||||||
|
|
||||||
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_media: bool,
|
||||||
|
|
||||||
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_battery: bool,
|
||||||
|
|
||||||
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_network: bool,
|
||||||
|
|
||||||
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_bluetooth: bool,
|
||||||
|
|
||||||
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_album_art: bool,
|
||||||
|
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub temp_screenshot: bool,
|
pub image: Option<PathBuf>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub wifi_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub bluetooth_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub battery_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub media_prev_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub media_stop_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub media_play_icon: Option<String>,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
pub media_next_icon: Option<String>,
|
||||||
|
|
||||||
|
/// Apply a pre-defined theme preset
|
||||||
|
#[arg(long)]
|
||||||
|
pub theme: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
pub fn load() -> Self {
|
pub fn load() -> Self {
|
||||||
let cli_config = Config::parse();
|
use clap::CommandFactory;
|
||||||
|
|
||||||
|
let mut config = Config::parse();
|
||||||
|
let cmd = Config::command();
|
||||||
|
let matches = cmd.get_matches();
|
||||||
|
|
||||||
// Use path from CLI if provided, otherwise default
|
// Helper to check if a value was explicitly set on command line
|
||||||
let config_path = cli_config.config.clone().unwrap_or_else(|| {
|
let is_cli = |key: &str| {
|
||||||
|
matches.value_source(key) == Some(clap::parser::ValueSource::CommandLine)
|
||||||
|
};
|
||||||
|
|
||||||
|
// 1. Config file layer (overrides defaults and themes)
|
||||||
|
let config_path = config.config.clone().unwrap_or_else(|| {
|
||||||
let mut path = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default());
|
let mut path = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default());
|
||||||
path.push(".config/rustlock/config.toml");
|
path.push(".config/rustlock/config.toml");
|
||||||
path
|
path
|
||||||
@@ -79,12 +136,133 @@ impl Config {
|
|||||||
|
|
||||||
if config_path.exists() {
|
if config_path.exists() {
|
||||||
if let Ok(file_content) = std::fs::read_to_string(&config_path) {
|
if let Ok(file_content) = std::fs::read_to_string(&config_path) {
|
||||||
if let Ok(_file_config) = toml::from_str::<Config>(&file_content) {
|
if let Ok(table) = toml::from_str::<toml::Table>(&file_content) {
|
||||||
return cli_config;
|
log::debug!("Loaded configuration from {:?}", config_path);
|
||||||
|
|
||||||
|
let merge_bool = |val: &mut bool, key: &str| {
|
||||||
|
if !is_cli(key) {
|
||||||
|
if let Some(toml::Value::Boolean(b)) = table.get(key) {
|
||||||
|
*val = *b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let merge_u32 = |val: &mut u32, key: &str| {
|
||||||
|
if !is_cli(key) {
|
||||||
|
if let Some(toml::Value::Integer(i)) = table.get(key) {
|
||||||
|
*val = *i as u32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let merge_f32 = |val: &mut f32, key: &str| {
|
||||||
|
if !is_cli(key) {
|
||||||
|
if let Some(toml::Value::Float(f)) = table.get(key) {
|
||||||
|
*val = *f as f32;
|
||||||
|
} else if let Some(toml::Value::Integer(i)) = table.get(key) {
|
||||||
|
*val = *i as f32;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let merge_string = |val: &mut String, key: &str| {
|
||||||
|
if !is_cli(key) {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get(key) {
|
||||||
|
*val = s.clone();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
merge_bool(&mut config.screenshots, "screenshots");
|
||||||
|
merge_bool(&mut config.clock, "clock");
|
||||||
|
merge_bool(&mut config.indicator, "indicator");
|
||||||
|
merge_u32(&mut config.indicator_radius, "indicator_radius");
|
||||||
|
merge_u32(&mut config.indicator_thickness, "indicator_thickness");
|
||||||
|
merge_f32(&mut config.grace, "grace");
|
||||||
|
merge_f32(&mut config.fade_in, "fade_in");
|
||||||
|
merge_string(&mut config.pam_service, "pam_service");
|
||||||
|
merge_bool(&mut config.show_media, "show_media");
|
||||||
|
merge_bool(&mut config.show_battery, "show_battery");
|
||||||
|
merge_bool(&mut config.show_network, "show_network");
|
||||||
|
merge_bool(&mut config.show_bluetooth, "show_bluetooth");
|
||||||
|
merge_bool(&mut config.show_album_art, "show_album_art");
|
||||||
|
|
||||||
|
if !is_cli("image") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("image") {
|
||||||
|
config.image = Some(std::path::PathBuf::from(s));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("wifi_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("wifi_icon") {
|
||||||
|
config.wifi_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("bluetooth_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("bluetooth_icon") {
|
||||||
|
config.bluetooth_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("battery_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("battery_icon") {
|
||||||
|
config.battery_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("media_prev_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("media_prev_icon") {
|
||||||
|
config.media_prev_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("media_stop_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("media_stop_icon") {
|
||||||
|
config.media_stop_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("media_play_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("media_play_icon") {
|
||||||
|
config.media_play_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !is_cli("media_next_icon") {
|
||||||
|
if let Some(toml::Value::String(s)) = table.get("media_next_icon") {
|
||||||
|
config.media_next_icon = Some(s.clone());
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cli_config
|
// 2. Theme presets (applied to fields NOT set on CLI or in File)
|
||||||
|
if let Some(theme) = &config.theme {
|
||||||
|
match theme.as_str() {
|
||||||
|
"modern" => {
|
||||||
|
if config.effect_blur.is_none() && !is_cli("effect_blur") { config.effect_blur = Some((10, 3)); }
|
||||||
|
if config.effect_vignette.is_none() && !is_cli("effect_vignette") { config.effect_vignette = Some((0.5, 0.5)); }
|
||||||
|
if !is_cli("indicator_radius") { config.indicator_radius = 120; }
|
||||||
|
if !is_cli("ring_color") { config.ring_color = (0.2, 0.6, 0.8, 1.0); }
|
||||||
|
}
|
||||||
|
"pixel" => {
|
||||||
|
if config.effect_pixelate.is_none() && !is_cli("effect_pixelate") { config.effect_pixelate = Some(10); }
|
||||||
|
if !is_cli("indicator_radius") { config.indicator_radius = 80; }
|
||||||
|
if !is_cli("ring_color") { config.ring_color = (0.8, 0.2, 0.2, 1.0); }
|
||||||
|
}
|
||||||
|
"glass" => {
|
||||||
|
if config.effect_blur.is_none() && !is_cli("effect_blur") { config.effect_blur = Some((20, 5)); }
|
||||||
|
if !is_cli("inside_color") { config.inside_color = (1.0, 1.0, 1.0, 0.1); }
|
||||||
|
if !is_cli("ring_color") { config.ring_color = (1.0, 1.0, 1.0, 0.5); }
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
log::warn!("Unknown theme: {}", theme);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
config
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+21
-127
@@ -4,24 +4,18 @@ use zeroize::Zeroizing;
|
|||||||
pub struct InputHandler {
|
pub struct InputHandler {
|
||||||
password_buffer: Zeroizing<String>,
|
password_buffer: Zeroizing<String>,
|
||||||
cursor_position: usize,
|
cursor_position: usize,
|
||||||
config: crate::config::Config,
|
|
||||||
wrong_password_timer: Option<std::time::Instant>,
|
wrong_password_timer: Option<std::time::Instant>,
|
||||||
key_highlight_timer: Option<std::time::Instant>,
|
key_highlight_timer: Option<std::time::Instant>,
|
||||||
temp_screenshot_timer: Option<std::time::Instant>,
|
|
||||||
temp_screenshot_active: bool,
|
|
||||||
caps_lock: bool,
|
caps_lock: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl InputHandler {
|
impl InputHandler {
|
||||||
pub fn new(config: crate::config::Config) -> Self {
|
pub fn new(_config: crate::config::Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
password_buffer: Zeroizing::new(String::new()),
|
password_buffer: Zeroizing::new(String::new()),
|
||||||
cursor_position: 0,
|
cursor_position: 0,
|
||||||
config,
|
|
||||||
wrong_password_timer: None,
|
wrong_password_timer: None,
|
||||||
key_highlight_timer: None,
|
key_highlight_timer: None,
|
||||||
temp_screenshot_timer: None,
|
|
||||||
temp_screenshot_active: false,
|
|
||||||
caps_lock: false,
|
caps_lock: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -30,117 +24,46 @@ impl InputHandler {
|
|||||||
pub fn handle_key_event(
|
pub fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||||
state: wayland_client::protocol::wl_keyboard::KeyState,
|
utf8: Option<String>,
|
||||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
) -> InputAction {
|
) -> InputAction {
|
||||||
// Update Caps Lock state
|
// Update Caps Lock state
|
||||||
self.caps_lock = modifiers.caps_lock;
|
self.caps_lock = modifiers.caps_lock;
|
||||||
|
|
||||||
// Only process key press events
|
// Handle special keys first using keysym
|
||||||
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||||
return InputAction::None;
|
match keysym {
|
||||||
}
|
Keysym::BackSpace => {
|
||||||
|
|
||||||
// Convert keysym to character
|
|
||||||
let ch = self.keysym_to_char(keysym, modifiers);
|
|
||||||
|
|
||||||
match ch {
|
|
||||||
Some('\x08') | Some('\x7f') => {
|
|
||||||
// Backspace or Delete
|
|
||||||
if !self.password_buffer.is_empty() && self.cursor_position > 0 {
|
if !self.password_buffer.is_empty() && self.cursor_position > 0 {
|
||||||
self.cursor_position -= 1;
|
self.cursor_position -= 1;
|
||||||
self.password_buffer.remove(self.cursor_position);
|
self.password_buffer.remove(self.cursor_position);
|
||||||
}
|
}
|
||||||
InputAction::PasswordChanged
|
return InputAction::PasswordChanged;
|
||||||
}
|
}
|
||||||
Some('\r') | Some('\n') => {
|
Keysym::Return | Keysym::KP_Enter => {
|
||||||
// Enter key - submit password
|
|
||||||
let password = self.password_buffer.clone();
|
let password = self.password_buffer.clone();
|
||||||
self.password_buffer.clear();
|
self.password_buffer.clear();
|
||||||
self.cursor_position = 0;
|
self.cursor_position = 0;
|
||||||
InputAction::SubmitPassword(password)
|
return InputAction::SubmitPassword(password);
|
||||||
}
|
}
|
||||||
Some('\x1b') => {
|
Keysym::Escape => {
|
||||||
// Escape key - cancel
|
return InputAction::Cancel;
|
||||||
InputAction::Cancel
|
|
||||||
}
|
}
|
||||||
Some('p') | Some('P') if self.config.temp_screenshot => {
|
|
||||||
// 'p' key for temp screenshot peek
|
|
||||||
self.activate_temp_screenshot();
|
|
||||||
InputAction::TempScreenshot
|
|
||||||
}
|
|
||||||
Some(c) if c.is_ascii() && !c.is_control() => {
|
|
||||||
// Printable ASCII character
|
|
||||||
self.password_buffer.insert(self.cursor_position, c);
|
|
||||||
self.cursor_position += 1;
|
|
||||||
InputAction::PasswordChanged
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
// Other keys (function keys, arrows, etc.)
|
|
||||||
InputAction::None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Convert a keysym to a character, considering modifiers
|
|
||||||
fn keysym_to_char(
|
|
||||||
&self,
|
|
||||||
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
|
||||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
|
||||||
) -> Option<char> {
|
|
||||||
use smithay_client_toolkit::seat::keyboard::Keysym;
|
|
||||||
|
|
||||||
// Handle special keys first
|
|
||||||
match keysym {
|
|
||||||
Keysym::BackSpace => return Some('\x08'),
|
|
||||||
Keysym::Delete => return Some('\x7f'),
|
|
||||||
Keysym::Return => return Some('\r'),
|
|
||||||
Keysym::KP_Enter => return Some('\n'),
|
|
||||||
Keysym::Escape => return Some('\x1b'),
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert keysym to character
|
// Use the UTF-8 string provided by SCTK for character input
|
||||||
let keysym_value = keysym.raw();
|
if let Some(txt) = utf8 {
|
||||||
|
for c in txt.chars() {
|
||||||
// Basic ASCII conversion (simplified - real implementation would use xkbcommon)
|
if c.is_ascii() && !c.is_control() {
|
||||||
// This is a simplified mapping for demonstration
|
self.password_buffer.insert(self.cursor_position, c);
|
||||||
if keysym_value >= 0x20 && keysym_value <= 0x7e {
|
self.cursor_position += 1;
|
||||||
let mut ch = keysym_value as u8 as char;
|
}
|
||||||
|
|
||||||
// Apply shift modifier
|
|
||||||
if modifiers.shift {
|
|
||||||
ch = match ch {
|
|
||||||
'`' => '~',
|
|
||||||
'1' => '!',
|
|
||||||
'2' => '@',
|
|
||||||
'3' => '#',
|
|
||||||
'4' => '$',
|
|
||||||
'5' => '%',
|
|
||||||
'6' => '^',
|
|
||||||
'7' => '&',
|
|
||||||
'8' => '*',
|
|
||||||
'9' => '(',
|
|
||||||
'0' => ')',
|
|
||||||
'-' => '_',
|
|
||||||
'=' => '+',
|
|
||||||
'[' => '{',
|
|
||||||
']' => '}',
|
|
||||||
'\\' => '|',
|
|
||||||
';' => ':',
|
|
||||||
'\'' => '"',
|
|
||||||
',' => '<',
|
|
||||||
'.' => '>',
|
|
||||||
'/' => '?',
|
|
||||||
c if c.is_ascii_lowercase() => c.to_ascii_uppercase(),
|
|
||||||
_ => ch,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
return InputAction::PasswordChanged;
|
||||||
Some(ch)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InputAction::None
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the current password (for display purposes only - returns masked version)
|
/// Get the current password (for display purposes only - returns masked version)
|
||||||
@@ -178,34 +101,6 @@ impl InputHandler {
|
|||||||
|
|
||||||
/// Update timers (should be called periodically)
|
/// Update timers (should be called periodically)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
// Update temp screenshot state
|
|
||||||
self.update_temp_screenshot();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Activate temporary screenshot display (peek feature)
|
|
||||||
pub fn activate_temp_screenshot(&mut self) {
|
|
||||||
self.temp_screenshot_timer = Some(std::time::Instant::now());
|
|
||||||
self.temp_screenshot_active = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if temporary screenshot should be shown
|
|
||||||
pub fn should_show_temp_screenshot(&self) -> bool {
|
|
||||||
if let Some(timer) = self.temp_screenshot_timer {
|
|
||||||
let elapsed = timer.elapsed();
|
|
||||||
// Show for 2 seconds
|
|
||||||
if elapsed < std::time::Duration::from_secs(2) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
false
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update temp screenshot state (call periodically)
|
|
||||||
pub fn update_temp_screenshot(&mut self) {
|
|
||||||
if self.temp_screenshot_active && !self.should_show_temp_screenshot() {
|
|
||||||
self.temp_screenshot_active = false;
|
|
||||||
self.temp_screenshot_timer = None;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,5 +111,4 @@ pub enum InputAction {
|
|||||||
PasswordChanged,
|
PasswordChanged,
|
||||||
SubmitPassword(Zeroizing<String>),
|
SubmitPassword(Zeroizing<String>),
|
||||||
Cancel,
|
Cancel,
|
||||||
TempScreenshot,
|
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-17
@@ -6,6 +6,7 @@ use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
|||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
use crate::input::{InputAction, InputHandler};
|
use crate::input::{InputAction, InputHandler};
|
||||||
use crate::render::Renderer;
|
use crate::render::Renderer;
|
||||||
|
use crate::system::SystemStatus;
|
||||||
use smithay_client_toolkit::seat::keyboard::KeyEvent;
|
use smithay_client_toolkit::seat::keyboard::KeyEvent;
|
||||||
use smithay_client_toolkit::shm::slot::SlotPool;
|
use smithay_client_toolkit::shm::slot::SlotPool;
|
||||||
|
|
||||||
@@ -19,7 +20,6 @@ pub struct LockedSurface {
|
|||||||
fade_alpha: f64,
|
fade_alpha: f64,
|
||||||
wrong_password_shown: bool,
|
wrong_password_shown: bool,
|
||||||
key_highlight_shown: bool,
|
key_highlight_shown: bool,
|
||||||
temp_screenshot_shown: bool,
|
|
||||||
start_time: Instant,
|
start_time: Instant,
|
||||||
wayland_surface: Option<wl_surface::WlSurface>,
|
wayland_surface: Option<wl_surface::WlSurface>,
|
||||||
output: wl_output::WlOutput,
|
output: wl_output::WlOutput,
|
||||||
@@ -50,7 +50,6 @@ impl LockedSurface {
|
|||||||
fade_alpha: 0.0,
|
fade_alpha: 0.0,
|
||||||
wrong_password_shown: false,
|
wrong_password_shown: false,
|
||||||
key_highlight_shown: false,
|
key_highlight_shown: false,
|
||||||
temp_screenshot_shown: false,
|
|
||||||
start_time: Instant::now(),
|
start_time: Instant::now(),
|
||||||
wayland_surface: None,
|
wayland_surface: None,
|
||||||
output,
|
output,
|
||||||
@@ -108,15 +107,6 @@ impl LockedSurface {
|
|||||||
self.key_highlight_shown = false;
|
self.key_highlight_shown = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if we should show/hide temp screenshot
|
|
||||||
if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown {
|
|
||||||
self.renderer.set_fade_alpha(0.3);
|
|
||||||
self.temp_screenshot_shown = true;
|
|
||||||
} else if !self.input_handler.should_show_temp_screenshot() && self.temp_screenshot_shown {
|
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
|
||||||
self.temp_screenshot_shown = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set background if available and not already applied
|
// Set background if available and not already applied
|
||||||
if !self.background_applied {
|
if !self.background_applied {
|
||||||
if let Some(ref background) = self.background {
|
if let Some(ref background) = self.background {
|
||||||
@@ -170,11 +160,12 @@ impl LockedSurface {
|
|||||||
pub fn handle_key_event(
|
pub fn handle_key_event(
|
||||||
&mut self,
|
&mut self,
|
||||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
||||||
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
) -> Option<InputAction> {
|
) -> Option<InputAction> {
|
||||||
let action = self.input_handler.handle_key_event(
|
let action = self.input_handler.handle_key_event(
|
||||||
event.keysym,
|
event.keysym,
|
||||||
wayland_client::protocol::wl_keyboard::KeyState::Pressed,
|
event.utf8,
|
||||||
smithay_client_toolkit::seat::keyboard::Modifiers::default(),
|
modifiers,
|
||||||
);
|
);
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
@@ -204,6 +195,10 @@ impl LockedSurface {
|
|||||||
self.background = Some(surface);
|
self.background = Some(surface);
|
||||||
self.background_applied = false;
|
self.background_applied = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||||
|
self.renderer.system_status = status;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct LockManager {
|
pub struct LockManager {
|
||||||
@@ -252,19 +247,23 @@ impl LockManager {
|
|||||||
.find(|surface| surface.matches_surface(wayland_surface))
|
.find(|surface| surface.matches_surface(wayland_surface))
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_key_event(&mut self, event: KeyEvent) -> Option<InputAction> {
|
pub fn handle_key_event(
|
||||||
|
&mut self,
|
||||||
|
event: KeyEvent,
|
||||||
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
|
) -> Option<InputAction> {
|
||||||
let mut action = None;
|
let mut action = None;
|
||||||
for surface in &mut self.surfaces {
|
for surface in &mut self.surfaces {
|
||||||
if let Some(a) = surface.handle_key_event(event.clone()) {
|
if let Some(a) = surface.handle_key_event(event.clone(), modifiers) {
|
||||||
action = Some(a);
|
action = Some(a);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
action
|
action
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn toggle_peek(&mut self) {
|
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||||
for surface in &mut self.surfaces {
|
for surface in &mut self.surfaces {
|
||||||
surface.input_handler.update_temp_screenshot();
|
surface.set_system_status(status.clone());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+155
-28
@@ -4,12 +4,14 @@ mod input;
|
|||||||
mod lock;
|
mod lock;
|
||||||
mod render;
|
mod render;
|
||||||
mod screenshot;
|
mod screenshot;
|
||||||
|
mod system;
|
||||||
mod timer;
|
mod timer;
|
||||||
mod util;
|
mod util;
|
||||||
|
|
||||||
use config::Config;
|
use config::Config;
|
||||||
use lock::LockManager;
|
use lock::LockManager;
|
||||||
use screenshot::{CaptureData, Screenshot, ScreenshotManager};
|
use screenshot::{CaptureData, Screenshot, ScreenshotManager};
|
||||||
|
use system::SystemManager;
|
||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::fs::OpenOptions;
|
use std::fs::OpenOptions;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -125,6 +127,9 @@ struct WaylandLock {
|
|||||||
unlocking: bool,
|
unlocking: bool,
|
||||||
screenshot_manager: Option<ScreenshotManager>,
|
screenshot_manager: Option<ScreenshotManager>,
|
||||||
grace_until: Option<Instant>,
|
grace_until: Option<Instant>,
|
||||||
|
system_manager: Arc<SystemManager>,
|
||||||
|
modifiers: Modifiers,
|
||||||
|
current_layout: u32,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl WaylandLock {
|
impl WaylandLock {
|
||||||
@@ -163,6 +168,41 @@ impl WaylandLock {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn handle_key_event(&mut self, event: KeyEvent) {
|
fn handle_key_event(&mut self, event: KeyEvent) {
|
||||||
|
use crate::input::InputAction;
|
||||||
|
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||||
|
|
||||||
|
// Media and Session keys (Always handle these first and don't trigger grace unlock)
|
||||||
|
match event.keysym {
|
||||||
|
Keysym::XF86_AudioPlay | Keysym::XF86_AudioPause => {
|
||||||
|
self.system_manager.media_play_pause();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Keysym::XF86_AudioNext => {
|
||||||
|
self.system_manager.media_next();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Keysym::XF86_AudioPrev => {
|
||||||
|
self.system_manager.media_prev();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Keysym::F1 => {
|
||||||
|
self.system_manager
|
||||||
|
.send_command(system::SystemCommand::Suspend);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Keysym::F2 => {
|
||||||
|
self.system_manager
|
||||||
|
.send_command(system::SystemCommand::Reboot);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
Keysym::F3 => {
|
||||||
|
self.system_manager
|
||||||
|
.send_command(system::SystemCommand::PowerOff);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
|
||||||
// Check if we're in the grace period (any key unlocks without password)
|
// Check if we're in the grace period (any key unlocks without password)
|
||||||
if let Some(grace_until) = self.grace_until {
|
if let Some(grace_until) = self.grace_until {
|
||||||
if Instant::now() < grace_until {
|
if Instant::now() < grace_until {
|
||||||
@@ -173,16 +213,14 @@ impl WaylandLock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
use crate::input::InputAction;
|
|
||||||
use smithay_client_toolkit::seat::keyboard::Keysym;
|
|
||||||
|
|
||||||
if event.keysym == Keysym::Return {
|
if event.keysym == Keysym::Return {
|
||||||
log::info!("Enter pressed - submitting password");
|
log::info!("Enter pressed - submitting password");
|
||||||
if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
||||||
let mut password = Zeroizing::new(String::new());
|
let mut password = Zeroizing::new(String::new());
|
||||||
|
let modifiers = self.modifiers;
|
||||||
for surface in &mut lock_manager.surfaces {
|
for surface in &mut lock_manager.surfaces {
|
||||||
if let Some(InputAction::SubmitPassword(p)) =
|
if let Some(InputAction::SubmitPassword(p)) =
|
||||||
surface.handle_key_event(event.clone())
|
surface.handle_key_event(event.clone(), modifiers)
|
||||||
{
|
{
|
||||||
password = p;
|
password = p;
|
||||||
}
|
}
|
||||||
@@ -194,17 +232,12 @@ impl WaylandLock {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
let action = self
|
let modifiers = self.modifiers;
|
||||||
|
let _action = self
|
||||||
.lock_manager
|
.lock_manager
|
||||||
.lock()
|
.lock()
|
||||||
.map(|mut lm| lm.handle_key_event(event))
|
.map(|mut lm| lm.handle_key_event(event, modifiers))
|
||||||
.unwrap_or(None);
|
.unwrap_or(None);
|
||||||
|
|
||||||
if let Some(InputAction::TempScreenshot) = action {
|
|
||||||
if let Ok(mut lm) = self.lock_manager.lock() {
|
|
||||||
lm.toggle_peek();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -372,9 +405,11 @@ impl KeyboardHandler for WaylandLock {
|
|||||||
_qh: &QueueHandle<Self>,
|
_qh: &QueueHandle<Self>,
|
||||||
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||||
_serial: u32,
|
_serial: u32,
|
||||||
_modifiers: Modifiers,
|
modifiers: Modifiers,
|
||||||
_layout: u32,
|
layout: u32,
|
||||||
) {
|
) {
|
||||||
|
self.modifiers = modifiers;
|
||||||
|
self.current_layout = layout;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -402,7 +437,9 @@ impl SeatHandler for WaylandLock {
|
|||||||
&seat,
|
&seat,
|
||||||
None,
|
None,
|
||||||
self.loop_handle.clone(),
|
self.loop_handle.clone(),
|
||||||
Box::new(|_state, _kbd, _event| {}),
|
Box::new(|state, _kbd, event| {
|
||||||
|
state.handle_key_event(event);
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -453,6 +490,7 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
|
|||||||
stride,
|
stride,
|
||||||
} => {
|
} => {
|
||||||
let format = format.into_result().unwrap();
|
let format = format.into_result().unwrap();
|
||||||
|
|
||||||
let mut info = data.info.lock().unwrap();
|
let mut info = data.info.lock().unwrap();
|
||||||
*info = Some(screenshot::BufferInfo {
|
*info = Some(screenshot::BufferInfo {
|
||||||
width,
|
width,
|
||||||
@@ -460,15 +498,22 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
|
|||||||
stride,
|
stride,
|
||||||
format,
|
format,
|
||||||
});
|
});
|
||||||
match state
|
|
||||||
.pool
|
// Create a dedicated pool for this screenshot to ensure offset 0
|
||||||
.create_buffer(width as i32, height as i32, stride as i32, format)
|
// This matches swaylock-effects and fixes "invalid buffer" on some compositors
|
||||||
{
|
let size = (stride as usize) * (height as usize);
|
||||||
Ok((buffer, _canvas)) => {
|
match SlotPool::new(size, &state.shm_state) {
|
||||||
frame.copy(buffer.wl_buffer());
|
Ok(mut pool) => {
|
||||||
*data.buffer.lock().unwrap() = Some(buffer);
|
match pool.create_buffer(width as i32, height as i32, stride as i32, format) {
|
||||||
|
Ok((buffer, _canvas)) => {
|
||||||
|
frame.copy(buffer.wl_buffer());
|
||||||
|
*data.buffer.lock().unwrap() = Some(buffer);
|
||||||
|
*data.pool.lock().unwrap() = Some(pool);
|
||||||
|
}
|
||||||
|
Err(e) => log::error!("Screencopy: Buffer creation failed: {:?}", e),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(e) => log::error!("Screencopy: Buffer creation failed: {:?}", e),
|
Err(e) => log::error!("Screencopy: Pool creation failed: {:?}", e),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Event::Flags { flags } => {
|
Event::Flags { flags } => {
|
||||||
@@ -480,13 +525,15 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
|
|||||||
let buffer = data.buffer.lock().unwrap().take();
|
let buffer = data.buffer.lock().unwrap().take();
|
||||||
let info = data.info.lock().unwrap().take();
|
let info = data.info.lock().unwrap().take();
|
||||||
let flags = data.flags.lock().unwrap().take();
|
let flags = data.flags.lock().unwrap().take();
|
||||||
if let (Some(buffer), Some(info), Some(flags)) = (buffer, info, flags) {
|
let pool = data.pool.lock().unwrap().take();
|
||||||
|
|
||||||
|
if let (Some(buffer), Some(info), Some(flags), Some(mut pool)) = (buffer, info, flags, pool) {
|
||||||
let handle = screenshot::ScreencopyBufferHandle {
|
let handle = screenshot::ScreencopyBufferHandle {
|
||||||
buffer,
|
buffer,
|
||||||
info,
|
info,
|
||||||
y_invert: flags.contains(Flags::YInvert),
|
y_invert: flags.contains(Flags::YInvert),
|
||||||
};
|
};
|
||||||
if let Ok(surface) = mgr.buffer_to_surface(handle, &mut state.pool) {
|
if let Ok(surface) = mgr.buffer_to_surface(handle, &mut pool) {
|
||||||
let mut ss = Screenshot::new(surface);
|
let mut ss = Screenshot::new(surface);
|
||||||
let _ = ss.apply_effects(&state.config);
|
let _ = ss.apply_effects(&state.config);
|
||||||
if data.output_idx < state.captured_backgrounds.len() {
|
if data.output_idx < state.captured_backgrounds.len() {
|
||||||
@@ -547,11 +594,25 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
let qh: QueueHandle<WaylandLock> = event_queue.handle();
|
let qh: QueueHandle<WaylandLock> = event_queue.handle();
|
||||||
|
|
||||||
let shm_state = Shm::bind(&globals, &qh).map_err(|_| "wl_shm not supported")?;
|
let shm_state = Shm::bind(&globals, &qh).map_err(|_| "wl_shm not supported")?;
|
||||||
let pool = SlotPool::new(1920 * 1080 * 4, &shm_state)?;
|
|
||||||
|
|
||||||
let (auth_tx_actual, auth_feedback_rx_actual) = auth::create_and_run_auth_loop();
|
let system_manager = Arc::new(SystemManager::new());
|
||||||
|
|
||||||
|
let (auth_tx_actual, auth_feedback_rx_actual) = match auth::create_and_run_auth_loop() {
|
||||||
|
Some(channels) => channels,
|
||||||
|
None => {
|
||||||
|
log::error!("Failed to initialize authentication. This usually means PAM is not configured correctly.");
|
||||||
|
log::error!("Please ensure you have a PAM service file at /etc/pam.d/rustlock");
|
||||||
|
std::process::exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?;
|
let mut event_loop: EventLoop<WaylandLock> = EventLoop::try_new()?;
|
||||||
|
|
||||||
|
// Initialize state without pool first
|
||||||
|
// We'll create the pool after we know the output dimensions
|
||||||
|
|
||||||
|
// Initialize pool with a minimal size, it will be resized once outputs are detected
|
||||||
|
let pool = SlotPool::new(1, &shm_state)?;
|
||||||
|
|
||||||
let mut state = WaylandLock {
|
let mut state = WaylandLock {
|
||||||
loop_handle: event_loop.handle(),
|
loop_handle: event_loop.handle(),
|
||||||
lock_manager: lock_manager.clone(),
|
lock_manager: lock_manager.clone(),
|
||||||
@@ -563,7 +624,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
registry_state: RegistryState::new(&globals),
|
registry_state: RegistryState::new(&globals),
|
||||||
session_lock_state: SessionLockState::new(&globals, &qh),
|
session_lock_state: SessionLockState::new(&globals, &qh),
|
||||||
seat_state: SeatState::new(&globals, &qh),
|
seat_state: SeatState::new(&globals, &qh),
|
||||||
shm_state,
|
shm_state: shm_state,
|
||||||
pool,
|
pool,
|
||||||
session_lock: None,
|
session_lock: None,
|
||||||
lock_surfaces: Vec::new(),
|
lock_surfaces: Vec::new(),
|
||||||
@@ -575,10 +636,67 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
unlocking: false,
|
unlocking: false,
|
||||||
screenshot_manager: ScreenshotManager::new(&globals, &qh).ok(),
|
screenshot_manager: ScreenshotManager::new(&globals, &qh).ok(),
|
||||||
grace_until: None,
|
grace_until: None,
|
||||||
|
system_manager: system_manager.clone(),
|
||||||
|
modifiers: Modifiers::default(),
|
||||||
|
current_layout: 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
event_queue.blocking_dispatch(&mut state)?;
|
event_queue.blocking_dispatch(&mut state)?;
|
||||||
|
|
||||||
|
// Handle custom background image if provided (prioritize over screenshots)
|
||||||
|
if let Some(ref image_path) = state.config.image {
|
||||||
|
log::info!("Loading custom background image from {:?}", image_path);
|
||||||
|
if let Ok(img) = image::open(image_path) {
|
||||||
|
let img = img.to_rgba8();
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let mut surface = cairo::ImageSurface::create(cairo::Format::ARgb32, w as i32, h as i32).unwrap();
|
||||||
|
{
|
||||||
|
let mut surface_data = surface.data().unwrap();
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let pixel = img.get_pixel(x, y);
|
||||||
|
let idx = ((y * w + x) * 4) as usize;
|
||||||
|
surface_data[idx] = pixel[2]; // B
|
||||||
|
surface_data[idx + 1] = pixel[1]; // G
|
||||||
|
surface_data[idx + 2] = pixel[0]; // R
|
||||||
|
surface_data[idx + 3] = pixel[3]; // A
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut ss = Screenshot::new(surface);
|
||||||
|
let _ = ss.apply_effects(&state.config);
|
||||||
|
let surface = ss.into_inner();
|
||||||
|
|
||||||
|
let num_outputs = state.output_state.outputs().count();
|
||||||
|
state.captured_backgrounds = vec![Some(surface); num_outputs];
|
||||||
|
|
||||||
|
// Disable screenshots if image was successfully loaded
|
||||||
|
state.config.screenshots = false;
|
||||||
|
} else {
|
||||||
|
log::error!("Failed to load custom background image from {:?}", image_path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that we have output info, we can resize the pool if needed
|
||||||
|
let mut total_size = 0;
|
||||||
|
for output in state.output_state.outputs() {
|
||||||
|
if let Some(info) = state.output_state.info(&output) {
|
||||||
|
if let Some(mode) = info.modes.first() {
|
||||||
|
let (w, h) = mode.dimensions;
|
||||||
|
total_size += (w as usize) * (h as usize) * 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if total_size > 0 {
|
||||||
|
// Resize pool to fit all outputs
|
||||||
|
// SlotPool doesn't have a direct resize, but we can just create a new one if needed,
|
||||||
|
// or rely on its internal growing if we use it that way.
|
||||||
|
// Actually SCTK SlotPool handles resizing when creating buffers if needed,
|
||||||
|
// but it's better to have a large enough base.
|
||||||
|
state.pool = SlotPool::new(total_size, &state.shm_state)?;
|
||||||
|
}
|
||||||
|
|
||||||
let _wayland_source =
|
let _wayland_source =
|
||||||
WaylandSource::new(conn.clone(), event_queue).insert(event_loop.handle())?;
|
WaylandSource::new(conn.clone(), event_queue).insert(event_loop.handle())?;
|
||||||
|
|
||||||
@@ -606,7 +724,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
return calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(100));
|
return calloop::timer::TimeoutAction::ToDuration(Duration::from_millis(100));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut status = state.system_manager.get_status();
|
||||||
|
status.keyboard_layout = Some(state.current_layout);
|
||||||
|
|
||||||
if let Ok(mut lm) = state.lock_manager.lock() {
|
if let Ok(mut lm) = state.lock_manager.lock() {
|
||||||
|
lm.set_system_status(status);
|
||||||
lm.update();
|
lm.update();
|
||||||
for surface in &mut lm.surfaces {
|
for surface in &mut lm.surfaces {
|
||||||
let _ = surface.commit(&mut state.pool);
|
let _ = surface.commit(&mut state.pool);
|
||||||
@@ -619,6 +741,11 @@ fn main() -> Result<(), Box<dyn Error>> {
|
|||||||
}
|
}
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Dispatch to ensure outputs are ready before capture
|
||||||
|
if state.config.screenshots {
|
||||||
|
event_loop.dispatch(Duration::from_millis(50), &mut state)?;
|
||||||
|
}
|
||||||
|
|
||||||
if state.config.screenshots {
|
if state.config.screenshots {
|
||||||
state.outputs = state.output_state.outputs().collect();
|
state.outputs = state.output_state.outputs().collect();
|
||||||
log::info!(
|
log::info!(
|
||||||
|
|||||||
+384
-2
@@ -2,6 +2,7 @@ use cairo::{Context, Format, ImageSurface};
|
|||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use crate::config::Config;
|
use crate::config::Config;
|
||||||
|
use crate::system::SystemStatus;
|
||||||
|
|
||||||
/// Cairo-based renderer for the lock screen
|
/// Cairo-based renderer for the lock screen
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -21,6 +22,16 @@ pub struct Renderer {
|
|||||||
uptime_cache: String,
|
uptime_cache: String,
|
||||||
last_uptime_update: Option<Instant>,
|
last_uptime_update: Option<Instant>,
|
||||||
caps_lock: bool,
|
caps_lock: bool,
|
||||||
|
pub system_status: SystemStatus,
|
||||||
|
media_art_surface: Option<ImageSurface>,
|
||||||
|
last_art_url: Option<String>,
|
||||||
|
wifi_icon_surface: Option<ImageSurface>,
|
||||||
|
bluetooth_icon_surface: Option<ImageSurface>,
|
||||||
|
battery_icon_surface: Option<ImageSurface>,
|
||||||
|
media_prev_icon_surface: Option<ImageSurface>,
|
||||||
|
media_stop_icon_surface: Option<ImageSurface>,
|
||||||
|
media_play_icon_surface: Option<ImageSurface>,
|
||||||
|
media_next_icon_surface: Option<ImageSurface>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Renderer {
|
impl Renderer {
|
||||||
@@ -31,10 +42,10 @@ impl Renderer {
|
|||||||
.expect("Failed to create Cairo surface");
|
.expect("Failed to create Cairo surface");
|
||||||
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
||||||
|
|
||||||
Self {
|
let mut renderer = Self {
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
config,
|
config: config.clone(),
|
||||||
surface,
|
surface,
|
||||||
context,
|
context,
|
||||||
fade_alpha: 0.0,
|
fade_alpha: 0.0,
|
||||||
@@ -48,6 +59,85 @@ impl Renderer {
|
|||||||
uptime_cache: String::new(),
|
uptime_cache: String::new(),
|
||||||
last_uptime_update: None,
|
last_uptime_update: None,
|
||||||
caps_lock: false,
|
caps_lock: false,
|
||||||
|
system_status: SystemStatus::default(),
|
||||||
|
media_art_surface: None,
|
||||||
|
last_art_url: None,
|
||||||
|
wifi_icon_surface: None,
|
||||||
|
bluetooth_icon_surface: None,
|
||||||
|
battery_icon_surface: None,
|
||||||
|
media_prev_icon_surface: None,
|
||||||
|
media_stop_icon_surface: None,
|
||||||
|
media_play_icon_surface: None,
|
||||||
|
media_next_icon_surface: None,
|
||||||
|
};
|
||||||
|
|
||||||
|
renderer.load_icons();
|
||||||
|
renderer
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icons(&mut self) {
|
||||||
|
if let Some(ref path) = self.config.wifi_icon {
|
||||||
|
self.wifi_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.bluetooth_icon {
|
||||||
|
self.bluetooth_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.battery_icon {
|
||||||
|
self.battery_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.media_prev_icon {
|
||||||
|
self.media_prev_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.media_stop_icon {
|
||||||
|
self.media_stop_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.media_play_icon {
|
||||||
|
self.media_play_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
if let Some(ref path) = self.config.media_next_icon {
|
||||||
|
self.media_next_icon_surface = self.load_icon(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn load_icon(&self, identifier: &str) -> Option<ImageSurface> {
|
||||||
|
let path = if identifier.starts_with('/') {
|
||||||
|
std::path::PathBuf::from(identifier)
|
||||||
|
} else {
|
||||||
|
return None;
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Ok(pixbuf) = gdk_pixbuf::Pixbuf::from_file(&path) {
|
||||||
|
let w = pixbuf.width();
|
||||||
|
let h = pixbuf.height();
|
||||||
|
let mut surface = ImageSurface::create(Format::ARgb32, w, h).ok()?;
|
||||||
|
{
|
||||||
|
let mut surface_data = surface.data().ok()?;
|
||||||
|
let pix_data = unsafe { pixbuf.pixels() };
|
||||||
|
let n_channels = pixbuf.n_channels();
|
||||||
|
let rowstride = pixbuf.rowstride() as usize;
|
||||||
|
|
||||||
|
for y in 0..h as usize {
|
||||||
|
for x in 0..w as usize {
|
||||||
|
let pix_idx = y * rowstride + x * n_channels as usize;
|
||||||
|
let surf_idx = (y * w as usize + x) * 4;
|
||||||
|
|
||||||
|
if n_channels == 4 {
|
||||||
|
surface_data[surf_idx] = pix_data[pix_idx + 2];
|
||||||
|
surface_data[surf_idx + 1] = pix_data[pix_idx + 1];
|
||||||
|
surface_data[surf_idx + 2] = pix_data[pix_idx];
|
||||||
|
surface_data[surf_idx + 3] = pix_data[pix_idx + 3];
|
||||||
|
} else if n_channels == 3 {
|
||||||
|
surface_data[surf_idx] = pix_data[pix_idx + 2];
|
||||||
|
surface_data[surf_idx + 1] = pix_data[pix_idx + 1];
|
||||||
|
surface_data[surf_idx + 2] = pix_data[pix_idx];
|
||||||
|
surface_data[surf_idx + 3] = 255;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(surface)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,6 +214,22 @@ impl Renderer {
|
|||||||
self.draw_clock();
|
self.draw_clock();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.config.show_media {
|
||||||
|
self.draw_media();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.show_network {
|
||||||
|
self.draw_network();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.show_battery {
|
||||||
|
self.draw_status();
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.config.show_bluetooth {
|
||||||
|
self.draw_bluetooth();
|
||||||
|
}
|
||||||
|
|
||||||
if !self.password_display.is_empty() {
|
if !self.password_display.is_empty() {
|
||||||
self.draw_password_display();
|
self.draw_password_display();
|
||||||
}
|
}
|
||||||
@@ -368,4 +474,280 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn draw_media(&mut self) {
|
||||||
|
if let Some(ref title) = self.system_status.media_title {
|
||||||
|
let center_x = self.width as f64 / 2.0;
|
||||||
|
let start_y = self.height as f64 - 120.0;
|
||||||
|
let art_size = 56.0;
|
||||||
|
let spacing = 80.0;
|
||||||
|
|
||||||
|
if self.config.show_album_art {
|
||||||
|
if self.system_status.media_art_url != self.last_art_url {
|
||||||
|
self.last_art_url = self.system_status.media_art_url.clone();
|
||||||
|
self.media_art_surface = None;
|
||||||
|
if let Some(ref data) = self.system_status.media_art_data {
|
||||||
|
if let Ok(img) = image::load_from_memory(data) {
|
||||||
|
let img = img.to_rgba8();
|
||||||
|
let (w, h) = img.dimensions();
|
||||||
|
let mut surface = ImageSurface::create(Format::ARgb32, w as i32, h as i32).unwrap();
|
||||||
|
{
|
||||||
|
let mut surface_data = surface.data().unwrap();
|
||||||
|
for y in 0..h {
|
||||||
|
for x in 0..w {
|
||||||
|
let pixel = img.get_pixel(x, y);
|
||||||
|
let idx = ((y * w + x) * 4) as usize;
|
||||||
|
surface_data[idx] = pixel[2];
|
||||||
|
surface_data[idx + 1] = pixel[1];
|
||||||
|
surface_data[idx + 2] = pixel[0];
|
||||||
|
surface_data[idx + 3] = pixel[3];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.media_art_surface = Some(surface);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let has_art = self.config.show_album_art && self.media_art_surface.is_some();
|
||||||
|
let text_x = if has_art { center_x - spacing / 2.0 } else { center_x };
|
||||||
|
let art_x = center_x - spacing - art_size / 2.0;
|
||||||
|
|
||||||
|
if has_art {
|
||||||
|
if let Some(ref art) = self.media_art_surface {
|
||||||
|
self.context.save().unwrap();
|
||||||
|
let scale = art_size / art.width() as f64;
|
||||||
|
self.context.translate(art_x, start_y);
|
||||||
|
self.context.scale(scale, scale);
|
||||||
|
self.context.set_source_surface(art, 0.0, 0.0).unwrap();
|
||||||
|
self.context.paint_with_alpha(self.fade_alpha).unwrap();
|
||||||
|
self.context.restore().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.9);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
|
||||||
|
let display_text = if let Some(ref artist) = self.system_status.media_artist {
|
||||||
|
format!("{} - {}", artist, title)
|
||||||
|
} else {
|
||||||
|
title.clone()
|
||||||
|
};
|
||||||
|
|
||||||
|
let te = self.context.text_extents(&display_text).unwrap();
|
||||||
|
self.context.move_to(text_x - te.width() / 2.0, start_y + 20.0);
|
||||||
|
self.context.show_text(&display_text).unwrap();
|
||||||
|
|
||||||
|
let status_text = if self.system_status.media_playing {
|
||||||
|
if let Some(ref icon) = self.media_play_icon_surface {
|
||||||
|
// Draw play icon instead of text
|
||||||
|
let play_y = start_y + 40.0;
|
||||||
|
self.draw_icon_at(center_x - icon.width() as f64 / 2.0, play_y - icon.height() as f64 / 2.0, icon);
|
||||||
|
""
|
||||||
|
} else {
|
||||||
|
"▶ Playing"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"⏸ Paused"
|
||||||
|
};
|
||||||
|
|
||||||
|
if !status_text.is_empty() {
|
||||||
|
self.context.set_font_size(12.0);
|
||||||
|
let se = self.context.text_extents(status_text).unwrap();
|
||||||
|
self.context.move_to(text_x - se.width() / 2.0, start_y + 40.0);
|
||||||
|
self.context.show_text(status_text).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
let controls_y = start_y + 65.0;
|
||||||
|
|
||||||
|
// Draw media control icons
|
||||||
|
let icon_size = 20.0;
|
||||||
|
let icon_spacing = 40.0;
|
||||||
|
let controls_center_x = center_x;
|
||||||
|
|
||||||
|
// Previous icon
|
||||||
|
if let Some(ref icon) = self.media_prev_icon_surface {
|
||||||
|
let ix = controls_center_x - icon_spacing;
|
||||||
|
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon);
|
||||||
|
} else {
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(controls_center_x - icon_spacing - 8.0, controls_y);
|
||||||
|
self.context.show_text("⏮").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop icon
|
||||||
|
if let Some(ref icon) = self.media_stop_icon_surface {
|
||||||
|
let ix = controls_center_x;
|
||||||
|
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon);
|
||||||
|
} else {
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(controls_center_x - 8.0, controls_y);
|
||||||
|
self.context.show_text("⏹").unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next icon
|
||||||
|
if let Some(ref icon) = self.media_next_icon_surface {
|
||||||
|
let ix = controls_center_x + icon_spacing;
|
||||||
|
self.draw_icon_at(ix - icon_size / 2.0, controls_y - icon_size / 2.0, icon);
|
||||||
|
} else {
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.7);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(controls_center_x + icon_spacing - 8.0, controls_y);
|
||||||
|
self.context.show_text("⏭").unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_network(&self) {
|
||||||
|
if !self.config.show_network {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let margin = 20.0;
|
||||||
|
let x = margin;
|
||||||
|
let y = margin + 20.0;
|
||||||
|
|
||||||
|
if let Some(ref ssid) = self.system_status.wifi_ssid {
|
||||||
|
if let Some(ref icon) = self.wifi_icon_surface {
|
||||||
|
self.draw_icon_at(x, y - 15.0, icon);
|
||||||
|
let text_x = x + icon.width() as f64 + 10.0;
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(text_x, y);
|
||||||
|
self.context.show_text(ssid).unwrap();
|
||||||
|
} else {
|
||||||
|
let strength = self.system_status.wifi_strength.unwrap_or(0);
|
||||||
|
let icon = if strength > 75 { "📶" } else if strength > 50 { "📶" } else if strength > 25 { "📶" } else { "📶" };
|
||||||
|
let text = format!("{} {}", icon, ssid);
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(x, y);
|
||||||
|
self.context.show_text(&text).unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let text = "📵 No WiFi";
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.5);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(x, y);
|
||||||
|
self.context.show_text(text).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_status(&self) {
|
||||||
|
if let Some(percent) = self.system_status.battery_percent {
|
||||||
|
let margin = 20.0;
|
||||||
|
let icon_width = 30.0;
|
||||||
|
let x = self.width as f64 - margin - icon_width - 50.0;
|
||||||
|
let y = margin + 20.0;
|
||||||
|
|
||||||
|
if let Some(ref icon) = self.battery_icon_surface {
|
||||||
|
self.draw_icon_at(x, y - 15.0, icon);
|
||||||
|
let text_x = x + icon.width() as f64 + 10.0;
|
||||||
|
let battery_text = format!("{:.0}%", percent);
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(text_x, y);
|
||||||
|
self.context.show_text(&battery_text).unwrap();
|
||||||
|
} else {
|
||||||
|
self.draw_battery_icon_at(x, y - 12.0, icon_width, 15.0, percent, self.system_status.is_charging);
|
||||||
|
let battery_text = format!("{:.0}%", percent);
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(16.0);
|
||||||
|
self.context.move_to(x + icon_width + 10.0, y);
|
||||||
|
self.context.show_text(&battery_text).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_bluetooth(&self) {
|
||||||
|
if !self.config.show_bluetooth {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let margin = 20.0;
|
||||||
|
let x = margin;
|
||||||
|
let y = margin + 50.0;
|
||||||
|
|
||||||
|
if self.system_status.bluetooth_connected {
|
||||||
|
if let Some(ref icon) = self.bluetooth_icon_surface {
|
||||||
|
self.draw_icon_at(x, y - 12.0, icon);
|
||||||
|
let text_x = x + icon.width() as f64 + 10.0;
|
||||||
|
let devices = self.system_status.bluetooth_devices.join(", ");
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(14.0);
|
||||||
|
self.context.move_to(text_x, y);
|
||||||
|
self.context.show_text(&devices).unwrap();
|
||||||
|
} else {
|
||||||
|
let text = format!("🔵 {} device(s)", self.system_status.bluetooth_devices.len());
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(14.0);
|
||||||
|
self.context.move_to(x, y);
|
||||||
|
self.context.show_text(&text).unwrap();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let text = "🔴 Bluetooth off";
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha * 0.5);
|
||||||
|
self.context.set_font_size(14.0);
|
||||||
|
self.context.move_to(x, y);
|
||||||
|
self.context.show_text(text).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_icon_at(&self, x: f64, y: f64, surface: &ImageSurface) {
|
||||||
|
self.context.save().unwrap();
|
||||||
|
let target_size = 24.0;
|
||||||
|
let scale_x = target_size / surface.width() as f64;
|
||||||
|
let scale_y = target_size / surface.height() as f64;
|
||||||
|
let scale = scale_x.min(scale_y);
|
||||||
|
self.context.translate(x, y);
|
||||||
|
self.context.scale(scale, scale);
|
||||||
|
self.context.set_source_surface(surface, 0.0, 0.0).unwrap();
|
||||||
|
self.context.paint_with_alpha(self.fade_alpha).unwrap();
|
||||||
|
self.context.restore().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_battery_icon_at(&self, x: f64, y: f64, width: f64, height: f64, percent: f64, charging: bool) {
|
||||||
|
let alpha = self.fade_alpha;
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 1.0, alpha * 0.5);
|
||||||
|
self.context.set_line_width(2.0);
|
||||||
|
self.context.rectangle(x, y, width, height);
|
||||||
|
self.context.stroke().unwrap();
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.rectangle(x + width, y + height / 4.0, 3.0, height / 2.0);
|
||||||
|
self.context.fill().unwrap();
|
||||||
|
let fill_width = (width - 4.0) * (percent / 100.0);
|
||||||
|
self.context.new_path();
|
||||||
|
if percent < 20.0 {
|
||||||
|
self.context.set_source_rgba(1.0, 0.2, 0.2, alpha);
|
||||||
|
} else {
|
||||||
|
self.context.set_source_rgba(0.2, 1.0, 0.2, alpha * 0.8);
|
||||||
|
}
|
||||||
|
self.context.rectangle(x + 2.0, y + 2.0, fill_width, height - 4.0);
|
||||||
|
self.context.fill().unwrap();
|
||||||
|
if charging {
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(1.0, 1.0, 0.0, alpha);
|
||||||
|
let bx = x + width / 2.0;
|
||||||
|
let by = y + height / 2.0;
|
||||||
|
self.context.move_to(bx - 3.0, by + 2.0);
|
||||||
|
self.context.line_to(bx + 1.0, by - 1.0);
|
||||||
|
self.context.line_to(bx - 1.0, by - 1.0);
|
||||||
|
self.context.line_to(bx + 3.0, by - 6.0);
|
||||||
|
self.context.line_to(bx - 1.0, by - 3.0);
|
||||||
|
self.context.line_to(bx + 1.0, by - 3.0);
|
||||||
|
self.context.close_path();
|
||||||
|
self.context.fill().unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,9 +41,161 @@ impl Screenshot {
|
|||||||
if let Some((base, factor)) = config.effect_vignette {
|
if let Some((base, factor)) = config.effect_vignette {
|
||||||
self.apply_vignette(base, factor);
|
self.apply_vignette(base, factor);
|
||||||
}
|
}
|
||||||
|
if let Some(pixel_size) = config.effect_pixelate {
|
||||||
|
self.apply_pixelate(pixel_size);
|
||||||
|
}
|
||||||
|
if let Some(angle) = config.effect_swirl {
|
||||||
|
self.apply_swirl(angle);
|
||||||
|
}
|
||||||
|
if let Some(factor) = config.effect_melting {
|
||||||
|
self.apply_melting(factor);
|
||||||
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Apply a swirl effect.
|
||||||
|
pub fn apply_swirl(&mut self, angle: f32) {
|
||||||
|
let width = self.surface.width();
|
||||||
|
let height = self.surface.height();
|
||||||
|
let center_x = width as f32 / 2.0;
|
||||||
|
let center_y = height as f32 / 2.0;
|
||||||
|
let radius = center_x.min(center_y);
|
||||||
|
|
||||||
|
let stride = self.surface.stride() as usize;
|
||||||
|
let mut data = vec![0u8; stride * height as usize];
|
||||||
|
self.surface
|
||||||
|
.with_data(|src| data.copy_from_slice(src))
|
||||||
|
.unwrap();
|
||||||
|
let original = data.clone();
|
||||||
|
|
||||||
|
for y in 0..height {
|
||||||
|
for x in 0..width {
|
||||||
|
let dx = x as f32 - center_x;
|
||||||
|
let dy = y as f32 - center_y;
|
||||||
|
let d = (dx * dx + dy * dy).sqrt();
|
||||||
|
|
||||||
|
if d < radius {
|
||||||
|
let percent = (radius - d) / radius;
|
||||||
|
let theta = percent * percent * angle;
|
||||||
|
let s = theta.sin();
|
||||||
|
let c = theta.cos();
|
||||||
|
|
||||||
|
let nx = (c * dx - s * dy + center_x) as i32;
|
||||||
|
let ny = (s * dx + c * dy + center_y) as i32;
|
||||||
|
|
||||||
|
if nx >= 0 && nx < width && ny >= 0 && ny < height {
|
||||||
|
let src_idx = (ny as usize * stride) + (nx as usize * 4);
|
||||||
|
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
||||||
|
data[dst_idx..dst_idx + 4].copy_from_slice(&original[src_idx..src_idx + 4]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut surface_data = self.surface.data().unwrap();
|
||||||
|
surface_data.copy_from_slice(&data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Apply a melting effect (vertical smear).
|
||||||
|
pub fn apply_melting(&mut self, factor: f32) {
|
||||||
|
let width = self.surface.width();
|
||||||
|
let height = self.surface.height();
|
||||||
|
|
||||||
|
let stride = self.surface.stride() as usize;
|
||||||
|
let mut data = vec![0u8; stride * height as usize];
|
||||||
|
self.surface
|
||||||
|
.with_data(|src| data.copy_from_slice(src))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
use rand::Rng;
|
||||||
|
let mut rng = rand::thread_rng();
|
||||||
|
|
||||||
|
for x in 0..width {
|
||||||
|
let mut melt_amount = 0.0;
|
||||||
|
for y in 0..height {
|
||||||
|
melt_amount += rng.gen_range(0.0..factor);
|
||||||
|
let src_y = (y as f32 - melt_amount).max(0.0) as i32;
|
||||||
|
|
||||||
|
let src_idx = (src_y as usize * stride) + (x as usize * 4);
|
||||||
|
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
||||||
|
|
||||||
|
// Copy the pixel from above to create a smear
|
||||||
|
let pixel = [
|
||||||
|
data[src_idx],
|
||||||
|
data[src_idx + 1],
|
||||||
|
data[src_idx + 2],
|
||||||
|
data[src_idx + 3],
|
||||||
|
];
|
||||||
|
data[dst_idx..dst_idx + 4].copy_from_slice(&pixel);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut surface_data = self.surface.data().unwrap();
|
||||||
|
surface_data.copy_from_slice(&data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Pixelate the surface.
|
||||||
|
pub fn apply_pixelate(&mut self, pixel_size: u32) {
|
||||||
|
if pixel_size <= 1 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let width = self.surface.width();
|
||||||
|
let height = self.surface.height();
|
||||||
|
let stride = self.surface.stride() as usize;
|
||||||
|
let mut data = vec![0u8; stride * height as usize];
|
||||||
|
self.surface
|
||||||
|
.with_data(|src| data.copy_from_slice(src))
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
for y in (0..height).step_by(pixel_size as usize) {
|
||||||
|
for x in (0..width).step_by(pixel_size as usize) {
|
||||||
|
let mut r = 0u32;
|
||||||
|
let mut g = 0u32;
|
||||||
|
let mut b = 0u32;
|
||||||
|
let mut count = 0u32;
|
||||||
|
|
||||||
|
// Average pixels in the block
|
||||||
|
for py in 0..pixel_size {
|
||||||
|
for px in 0..pixel_size {
|
||||||
|
let cur_x = x + px as i32;
|
||||||
|
let cur_y = y + py as i32;
|
||||||
|
if cur_x < width && cur_y < height {
|
||||||
|
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
||||||
|
r += data[index] as u32;
|
||||||
|
g += data[index + 1] as u32;
|
||||||
|
b += data[index + 2] as u32;
|
||||||
|
count += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if count > 0 {
|
||||||
|
let r = (r / count) as u8;
|
||||||
|
let g = (g / count) as u8;
|
||||||
|
let b = (b / count) as u8;
|
||||||
|
|
||||||
|
// Fill the block
|
||||||
|
for py in 0..pixel_size {
|
||||||
|
for px in 0..pixel_size {
|
||||||
|
let cur_x = x + px as i32;
|
||||||
|
let cur_y = y + py as i32;
|
||||||
|
if cur_x < width && cur_y < height {
|
||||||
|
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
||||||
|
data[index] = r;
|
||||||
|
data[index + 1] = g;
|
||||||
|
data[index + 2] = b;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut surface_data = self.surface.data().unwrap();
|
||||||
|
surface_data.copy_from_slice(&data);
|
||||||
|
}
|
||||||
|
|
||||||
/// Apply a Gaussian blur effect.
|
/// Apply a Gaussian blur effect.
|
||||||
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
|
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
|
||||||
if radius == 0 || times == 0 {
|
if radius == 0 || times == 0 {
|
||||||
@@ -304,6 +456,7 @@ pub struct CaptureData {
|
|||||||
pub info: Mutex<Option<BufferInfo>>,
|
pub info: Mutex<Option<BufferInfo>>,
|
||||||
pub flags: Mutex<Option<Flags>>,
|
pub flags: Mutex<Option<Flags>>,
|
||||||
pub buffer: Mutex<Option<Buffer>>,
|
pub buffer: Mutex<Option<Buffer>>,
|
||||||
|
pub pool: Mutex<Option<SlotPool>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl CaptureData {
|
impl CaptureData {
|
||||||
@@ -314,6 +467,7 @@ impl CaptureData {
|
|||||||
info: Mutex::new(None),
|
info: Mutex::new(None),
|
||||||
flags: Mutex::new(None),
|
flags: Mutex::new(None),
|
||||||
buffer: Mutex::new(None),
|
buffer: Mutex::new(None),
|
||||||
|
pool: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use mpris::PlayerFinder;
|
||||||
|
use zbus::Connection;
|
||||||
|
use log::{error, debug};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
|
||||||
|
#[derive(Clone, Default)]
|
||||||
|
pub struct SystemStatus {
|
||||||
|
pub battery_percent: Option<f64>,
|
||||||
|
pub is_charging: bool,
|
||||||
|
pub media_title: Option<String>,
|
||||||
|
pub media_artist: Option<String>,
|
||||||
|
pub media_playing: bool,
|
||||||
|
pub media_art_url: Option<String>,
|
||||||
|
pub media_art_data: Option<Arc<Vec<u8>>>,
|
||||||
|
pub wifi_ssid: Option<String>,
|
||||||
|
pub wifi_strength: Option<u8>,
|
||||||
|
pub bluetooth_connected: bool,
|
||||||
|
pub bluetooth_devices: Vec<String>,
|
||||||
|
pub keyboard_layout: Option<u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy)]
|
||||||
|
pub enum SystemCommand {
|
||||||
|
PowerOff,
|
||||||
|
Reboot,
|
||||||
|
Suspend,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SystemManager {
|
||||||
|
status: Arc<Mutex<SystemStatus>>,
|
||||||
|
cmd_tx: mpsc::UnboundedSender<SystemCommand>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SystemManager {
|
||||||
|
pub fn new() -> Self {
|
||||||
|
let status = Arc::new(Mutex::new(SystemStatus::default()));
|
||||||
|
let s_clone = status.clone();
|
||||||
|
let (cmd_tx, mut cmd_rx) = mpsc::unbounded_channel::<SystemCommand>();
|
||||||
|
|
||||||
|
// Spawn a thread to update status periodically and handle commands
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
let rt = match tokio::runtime::Runtime::new() {
|
||||||
|
Ok(rt) => rt,
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to create tokio runtime for SystemManager: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
rt.block_on(async {
|
||||||
|
let mut conn: Option<Connection> = None;
|
||||||
|
let mut interval = tokio::time::interval(tokio::time::Duration::from_secs(2));
|
||||||
|
let mut last_art_url: Option<String> = None;
|
||||||
|
let mut last_art_data: Option<Arc<Vec<u8>>> = None;
|
||||||
|
|
||||||
|
loop {
|
||||||
|
// Try to connect to system DBus if not connected
|
||||||
|
if conn.is_none() {
|
||||||
|
match Connection::system().await {
|
||||||
|
Ok(c) => conn = Some(c),
|
||||||
|
Err(e) => {
|
||||||
|
error!("Failed to connect to system DBus: {}", e);
|
||||||
|
tokio::time::sleep(tokio::time::Duration::from_secs(5)).await;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tokio::select! {
|
||||||
|
_ = interval.tick() => {
|
||||||
|
let mut new_status = SystemStatus::default();
|
||||||
|
|
||||||
|
if let Some(ref c) = conn {
|
||||||
|
// 1. Battery status
|
||||||
|
if let Ok(proxy) = upower_dbus::UPowerProxy::new(c).await {
|
||||||
|
if let Ok(display_device) = proxy.get_display_device().await {
|
||||||
|
new_status.battery_percent = display_device.percentage().await.ok();
|
||||||
|
if let Ok(state) = display_device.state().await {
|
||||||
|
new_status.is_charging = format!("{:?}", state).contains("Charging");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. MPRIS status
|
||||||
|
if let Ok(finder) = PlayerFinder::new() {
|
||||||
|
if let Ok(player) = finder.find_active() {
|
||||||
|
if let Ok(metadata) = player.get_metadata() {
|
||||||
|
new_status.media_title = metadata.title().map(|s| s.to_string());
|
||||||
|
new_status.media_artist = metadata.artists().map(|a| a.join(", "));
|
||||||
|
new_status.media_art_url = metadata.art_url().map(|u| u.to_string());
|
||||||
|
|
||||||
|
if new_status.media_art_url != last_art_url {
|
||||||
|
last_art_url = new_status.media_art_url.clone();
|
||||||
|
last_art_data = None;
|
||||||
|
if let Some(ref url) = last_art_url {
|
||||||
|
if url.starts_with("file://") {
|
||||||
|
let path = url.trim_start_matches("file://");
|
||||||
|
if let Ok(data) = std::fs::read(path) {
|
||||||
|
last_art_data = Some(Arc::new(data));
|
||||||
|
}
|
||||||
|
} else if url.starts_with("http") {
|
||||||
|
#[cfg(feature = "networking")]
|
||||||
|
if let Ok(resp) = reqwest::get(url).await {
|
||||||
|
if let Ok(bytes) = resp.bytes().await {
|
||||||
|
last_art_data = Some(Arc::new(bytes.to_vec()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
#[cfg(not(feature = "networking"))]
|
||||||
|
{
|
||||||
|
log::debug!("Networking disabled, skipping remote album art: {}", url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
new_status.media_art_data = last_art_data.clone();
|
||||||
|
}
|
||||||
|
new_status.media_playing = player.get_playback_status().map(|s| matches!(s, mpris::PlaybackStatus::Playing)).unwrap_or(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. WiFi status (NetworkManager)
|
||||||
|
if let Some(ref c) = conn {
|
||||||
|
if let Ok(reply) = c.call_method(
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
"/org/freedesktop/NetworkManager",
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
"GetDevices",
|
||||||
|
&(),
|
||||||
|
).await {
|
||||||
|
let devices: Vec<zbus::zvariant::OwnedObjectPath> = reply.body().unwrap();
|
||||||
|
for dev_path in devices {
|
||||||
|
if let Ok(dev_type_reply) = c.call_method(
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
&dev_path,
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&("org.freedesktop.NetworkManager.Device", "DeviceType"),
|
||||||
|
).await {
|
||||||
|
let dev_type: u32 = dev_type_reply.body::<zbus::zvariant::Value>().unwrap().downcast().unwrap();
|
||||||
|
if dev_type == 2 { // WiFi
|
||||||
|
if let Ok(active_ap_reply) = c.call_method(
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
&dev_path,
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&("org.freedesktop.NetworkManager.Device.Wireless", "ActiveAccessPoint"),
|
||||||
|
).await {
|
||||||
|
let ap_path: zbus::zvariant::OwnedObjectPath = active_ap_reply.body::<zbus::zvariant::Value>().unwrap().downcast().unwrap();
|
||||||
|
if ap_path.as_str() != "/" {
|
||||||
|
if let Ok(ssid_reply) = c.call_method(
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
&ap_path,
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&("org.freedesktop.NetworkManager.AccessPoint", "Ssid"),
|
||||||
|
).await {
|
||||||
|
let ssid_bytes: Vec<u8> = ssid_reply.body::<zbus::zvariant::Value>().unwrap().downcast().unwrap();
|
||||||
|
new_status.wifi_ssid = Some(String::from_utf8_lossy(&ssid_bytes).to_string());
|
||||||
|
}
|
||||||
|
if let Ok(strength_reply) = c.call_method(
|
||||||
|
Some("org.freedesktop.NetworkManager"),
|
||||||
|
&ap_path,
|
||||||
|
Some("org.freedesktop.DBus.Properties"),
|
||||||
|
"Get",
|
||||||
|
&("org.freedesktop.NetworkManager.AccessPoint", "Strength"),
|
||||||
|
).await {
|
||||||
|
new_status.wifi_strength = Some(strength_reply.body::<zbus::zvariant::Value>().unwrap().downcast().unwrap());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 4. Bluetooth status (BlueZ)
|
||||||
|
if let Ok(objects_reply) = c.call_method(
|
||||||
|
Some("org.bluez"),
|
||||||
|
"/",
|
||||||
|
Some("org.freedesktop.DBus.ObjectManager"),
|
||||||
|
"GetManagedObjects",
|
||||||
|
&(),
|
||||||
|
).await {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
type ManagedObjects = HashMap<zbus::zvariant::OwnedObjectPath, HashMap<String, HashMap<String, zbus::zvariant::OwnedValue>>>;
|
||||||
|
if let Ok(objects) = objects_reply.body::<ManagedObjects>() {
|
||||||
|
for (_path, interfaces) in objects {
|
||||||
|
if let Some(device) = interfaces.get("org.bluez.Device1") {
|
||||||
|
if let Some(connected) = device.get("Connected") {
|
||||||
|
if connected.downcast_ref::<bool>().copied().unwrap_or(false) {
|
||||||
|
new_status.bluetooth_connected = true;
|
||||||
|
if let Some(name) = device.get("Name") {
|
||||||
|
let name_str: String = name.downcast_ref::<str>().map(|s| s.to_string()).unwrap_or_default();
|
||||||
|
new_status.bluetooth_devices.push(name_str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
{
|
||||||
|
if let Ok(mut s) = s_clone.lock() {
|
||||||
|
*s = new_status;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Some(command) = cmd_rx.recv() => {
|
||||||
|
if let Some(ref c) = conn {
|
||||||
|
let method = match command {
|
||||||
|
SystemCommand::PowerOff => "PowerOff",
|
||||||
|
SystemCommand::Reboot => "Reboot",
|
||||||
|
SystemCommand::Suspend => "Suspend",
|
||||||
|
};
|
||||||
|
|
||||||
|
debug!("Executing system command: {}", method);
|
||||||
|
// Set a timeout for the DBus call to prevent hanging the background thread
|
||||||
|
let result = tokio::time::timeout(
|
||||||
|
tokio::time::Duration::from_secs(5),
|
||||||
|
c.call_method(
|
||||||
|
Some("org.freedesktop.login1"),
|
||||||
|
"/org/freedesktop/login1",
|
||||||
|
Some("org.freedesktop.login1.Manager"),
|
||||||
|
method,
|
||||||
|
&(true),
|
||||||
|
)
|
||||||
|
).await;
|
||||||
|
|
||||||
|
if let Err(_) = result {
|
||||||
|
error!("System command {} timed out", method);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
Self { status, cmd_tx }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_status(&self) -> SystemStatus {
|
||||||
|
self.status.lock().unwrap().clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn send_command(&self, cmd: SystemCommand) {
|
||||||
|
let _ = self.cmd_tx.send(cmd);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn media_play_pause(&self) {
|
||||||
|
if let Ok(finder) = PlayerFinder::new() {
|
||||||
|
if let Ok(player) = finder.find_active() {
|
||||||
|
let _ = player.play_pause();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn media_next(&self) {
|
||||||
|
if let Ok(finder) = PlayerFinder::new() {
|
||||||
|
if let Ok(player) = finder.find_active() {
|
||||||
|
let _ = player.next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn media_prev(&self) {
|
||||||
|
if let Ok(finder) = PlayerFinder::new() {
|
||||||
|
if let Ok(player) = finder.find_active() {
|
||||||
|
let _ = player.previous();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1 +0,0 @@
|
|||||||
|
|
||||||
Reference in New Issue
Block a user