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

- 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:
2026-03-16 14:12:43 +01:00
parent 1dc4d68cf7
commit f489a6e46e
15 changed files with 3582 additions and 829 deletions
+108
View File
@@ -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
+156
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+39 -15
View File
@@ -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
+85 -20
View File
@@ -2,7 +2,6 @@
[![License](https://img.shields.io/badge/license-AGPL--3.0%2B-blue.svg)](https://github.com/yourusername/rustlock/blob/main/LICENSE) [![License](https://img.shields.io/badge/license-AGPL--3.0%2B-blue.svg)](https://github.com/yourusername/rustlock/blob/main/LICENSE)
[![Version](https://img.shields.io/badge/version-0.1.0-green.svg)](https://github.com/yourusername/rustlock/releases) [![Version](https://img.shields.io/badge/version-0.1.0-green.svg)](https://github.com/yourusername/rustlock/releases)
[![Updated](https://img.shields.io/badge/updated-2026--03--07-blue.svg)](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
--- ---
+3
View File
@@ -10,7 +10,10 @@ pkgs.rustPlatform.buildRustPackage {
cairo cairo
pam pam
gdk-pixbuf gdk-pixbuf
librsvg
pango
libxkbcommon libxkbcommon
dbus
]; ];
nativeBuildInputs = [ nativeBuildInputs = [
+24 -12
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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();
}
}
} }
+154
View File
@@ -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
View File
@@ -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
View File
@@ -1 +0,0 @@