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"
|
||||
|
||||
[dependencies]
|
||||
smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
||||
smithay-client-toolkit = { version = "0.19", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||
wayland-client = { version = "0.31" }
|
||||
wayland-protocols = { version = "0.32", features = ["client"] }
|
||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
cairo-rs = { version = "0.20", features = ["png"] }
|
||||
image = "0.25"
|
||||
fastblur = "0.1"
|
||||
xkbcommon = "0.7"
|
||||
pam-client = "0.5"
|
||||
secstr = "0.5"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
toml = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
cairo-rs = { version = "0.20", default-features = false, features = ["png"] }
|
||||
gdk-pixbuf = { version = "0.20", default-features = false, features = ["v2_40"] }
|
||||
gio = { version = "0.20" }
|
||||
pangocairo = { version = "0.20" }
|
||||
clap = { version = "4.4", default-features = false, features = ["derive", "std", "help", "usage", "error-context"] }
|
||||
toml = { version = "1.0", default-features = false, features = ["parse", "display", "serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||
zeroize = "1.7"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
chrono = "0.4"
|
||||
env_logger = { version = "0.11", default-features = false, features = ["color", "humantime"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
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/releases)
|
||||
[](https://github.com/yourusername/rustlock/commits/main)
|
||||
|
||||
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
|
||||
|
||||
- ⚡ **Performance**: Written in safe Rust with minimal dependencies
|
||||
- ⚡ **Performance**: Written in safe Rust, optimized binary size (~2.4MB without networking, ~4MB with)
|
||||
- 🎨 **Visual Effects**:
|
||||
- Gaussian blur (configurable radius and passes)
|
||||
- Vignette effect (configurable base and factor)
|
||||
- Pixelate, Swirl, and Melting effects
|
||||
- Smooth fade-in animation
|
||||
- 🔐 **Password Indicator**:
|
||||
- Circular ring with configurable radius and thickness
|
||||
- Dynamic key highlight segments that rotate with each keystroke
|
||||
- Customizable colors (ring, inside, separator, highlight)
|
||||
- Caps lock indicator
|
||||
- 🕐 **Information Display**:
|
||||
- Centered clock (HH:MM format)
|
||||
- Full date
|
||||
- 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**:
|
||||
- Captures desktop background before locking
|
||||
- Applies visual effects to background
|
||||
- 🔑 **Authentication**:
|
||||
- Custom background image support
|
||||
- 🔐 **Authentication**:
|
||||
- PAM-based authentication
|
||||
- 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
|
||||
```
|
||||
|
||||
### Session Controls
|
||||
|
||||
When locked, use function keys to control the system:
|
||||
- **F1**: Suspend to RAM
|
||||
- **F2**: Reboot
|
||||
- **F3**: Power Off
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 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
|
||||
|
||||
| Option | Description |
|
||||
|--------|-------------|
|
||||
| **General** | |
|
||||
| `--screenshots` | Capture desktop background before locking |
|
||||
| `--image <PATH>` | Use custom background image instead of screenshot |
|
||||
| `--clock` | Display centered clock and date |
|
||||
| `--indicator` | Show password entry ring (default: true) |
|
||||
| `--indicator-radius <N>` | Ring radius in pixels (default: 100) |
|
||||
| `--indicator-thickness <N>` | Ring thickness in pixels (default: 7) |
|
||||
| **Effects** | |
|
||||
| `--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`) |
|
||||
| **Colors** | |
|
||||
| `--ring-color <RRGGBB[AA]>` | Outer ring color (hex, optional alpha) |
|
||||
| `--key-hl-color <RRGGBB[AA]>` | Key highlight segment color |
|
||||
| `--line-color <RRGGBB[AA]>` | Separator line color |
|
||||
| `--inside-color <RRGGBB[AA]>` | Inner circle 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) |
|
||||
| `--fade-in <SECONDS>` | Fade-in animation duration (default: 0.2) |
|
||||
| `--pam-service <NAME>` | PAM service name (default: "rustlock") |
|
||||
| `--config <PATH>` | Path to config file |
|
||||
| `--theme <NAME>` | Theme preset: dark, light, nord, dracula |
|
||||
| `--debug` | Enable debug logging |
|
||||
| `--log-file` | Write logs to `~/.rustlock.log` |
|
||||
| `--temp-screenshot` | Enable peek feature (press 'p' to temporarily show background) |
|
||||
|
||||
---
|
||||
|
||||
## 📦 Installation
|
||||
|
||||
### Using Nix (Recommended)
|
||||
|
||||
```bash
|
||||
nix-shell -p rustlock
|
||||
```
|
||||
|
||||
Or with flakes:
|
||||
```bash
|
||||
nix run github:yourusername/rustlock
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
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
|
||||
|
||||
- [x] PAM-based authentication
|
||||
- [x] Grace period (any key unlocks within N seconds)
|
||||
- [x] Screenshot capture with blur/vignette effects
|
||||
- [x] Configuration file support (`~/.config/rustlock/config.toml`)
|
||||
- [x] Screenshot capture with blur/vignette/pixelate/swirl/melting effects
|
||||
- [x] Configuration file support (`~/.config/rustlock/config.toml`) with schema validation
|
||||
- [x] Debug logging to `~/.rustlock.log`
|
||||
- [x] Clock and date display
|
||||
- [x] Password indicator ring with rotating highlights
|
||||
|
||||
## 🚧 TODO
|
||||
|
||||
- [ ] Don't hardcode screen resolution (detect dynamically)
|
||||
- [ ] Add multi-monitor support (handle multiple outputs)
|
||||
- [ ] Automatically install PAM configuration file
|
||||
- [ ] Configuration file schema validation
|
||||
- [ ] Additional visual effects (pixelate, swirl, etc.)
|
||||
- [ ] Theme/profile support with presets
|
||||
- [ ] Wayland protocol stability updates
|
||||
- [x] Dynamic screen resolution detection
|
||||
- [x] Full multi-monitor support with different resolutions
|
||||
- [x] Theme/profile support with presets (dark, light, nord, dracula)
|
||||
- [x] Wayland protocol stability fixes
|
||||
- [x] Media control integration (MPRIS support with Album Art)
|
||||
- [x] Battery, WiFi, and Bluetooth status indicators
|
||||
- [x] Custom background image support
|
||||
- [x] Custom icons for status indicators
|
||||
- [x] Keyboard layout indicator
|
||||
- [x] Session management (F1-F3 keys)
|
||||
- [x] Caps lock indicator
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -10,7 +10,10 @@ pkgs.rustPlatform.buildRustPackage {
|
||||
cairo
|
||||
pam
|
||||
gdk-pixbuf
|
||||
librsvg
|
||||
pango
|
||||
libxkbcommon
|
||||
dbus
|
||||
];
|
||||
|
||||
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()
|
||||
.expect("Failed to get username")
|
||||
.to_str()
|
||||
@@ -43,9 +43,16 @@ pub fn create_and_run_auth_loop() -> (channel::Sender<Zeroizing<String>>, channe
|
||||
.to_string();
|
||||
|
||||
let conversation = LockConversation { password: None };
|
||||
let _context = Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
|
||||
.expect("Failed to initialize PAM context");
|
||||
debug!("Prepared to authenticate user '{}'", username);
|
||||
match Context::new(SERVICE_NAME, Some(username.as_str()), conversation) {
|
||||
Ok(_) => {
|
||||
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_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 {
|
||||
password: Some(password),
|
||||
};
|
||||
let mut context =
|
||||
Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
|
||||
.expect("Failed to initialize PAM context");
|
||||
match context.authenticate(Flag::NONE) {
|
||||
Ok(()) => {
|
||||
auth_res_send.send(true).unwrap();
|
||||
match Context::new(SERVICE_NAME, Some(username.as_str()), conversation) {
|
||||
Ok(mut context) => {
|
||||
match context.authenticate(Flag::NONE) {
|
||||
Ok(()) => {
|
||||
auth_res_send.send(true).unwrap();
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Pam authenticate failed with {:?}", err);
|
||||
auth_res_send.send(false).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Pam authenticate failed with {:?}", err);
|
||||
error!("Failed to re-initialize PAM context: {:?}", err);
|
||||
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)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
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,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||
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,
|
||||
|
||||
#[arg(long, default_value = "100")]
|
||||
@@ -27,6 +27,15 @@ pub struct Config {
|
||||
#[arg(long, value_parser = util::parse_vignette_effect)]
|
||||
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)]
|
||||
pub ring_color: (f64, f64, f64, f64),
|
||||
|
||||
@@ -61,17 +70,65 @@ pub struct Config {
|
||||
#[arg(long)]
|
||||
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)]
|
||||
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 {
|
||||
pub fn load() -> Self {
|
||||
let cli_config = Config::parse();
|
||||
use clap::CommandFactory;
|
||||
|
||||
// Use path from CLI if provided, otherwise default
|
||||
let config_path = cli_config.config.clone().unwrap_or_else(|| {
|
||||
let mut config = Config::parse();
|
||||
let cmd = Config::command();
|
||||
let matches = cmd.get_matches();
|
||||
|
||||
// Helper to check if a value was explicitly set on command line
|
||||
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());
|
||||
path.push(".config/rustlock/config.toml");
|
||||
path
|
||||
@@ -79,12 +136,133 @@ impl Config {
|
||||
|
||||
if config_path.exists() {
|
||||
if let Ok(file_content) = std::fs::read_to_string(&config_path) {
|
||||
if let Ok(_file_config) = toml::from_str::<Config>(&file_content) {
|
||||
return cli_config;
|
||||
if let Ok(table) = toml::from_str::<toml::Table>(&file_content) {
|
||||
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 {
|
||||
password_buffer: Zeroizing<String>,
|
||||
cursor_position: usize,
|
||||
config: crate::config::Config,
|
||||
wrong_password_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,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
pub fn new(config: crate::config::Config) -> Self {
|
||||
pub fn new(_config: crate::config::Config) -> Self {
|
||||
Self {
|
||||
password_buffer: Zeroizing::new(String::new()),
|
||||
cursor_position: 0,
|
||||
config,
|
||||
wrong_password_timer: None,
|
||||
key_highlight_timer: None,
|
||||
temp_screenshot_timer: None,
|
||||
temp_screenshot_active: false,
|
||||
caps_lock: false,
|
||||
}
|
||||
}
|
||||
@@ -30,117 +24,46 @@ impl InputHandler {
|
||||
pub fn handle_key_event(
|
||||
&mut self,
|
||||
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||
state: wayland_client::protocol::wl_keyboard::KeyState,
|
||||
utf8: Option<String>,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> InputAction {
|
||||
// Update Caps Lock state
|
||||
self.caps_lock = modifiers.caps_lock;
|
||||
|
||||
// Only process key press events
|
||||
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
||||
return InputAction::None;
|
||||
}
|
||||
|
||||
// Convert keysym to character
|
||||
let ch = self.keysym_to_char(keysym, modifiers);
|
||||
|
||||
match ch {
|
||||
Some('\x08') | Some('\x7f') => {
|
||||
// Backspace or Delete
|
||||
// Handle special keys first using keysym
|
||||
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||
match keysym {
|
||||
Keysym::BackSpace => {
|
||||
if !self.password_buffer.is_empty() && self.cursor_position > 0 {
|
||||
self.cursor_position -= 1;
|
||||
self.password_buffer.remove(self.cursor_position);
|
||||
}
|
||||
InputAction::PasswordChanged
|
||||
return InputAction::PasswordChanged;
|
||||
}
|
||||
Some('\r') | Some('\n') => {
|
||||
// Enter key - submit password
|
||||
Keysym::Return | Keysym::KP_Enter => {
|
||||
let password = self.password_buffer.clone();
|
||||
self.password_buffer.clear();
|
||||
self.cursor_position = 0;
|
||||
InputAction::SubmitPassword(password)
|
||||
return InputAction::SubmitPassword(password);
|
||||
}
|
||||
Some('\x1b') => {
|
||||
// Escape key - cancel
|
||||
InputAction::Cancel
|
||||
Keysym::Escape => {
|
||||
return 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
|
||||
let keysym_value = keysym.raw();
|
||||
|
||||
// Basic ASCII conversion (simplified - real implementation would use xkbcommon)
|
||||
// This is a simplified mapping for demonstration
|
||||
if keysym_value >= 0x20 && keysym_value <= 0x7e {
|
||||
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,
|
||||
};
|
||||
// Use the UTF-8 string provided by SCTK for character input
|
||||
if let Some(txt) = utf8 {
|
||||
for c in txt.chars() {
|
||||
if c.is_ascii() && !c.is_control() {
|
||||
self.password_buffer.insert(self.cursor_position, c);
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(ch)
|
||||
} else {
|
||||
None
|
||||
return InputAction::PasswordChanged;
|
||||
}
|
||||
|
||||
InputAction::None
|
||||
}
|
||||
|
||||
/// Get the current password (for display purposes only - returns masked version)
|
||||
@@ -178,34 +101,6 @@ impl InputHandler {
|
||||
|
||||
/// Update timers (should be called periodically)
|
||||
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,
|
||||
SubmitPassword(Zeroizing<String>),
|
||||
Cancel,
|
||||
TempScreenshot,
|
||||
}
|
||||
|
||||
+16
-17
@@ -6,6 +6,7 @@ use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
||||
use crate::config::Config;
|
||||
use crate::input::{InputAction, InputHandler};
|
||||
use crate::render::Renderer;
|
||||
use crate::system::SystemStatus;
|
||||
use smithay_client_toolkit::seat::keyboard::KeyEvent;
|
||||
use smithay_client_toolkit::shm::slot::SlotPool;
|
||||
|
||||
@@ -19,7 +20,6 @@ pub struct LockedSurface {
|
||||
fade_alpha: f64,
|
||||
wrong_password_shown: bool,
|
||||
key_highlight_shown: bool,
|
||||
temp_screenshot_shown: bool,
|
||||
start_time: Instant,
|
||||
wayland_surface: Option<wl_surface::WlSurface>,
|
||||
output: wl_output::WlOutput,
|
||||
@@ -50,7 +50,6 @@ impl LockedSurface {
|
||||
fade_alpha: 0.0,
|
||||
wrong_password_shown: false,
|
||||
key_highlight_shown: false,
|
||||
temp_screenshot_shown: false,
|
||||
start_time: Instant::now(),
|
||||
wayland_surface: None,
|
||||
output,
|
||||
@@ -108,15 +107,6 @@ impl LockedSurface {
|
||||
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
|
||||
if !self.background_applied {
|
||||
if let Some(ref background) = self.background {
|
||||
@@ -170,11 +160,12 @@ impl LockedSurface {
|
||||
pub fn handle_key_event(
|
||||
&mut self,
|
||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> Option<InputAction> {
|
||||
let action = self.input_handler.handle_key_event(
|
||||
event.keysym,
|
||||
wayland_client::protocol::wl_keyboard::KeyState::Pressed,
|
||||
smithay_client_toolkit::seat::keyboard::Modifiers::default(),
|
||||
event.utf8,
|
||||
modifiers,
|
||||
);
|
||||
|
||||
match action {
|
||||
@@ -204,6 +195,10 @@ impl LockedSurface {
|
||||
self.background = Some(surface);
|
||||
self.background_applied = false;
|
||||
}
|
||||
|
||||
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||
self.renderer.system_status = status;
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LockManager {
|
||||
@@ -252,19 +247,23 @@ impl LockManager {
|
||||
.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;
|
||||
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
|
||||
}
|
||||
|
||||
pub fn toggle_peek(&mut self) {
|
||||
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||
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 render;
|
||||
mod screenshot;
|
||||
mod system;
|
||||
mod timer;
|
||||
mod util;
|
||||
|
||||
use config::Config;
|
||||
use lock::LockManager;
|
||||
use screenshot::{CaptureData, Screenshot, ScreenshotManager};
|
||||
use system::SystemManager;
|
||||
use std::error::Error;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
@@ -125,6 +127,9 @@ struct WaylandLock {
|
||||
unlocking: bool,
|
||||
screenshot_manager: Option<ScreenshotManager>,
|
||||
grace_until: Option<Instant>,
|
||||
system_manager: Arc<SystemManager>,
|
||||
modifiers: Modifiers,
|
||||
current_layout: u32,
|
||||
}
|
||||
|
||||
impl WaylandLock {
|
||||
@@ -163,6 +168,41 @@ impl WaylandLock {
|
||||
}
|
||||
|
||||
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)
|
||||
if let Some(grace_until) = self.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 {
|
||||
log::info!("Enter pressed - submitting password");
|
||||
if let Ok(mut lock_manager) = self.lock_manager.lock() {
|
||||
let mut password = Zeroizing::new(String::new());
|
||||
let modifiers = self.modifiers;
|
||||
for surface in &mut lock_manager.surfaces {
|
||||
if let Some(InputAction::SubmitPassword(p)) =
|
||||
surface.handle_key_event(event.clone())
|
||||
surface.handle_key_event(event.clone(), modifiers)
|
||||
{
|
||||
password = p;
|
||||
}
|
||||
@@ -194,17 +232,12 @@ impl WaylandLock {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let action = self
|
||||
let modifiers = self.modifiers;
|
||||
let _action = self
|
||||
.lock_manager
|
||||
.lock()
|
||||
.map(|mut lm| lm.handle_key_event(event))
|
||||
.map(|mut lm| lm.handle_key_event(event, modifiers))
|
||||
.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>,
|
||||
_keyboard: &wayland_client::protocol::wl_keyboard::WlKeyboard,
|
||||
_serial: u32,
|
||||
_modifiers: Modifiers,
|
||||
_layout: u32,
|
||||
modifiers: Modifiers,
|
||||
layout: u32,
|
||||
) {
|
||||
self.modifiers = modifiers;
|
||||
self.current_layout = layout;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -402,7 +437,9 @@ impl SeatHandler for WaylandLock {
|
||||
&seat,
|
||||
None,
|
||||
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,
|
||||
} => {
|
||||
let format = format.into_result().unwrap();
|
||||
|
||||
let mut info = data.info.lock().unwrap();
|
||||
*info = Some(screenshot::BufferInfo {
|
||||
width,
|
||||
@@ -460,15 +498,22 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
|
||||
stride,
|
||||
format,
|
||||
});
|
||||
match state
|
||||
.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);
|
||||
|
||||
// Create a dedicated pool for this screenshot to ensure offset 0
|
||||
// This matches swaylock-effects and fixes "invalid buffer" on some compositors
|
||||
let size = (stride as usize) * (height as usize);
|
||||
match SlotPool::new(size, &state.shm_state) {
|
||||
Ok(mut pool) => {
|
||||
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 } => {
|
||||
@@ -480,13 +525,15 @@ impl Dispatch<ZwlrScreencopyFrameV1, CaptureData> for WaylandLock {
|
||||
let buffer = data.buffer.lock().unwrap().take();
|
||||
let info = data.info.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 {
|
||||
buffer,
|
||||
info,
|
||||
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 _ = ss.apply_effects(&state.config);
|
||||
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 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()?;
|
||||
|
||||
// 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 {
|
||||
loop_handle: event_loop.handle(),
|
||||
lock_manager: lock_manager.clone(),
|
||||
@@ -563,7 +624,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
registry_state: RegistryState::new(&globals),
|
||||
session_lock_state: SessionLockState::new(&globals, &qh),
|
||||
seat_state: SeatState::new(&globals, &qh),
|
||||
shm_state,
|
||||
shm_state: shm_state,
|
||||
pool,
|
||||
session_lock: None,
|
||||
lock_surfaces: Vec::new(),
|
||||
@@ -575,10 +636,67 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
unlocking: false,
|
||||
screenshot_manager: ScreenshotManager::new(&globals, &qh).ok(),
|
||||
grace_until: None,
|
||||
system_manager: system_manager.clone(),
|
||||
modifiers: Modifiers::default(),
|
||||
current_layout: 0,
|
||||
};
|
||||
|
||||
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 =
|
||||
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));
|
||||
}
|
||||
|
||||
let mut status = state.system_manager.get_status();
|
||||
status.keyboard_layout = Some(state.current_layout);
|
||||
|
||||
if let Ok(mut lm) = state.lock_manager.lock() {
|
||||
lm.set_system_status(status);
|
||||
lm.update();
|
||||
for surface in &mut lm.surfaces {
|
||||
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 {
|
||||
state.outputs = state.output_state.outputs().collect();
|
||||
log::info!(
|
||||
|
||||
+384
-2
@@ -2,6 +2,7 @@ use cairo::{Context, Format, ImageSurface};
|
||||
use std::time::Instant;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::system::SystemStatus;
|
||||
|
||||
/// Cairo-based renderer for the lock screen
|
||||
pub struct Renderer {
|
||||
@@ -21,6 +22,16 @@ pub struct Renderer {
|
||||
uptime_cache: String,
|
||||
last_uptime_update: Option<Instant>,
|
||||
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 {
|
||||
@@ -31,10 +42,10 @@ impl Renderer {
|
||||
.expect("Failed to create Cairo surface");
|
||||
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
||||
|
||||
Self {
|
||||
let mut renderer = Self {
|
||||
width,
|
||||
height,
|
||||
config,
|
||||
config: config.clone(),
|
||||
surface,
|
||||
context,
|
||||
fade_alpha: 0.0,
|
||||
@@ -48,6 +59,85 @@ impl Renderer {
|
||||
uptime_cache: String::new(),
|
||||
last_uptime_update: None,
|
||||
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();
|
||||
}
|
||||
|
||||
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() {
|
||||
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 {
|
||||
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(())
|
||||
}
|
||||
|
||||
/// 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.
|
||||
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
|
||||
if radius == 0 || times == 0 {
|
||||
@@ -304,6 +456,7 @@ pub struct CaptureData {
|
||||
pub info: Mutex<Option<BufferInfo>>,
|
||||
pub flags: Mutex<Option<Flags>>,
|
||||
pub buffer: Mutex<Option<Buffer>>,
|
||||
pub pool: Mutex<Option<SlotPool>>,
|
||||
}
|
||||
|
||||
impl CaptureData {
|
||||
@@ -314,6 +467,7 @@ impl CaptureData {
|
||||
info: Mutex::new(None),
|
||||
flags: 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