Compare commits
19 Commits
7cdcf19415
...
v1.0
| Author | SHA1 | Date | |
|---|---|---|---|
|
601c9677b7
|
|||
|
4d90e52c36
|
|||
|
55c8fc0a4f
|
|||
|
aa59067bb3
|
|||
|
c05d987ff1
|
|||
|
a5115c0a15
|
|||
|
20cc6e210e
|
|||
|
3038c29c57
|
|||
|
50c5de5371
|
|||
|
446426ea47
|
|||
| f4f8e94e6e | |||
|
0309522e25
|
|||
|
5e47f8a69c
|
|||
|
f489a6e46e
|
|||
| 1dc4d68cf7 | |||
| add5c9f4f6 | |||
| ab960c5f0c | |||
| 5b3377a15b | |||
| 88f7833d8d |
@@ -0,0 +1,17 @@
|
|||||||
|
name: Install system dependencies
|
||||||
|
description: Install system dependencies required for building rustlock
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: composite
|
||||||
|
steps:
|
||||||
|
- name: Install system dependencies (Ubuntu/Debian)
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y \
|
||||||
|
curl \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libgdk-pixbuf-2.0-dev libpam0g-dev libdbus-1-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
name: Code Quality
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: ["master", "main"]
|
||||||
|
pull_request:
|
||||||
|
branches: ["master", "main"]
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
quality-checks:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- uses: ./.github/actions/deps
|
||||||
|
|
||||||
|
- name: Install Rust with tools
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
with:
|
||||||
|
components: rustfmt, clippy
|
||||||
|
|
||||||
|
- name: Check formatting
|
||||||
|
run: cargo fmt -- --check
|
||||||
|
|
||||||
|
- name: Run clippy
|
||||||
|
run: cargo clippy -- -D warnings
|
||||||
|
|
||||||
|
- name: Run unit tests
|
||||||
|
run: cargo test --quiet
|
||||||
|
|
||||||
|
- name: Check documentation
|
||||||
|
run: cargo doc --no-deps --document-private-items
|
||||||
@@ -0,0 +1,113 @@
|
|||||||
|
name: Release
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
release-build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- variant: "default"
|
||||||
|
args: ""
|
||||||
|
distro: "ubuntu"
|
||||||
|
- variant: "no-networking"
|
||||||
|
args: "--no-default-features"
|
||||||
|
distro: "ubuntu"
|
||||||
|
- variant: "default"
|
||||||
|
args: ""
|
||||||
|
distro: "debian"
|
||||||
|
- variant: "default"
|
||||||
|
args: ""
|
||||||
|
distro: "fedora"
|
||||||
|
- variant: "default"
|
||||||
|
args: ""
|
||||||
|
distro: "arch"
|
||||||
|
container: ${{ matrix.distro == 'ubuntu' && 'ubuntu:latest' || matrix.distro == 'debian' && 'debian:stable-slim' || matrix.distro == 'fedora' && 'fedora:latest' || 'archlinux:latest' }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Install system dependencies (Ubuntu/Debian)
|
||||||
|
if: matrix.distro == 'ubuntu' || matrix.distro == 'debian'
|
||||||
|
run: |
|
||||||
|
apt-get update
|
||||||
|
apt-get install -y \
|
||||||
|
curl \
|
||||||
|
llvm clang libclang-dev \
|
||||||
|
pkg-config \
|
||||||
|
libglib2.0-dev libcairo2-dev libpango1.0-dev libatk1.0-dev \
|
||||||
|
libgdk-pixbuf-2.0-dev libpam0g-dev libdbus-1-dev \
|
||||||
|
libwayland-dev libxkbcommon-dev
|
||||||
|
|
||||||
|
- name: Install system dependencies (Fedora)
|
||||||
|
if: matrix.distro == 'fedora'
|
||||||
|
run: |
|
||||||
|
dnf install -y \
|
||||||
|
curl \
|
||||||
|
llvm clang clang-devel \
|
||||||
|
pkgconfig \
|
||||||
|
glib2-devel cairo-devel cairo-gobject-devel pango-devel atk-devel \
|
||||||
|
gdk-pixbuf2-devel pam-devel dbus-devel \
|
||||||
|
wayland-devel libxkbcommon-devel
|
||||||
|
|
||||||
|
- name: Install system dependencies (Arch)
|
||||||
|
if: matrix.distro == 'arch'
|
||||||
|
run: |
|
||||||
|
pacman -Sy --noconfirm \
|
||||||
|
base-devel \
|
||||||
|
llvm clang pkgconf \
|
||||||
|
glib2 cairo pango atk gdk-pixbuf2 \
|
||||||
|
pam dbus \
|
||||||
|
wayland libxkbcommon
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Build ${{ matrix.variant }} (${{ matrix.distro }})
|
||||||
|
run: cargo build --release ${{ matrix.args }}
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: rustlock-${{ matrix.variant }}-${{ matrix.distro }}
|
||||||
|
path: target/release/rustlock
|
||||||
|
|
||||||
|
release:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
needs: [release-build]
|
||||||
|
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@v2
|
||||||
|
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/rustlock:rustlock-default-ubuntu
|
||||||
|
artifacts/rustlock-no-networking-ubuntu/rustlock/rustlock:rustlock-no-networking-ubuntu
|
||||||
|
artifacts/rustlock-default-debian/rustlock/rustlock:rustlock-default-debian
|
||||||
|
artifacts/rustlock-default-fedora/rustlock/rustlock:rustlock-default-fedora
|
||||||
|
artifacts/rustlock-default-arch/rustlock/rustlock:rustlock-default-arch
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
name: Security Scan
|
||||||
|
|
||||||
|
on:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * 0' # Weekly on Sunday at midnight
|
||||||
|
workflow_dispatch: # Manual trigger
|
||||||
|
push:
|
||||||
|
branches: [main, master]
|
||||||
|
paths:
|
||||||
|
- 'Cargo.toml'
|
||||||
|
- 'Cargo.lock'
|
||||||
|
- 'deny.toml'
|
||||||
|
- '.github/workflows/security.yml'
|
||||||
|
|
||||||
|
env:
|
||||||
|
CARGO_TERM_COLOR: always
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
security-audit:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: Swatinem/rust-cache@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Install security tools
|
||||||
|
run: |
|
||||||
|
cargo install cargo-audit
|
||||||
|
cargo install cargo-deny
|
||||||
|
|
||||||
|
- name: Run cargo audit
|
||||||
|
run: cargo audit
|
||||||
|
|
||||||
|
- name: Run cargo deny
|
||||||
|
run: cargo deny check
|
||||||
|
|
||||||
|
- name: Generate Software Bill of Materials (SBOM)
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cyclonedx
|
||||||
|
cargo cyclonedx --format json --output bom.json
|
||||||
|
|
||||||
|
- name: Upload SBOM
|
||||||
|
uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: sbom
|
||||||
|
path: bom.json
|
||||||
|
retention-days: 90
|
||||||
|
|
||||||
|
- name: Check for outdated dependencies
|
||||||
|
run: |
|
||||||
|
cargo install cargo-outdated
|
||||||
|
cargo outdated --exit-code 1 || echo "Some dependencies are outdated"
|
||||||
|
|
||||||
|
- name: Security summary
|
||||||
|
run: |
|
||||||
|
echo "=== Security Scan Complete ==="
|
||||||
|
echo "✅ cargo audit - Vulnerability scanning"
|
||||||
|
echo "✅ cargo deny - Advisory and license checking"
|
||||||
|
echo "✅ SBOM generated - Software Bill of Materials"
|
||||||
|
echo "✅ Outdated dependencies checked"
|
||||||
|
echo ""
|
||||||
|
echo "Next scheduled scan: Weekly (Sunday 00:00 UTC)"
|
||||||
Generated
+2025
-616
File diff suppressed because it is too large
Load Diff
+41
-17
@@ -1,26 +1,50 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "wayrustlock"
|
name = "rustlock"
|
||||||
version = "0.1.0"
|
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"
|
whoami = "1.0"
|
||||||
|
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
|
||||||
|
|||||||
@@ -0,0 +1,661 @@
|
|||||||
|
GNU AFFERO GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 19 November 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <http://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU Affero General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works, specifically designed to ensure
|
||||||
|
cooperation with the community in the case of network server software.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
our General Public Licenses are intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
Developers that use our General Public Licenses protect your rights
|
||||||
|
with two steps: (1) assert copyright on the software, and (2) offer
|
||||||
|
you this License which gives you legal permission to copy, distribute
|
||||||
|
and/or modify the software.
|
||||||
|
|
||||||
|
A secondary benefit of defending all users' freedom is that
|
||||||
|
improvements made in alternate versions of the program, if they
|
||||||
|
receive widespread use, become available for other developers to
|
||||||
|
incorporate. Many developers of free software are heartened and
|
||||||
|
encouraged by the resulting cooperation. However, in the case of
|
||||||
|
software used on network servers, this result may fail to come about.
|
||||||
|
The GNU General Public License permits making a modified version and
|
||||||
|
letting the public access it on a server without ever releasing its
|
||||||
|
source code to the public.
|
||||||
|
|
||||||
|
The GNU Affero General Public License is designed specifically to
|
||||||
|
ensure that, in such cases, the modified source code becomes available
|
||||||
|
to the community. It requires the operator of a network server to
|
||||||
|
provide the source code of the modified version running there to the
|
||||||
|
users of that server. Therefore, public use of a modified version, on
|
||||||
|
a publicly accessible server, gives the public access to the source
|
||||||
|
code of the modified version.
|
||||||
|
|
||||||
|
An older license, called the Affero General Public License and
|
||||||
|
published by Affero, was designed to accomplish similar goals. This is
|
||||||
|
a different license, not a version of the Affero GPL, but Affero has
|
||||||
|
released a new version of the Affero GPL which permits relicensing under
|
||||||
|
this license.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU Affero General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Remote Network Interaction; Use with the GNU General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, if you modify the
|
||||||
|
Program, your modified version must prominently offer all users
|
||||||
|
interacting with it remotely through a computer network (if your version
|
||||||
|
supports such interaction) an opportunity to receive the Corresponding
|
||||||
|
Source of your version by providing access to the Corresponding Source
|
||||||
|
from a network server at no charge, through some standard or customary
|
||||||
|
means of facilitating copying of software. This Corresponding Source
|
||||||
|
shall include the Corresponding Source for any work covered by version 3
|
||||||
|
of the GNU General Public License that is incorporated pursuant to the
|
||||||
|
following paragraph.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the work with which it is combined will remain governed by version
|
||||||
|
3 of the GNU General Public License.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU Affero General Public License from time to time. Such new versions
|
||||||
|
will be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU Affero General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU Affero General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU Affero General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU Affero General Public License as published
|
||||||
|
by the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU Affero General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU Affero General Public License
|
||||||
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If your software can interact with users remotely through a computer
|
||||||
|
network, you should also make sure that it provides a way for users to
|
||||||
|
get its source. For example, if your program is a web application, its
|
||||||
|
interface could display a "Source" link that leads users to an archive
|
||||||
|
of the code. There are many ways you could offer source, and different
|
||||||
|
solutions will be better for different programs; see section 13 for the
|
||||||
|
specific requirements.
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU AGPL, see
|
||||||
|
<http://www.gnu.org/licenses/>.
|
||||||
@@ -1,149 +1,195 @@
|
|||||||
# wayrustlock
|
# 🔒 RustLock
|
||||||
|
|
||||||
A production-ready Wayland screen locker inspired by swaylock-effects.
|
[](https://github.com/yourusername/rustlock/blob/main/LICENSE)
|
||||||
|
[](https://github.com/yourusername/rustlock/releases)
|
||||||
|
|
||||||
## ⚠️ SAFETY WARNING - READ BEFORE USE
|
A high-performance Wayland screen locker written in Rust, inspired by `swaylock-effects`.
|
||||||
|
|
||||||
**This tool is under active development.** Screen lockers can cause system lockups if they malfunction.
|
---
|
||||||
|
|
||||||
**If the screen locker gets stuck:**
|
## ✨ Features
|
||||||
- Type password and press **Enter** to unlock (demo mode - any password works)
|
|
||||||
- Switch to another TTY: Press `Ctrl+Alt+F2`, login, then run `pkill -9 wayrustlock`
|
|
||||||
- From another terminal: `pkill -9 wayrustlock` or `killall wayrustlock`
|
|
||||||
- If screen is black/red: hard restart may be required
|
|
||||||
|
|
||||||
**Debug logging:** Check `~/.wayrustlock.log` to see what's happening
|
- ⚡ **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
|
||||||
|
- 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
|
||||||
|
- Custom background image support
|
||||||
|
- 🔐 **Authentication**:
|
||||||
|
- PAM-based authentication
|
||||||
|
- Configurable grace period (any key press within N seconds unlocks without password)
|
||||||
|
- 🎯 **Customization**:
|
||||||
|
- Custom icons for WiFi, Bluetooth, Battery
|
||||||
|
- Theme presets (dark, light, nord, dracula)
|
||||||
|
- Configuration via config file or CLI
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🚀 Usage
|
||||||
|
|
||||||
|
### Basic Example
|
||||||
|
|
||||||
**Test with timeout first:**
|
|
||||||
```bash
|
```bash
|
||||||
timeout 15 ./target/release/wayrustlock --indicator --clock
|
rustlock --screenshots --effect-blur 7x5 --effect-vignette 0.5:0.5
|
||||||
```
|
```
|
||||||
|
|
||||||
Then check the log file:
|
### Full Configuration
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
cat ~/.wayrustlock.log
|
rustlock \
|
||||||
|
--screenshots \
|
||||||
|
--clock \
|
||||||
|
--indicator \
|
||||||
|
--indicator-radius 100 \
|
||||||
|
--indicator-thickness 7 \
|
||||||
|
--effect-blur 7x5 \
|
||||||
|
--effect-vignette 0.5:0.5 \
|
||||||
|
--ring-color 785412 \
|
||||||
|
--key-hl-color 4EAC41 \
|
||||||
|
--line-color 00000000 \
|
||||||
|
--inside-color 00000088 \
|
||||||
|
--separator-color 00000000 \
|
||||||
|
--grace 2 \
|
||||||
|
--fade-in 0.2
|
||||||
```
|
```
|
||||||
|
|
||||||
## Features (Implemented vs Planned)
|
### Session Controls
|
||||||
|
|
||||||
### ✅ Implemented
|
When locked, use function keys to control the system:
|
||||||
- Session locking via ext-session-lock-v1 protocol (tested on sway)
|
- **F1**: Suspend to RAM
|
||||||
- Buffer creation from Cairo surfaces (wl_shm)
|
- **F2**: Reboot
|
||||||
- CLI argument parsing with all swaylock-effects options
|
- **F3**: Power Off
|
||||||
- PAM authentication infrastructure (using pam-client crate)
|
|
||||||
- Keyboard handler with proper KeyEvent processing
|
|
||||||
- Module architecture (auth, input, lock, render, screenshot, timer, util)
|
|
||||||
|
|
||||||
### 🔄 In Progress
|
---
|
||||||
- Screenshot capture (wlr-screencopy protocol not yet integrated)
|
|
||||||
- Full PAM integration with auth loop
|
|
||||||
|
|
||||||
### ❌ Not Yet Implemented
|
## ⚙️ Configuration
|
||||||
- Real screenshot capture (currently shows solid color background)
|
|
||||||
- Grace period and fade-in animations
|
|
||||||
|
|
||||||
## Installation
|
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` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📦 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
|
||||||
```
|
```
|
||||||
|
|
||||||
## Usage
|
The binary will be available at `target/release/rustlock`.
|
||||||
|
|
||||||
Basic usage:
|
### Build Options
|
||||||
```bash
|
|
||||||
wayrustlock
|
|
||||||
```
|
|
||||||
|
|
||||||
With all options from swaylock-effects compatibility:
|
- **With networking** (default): Includes reqwest for album art fetching
|
||||||
```bash
|
```bash
|
||||||
wayrustlock \
|
cargo build --release --features networking
|
||||||
--screenshots \
|
```
|
||||||
--clock \
|
|
||||||
--indicator \
|
|
||||||
--indicator-radius 100 \
|
|
||||||
--indicator-thickness 7 \
|
|
||||||
--effect-blur 7x5 \
|
|
||||||
--effect-vignette 0.5:0.5 \
|
|
||||||
--ring-color 785412 \
|
|
||||||
--key-hl-color 4EAC41 \
|
|
||||||
--line-color 00000000 \
|
|
||||||
--inside-color 00000088 \
|
|
||||||
--separator-color 00000000 \
|
|
||||||
--grace 2 \
|
|
||||||
--fade-in 0.2
|
|
||||||
```
|
|
||||||
|
|
||||||
## Command-Line Options
|
- **Without networking**: Smaller binary (~2.4MB)
|
||||||
|
```bash
|
||||||
|
cargo build --release --no-default-features
|
||||||
|
```
|
||||||
|
|
||||||
| Option | Description | Default |
|
---
|
||||||
|--------|-------------|---------|
|
|
||||||
| `--screenshots` | Take screenshots of each output as background | false |
|
|
||||||
| `--clock` | Show clock in center of screen | false |
|
|
||||||
| `--indicator` | Show password indicator ring | false |
|
|
||||||
| `--indicator-radius` | Radius of indicator ring in pixels | 100 |
|
|
||||||
| `--indicator-thickness` | Thickness of indicator ring in pixels | 7 |
|
|
||||||
| `--effect-blur` | Blur radius and iterations (e.g., 7x5) | none |
|
|
||||||
| `--effect-vignette` | Vignette base:factor (e.g., 0.5:0.5) | none |
|
|
||||||
| `--ring-color` | Ring color (hex RRGGBB) | 785412 |
|
|
||||||
| `--key-hl-color` | Key press highlight color | 4EAC41 |
|
|
||||||
| `--line-color` | Line color | 00000000 |
|
|
||||||
| `--inside-color` | Inside fill color | 00000088 |
|
|
||||||
| `--separator-color` | Separator color | 00000000 |
|
|
||||||
| `--grace` | Grace period in seconds before password required | 2 |
|
|
||||||
| `--fade-in` | Fade-in duration in seconds | 0.2 |
|
|
||||||
| `--pam-service` | PAM service name | login |
|
|
||||||
| `--config` | Path to TOML config file | none |
|
|
||||||
| `--debug` | Enable debug logging | false |
|
|
||||||
|
|
||||||
## Configuration File
|
## ✅ Completed
|
||||||
|
|
||||||
You can also use a TOML configuration file:
|
- [x] PAM-based authentication
|
||||||
|
- [x] Grace period (any key unlocks within N seconds)
|
||||||
|
- [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
|
||||||
|
- [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
|
||||||
|
|
||||||
```toml
|
---
|
||||||
screenshots = true
|
|
||||||
clock = true
|
|
||||||
indicator = true
|
|
||||||
indicator_radius = 100
|
|
||||||
indicator_thickness = 7
|
|
||||||
effect_blur = "7x5"
|
|
||||||
effect_vignette = "0.5:0.5"
|
|
||||||
ring_color = "785412"
|
|
||||||
key_hl_color = "4EAC41"
|
|
||||||
line_color = "00000000"
|
|
||||||
inside_color = "00000088"
|
|
||||||
separator_color = "00000000"
|
|
||||||
grace = 2
|
|
||||||
fade_in = 0.2
|
|
||||||
pam_service = "login"
|
|
||||||
```
|
|
||||||
|
|
||||||
## Dependencies
|
## 📄 License
|
||||||
|
|
||||||
- Wayland compositor (sway, labwc, etc.)
|
AGPL v3+
|
||||||
- PAM (linux-pam)
|
|
||||||
- Required Wayland protocols:
|
|
||||||
- ext-session-lock-v1
|
|
||||||
- wlr-screencopy-unstable-v1
|
|
||||||
|
|
||||||
## Building
|
|
||||||
|
|
||||||
This project requires Rust 2021 edition and the following dependencies:
|
|
||||||
|
|
||||||
- wayland development libraries
|
|
||||||
- cairo development libraries
|
|
||||||
- pam development libraries
|
|
||||||
|
|
||||||
On Debian/Ubuntu:
|
|
||||||
```bash
|
|
||||||
sudo apt install libwayland-dev libcairo2-dev libpam0g-dev
|
|
||||||
```
|
|
||||||
|
|
||||||
On Fedora:
|
|
||||||
```bash
|
|
||||||
sudo dnf install wayland-devel cairo-devel pam-devel
|
|
||||||
```
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
MIT
|
|
||||||
|
|||||||
+25
@@ -0,0 +1,25 @@
|
|||||||
|
{ pkgs ? import <nixpkgs> {} }:
|
||||||
|
|
||||||
|
pkgs.rustPlatform.buildRustPackage {
|
||||||
|
pname = "rustlock";
|
||||||
|
version = "0.1.0";
|
||||||
|
src = ./.;
|
||||||
|
cargoLock = { lockFile = ./Cargo.lock; };
|
||||||
|
|
||||||
|
buildInputs = with pkgs; [
|
||||||
|
cairo
|
||||||
|
pam
|
||||||
|
gdk-pixbuf
|
||||||
|
librsvg
|
||||||
|
pango
|
||||||
|
libxkbcommon
|
||||||
|
dbus
|
||||||
|
];
|
||||||
|
|
||||||
|
nativeBuildInputs = [
|
||||||
|
pkgs.pkg-config
|
||||||
|
pkgs.rustPlatform.bindgenHook
|
||||||
|
pkgs.rustfmt
|
||||||
|
pkgs.clippy
|
||||||
|
];
|
||||||
|
}
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
[advisories]
|
||||||
|
vulnerability = "deny"
|
||||||
|
ignore = []
|
||||||
|
|
||||||
|
[bans]
|
||||||
|
multiple-versions = "allow"
|
||||||
|
|
||||||
|
[sources]
|
||||||
|
allow-registry = ["https://github.com/rust-lang/crates.io-index"]
|
||||||
|
allow-git = []
|
||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
# PAM configuration for wayrustlock
|
# PAM configuration for rustlock
|
||||||
# Install this file to /etc/pam.d/wayrustlock
|
# Install this file to /etc/pam.d/rustlock
|
||||||
|
|
||||||
# Use the standard login service authentication
|
# Use the standard login service authentication
|
||||||
auth include login
|
auth include login
|
||||||
|
|||||||
+41
-39
@@ -4,10 +4,10 @@ use std::thread;
|
|||||||
use log::{debug, error};
|
use log::{debug, error};
|
||||||
use pam_client::{Context, ErrorCode, Flag};
|
use pam_client::{Context, ErrorCode, Flag};
|
||||||
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
|
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
|
||||||
use users::get_current_username;
|
use whoami::username;
|
||||||
use zeroize::Zeroizing;
|
use zeroize::Zeroizing;
|
||||||
|
|
||||||
const SERVICE_NAME: &str = "wayrustlock";
|
const SERVICE_NAME: &str = "rustlock";
|
||||||
|
|
||||||
pub struct LockConversation {
|
pub struct LockConversation {
|
||||||
pub password: Option<Zeroizing<String>>,
|
pub password: Option<Zeroizing<String>>,
|
||||||
@@ -35,59 +35,61 @@ 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(
|
||||||
struct AuthLoopState {
|
) -> Option<(channel::Sender<Zeroizing<String>>, channel::Channel<bool>)> {
|
||||||
auth_res_send: channel::Sender<bool>,
|
let username = username();
|
||||||
main_closed: bool,
|
|
||||||
context: pam_client::Context<LockConversation>,
|
|
||||||
}
|
|
||||||
|
|
||||||
let username = get_current_username()
|
|
||||||
.expect("Failed to get username")
|
|
||||||
.to_str()
|
|
||||||
.expect("Failed to get non-unicode username")
|
|
||||||
.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>();
|
||||||
|
|
||||||
thread::spawn(move || {
|
thread::spawn(move || {
|
||||||
let mut event_loop: EventLoop<AuthLoopState> = EventLoop::try_new().unwrap();
|
let mut event_loop: EventLoop<()> = EventLoop::try_new().unwrap();
|
||||||
event_loop
|
event_loop
|
||||||
.handle()
|
.handle()
|
||||||
.insert_source(auth_req_recv, |evt, _metadata, state| match evt {
|
.insert_source(auth_req_recv, |evt, _metadata, _state| match evt {
|
||||||
channel::Event::Msg(password) => {
|
channel::Event::Msg(password) => {
|
||||||
state.context.conversation_mut().password = Some(password);
|
let conversation = LockConversation {
|
||||||
let status = match state.context.authenticate(Flag::NONE) {
|
password: Some(password),
|
||||||
Ok(()) => true,
|
|
||||||
Err(err) => {
|
|
||||||
error!("Pam authenticate failed with {:?}", err);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
state.auth_res_send.send(status).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!("Failed to re-initialize PAM context: {:?}", err);
|
||||||
|
auth_res_send.send(false).unwrap();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
channel::Event::Closed => state.main_closed = true,
|
channel::Event::Closed => {}
|
||||||
})
|
})
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
let mut state = AuthLoopState {
|
loop {
|
||||||
auth_res_send,
|
event_loop.dispatch(None, &mut ()).expect("Failed to run");
|
||||||
main_closed: false,
|
|
||||||
context,
|
|
||||||
};
|
|
||||||
|
|
||||||
while !state.main_closed {
|
|
||||||
event_loop
|
|
||||||
.dispatch(None, &mut state)
|
|
||||||
.expect("Failed to run");
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
(auth_req_send, auth_res_recv)
|
Some((auth_req_send, auth_res_recv))
|
||||||
}
|
}
|
||||||
|
|||||||
+237
-17
@@ -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,12 +27,36 @@ 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),
|
||||||
|
|
||||||
#[arg(long, default_value = "4EAC41", value_parser = util::parse_hex_color)]
|
#[arg(long, default_value = "4EAC41", value_parser = util::parse_hex_color)]
|
||||||
pub key_hl_color: (f64, f64, f64, f64),
|
pub key_hl_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "4EAC41", value_parser = util::parse_hex_color)]
|
||||||
|
pub caps_lock_key_hl_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "DB3300", value_parser = util::parse_hex_color)]
|
||||||
|
pub caps_lock_bs_hl_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "E5A445", value_parser = util::parse_hex_color)]
|
||||||
|
pub caps_lock_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, default_value = "E5A445", value_parser = util::parse_hex_color)]
|
||||||
|
pub caps_lock_text_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
|
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||||
|
pub show_caps_lock_text: bool,
|
||||||
|
|
||||||
#[arg(long, default_value = "00000000", value_parser = util::parse_hex_color)]
|
#[arg(long, default_value = "00000000", value_parser = util::parse_hex_color)]
|
||||||
pub line_color: (f64, f64, f64, f64),
|
pub line_color: (f64, f64, f64, f64),
|
||||||
|
|
||||||
@@ -57,27 +81,223 @@ pub struct Config {
|
|||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub debug: bool,
|
pub debug: bool,
|
||||||
|
|
||||||
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
/// Write verbose logs to ~/.rustlock.log
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
pub temp_screenshot: bool,
|
pub log_file: bool,
|
||||||
|
|
||||||
|
#[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, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||||
|
pub show_keyboard_layout: bool,
|
||||||
|
|
||||||
|
#[arg(long)]
|
||||||
|
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 mut config = Config::parse();
|
use clap::CommandFactory;
|
||||||
|
|
||||||
if let Some(config_path) = &config.config {
|
let mut config = Config::parse();
|
||||||
if let Ok(file_content) = std::fs::read_to_string(config_path) {
|
let cmd = Config::command();
|
||||||
if let Ok(file_config) = toml::from_str::<Config>(&file_content) {
|
let matches = cmd.get_matches();
|
||||||
config = file_config;
|
|
||||||
} else {
|
// Helper to check if a value was explicitly set on command line
|
||||||
eprintln!(
|
let is_cli =
|
||||||
"Warning: Failed to parse config file {}",
|
|key: &str| matches.value_source(key) == Some(clap::parser::ValueSource::CommandLine);
|
||||||
config_path.display()
|
|
||||||
);
|
// 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
|
||||||
|
});
|
||||||
|
|
||||||
|
if config_path.exists() {
|
||||||
|
if let Ok(file_content) = std::fs::read_to_string(&config_path) {
|
||||||
|
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");
|
||||||
|
merge_bool(&mut config.show_keyboard_layout, "show_keyboard_layout");
|
||||||
|
|
||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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);
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
eprintln!("Warning: Config file {} not found", config_path.display());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+29
-142
@@ -4,23 +4,19 @@ 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>,
|
caps_lock: bool,
|
||||||
temp_screenshot_active: 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,
|
caps_lock: false,
|
||||||
temp_screenshot_active: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -28,114 +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 {
|
||||||
// Only process key press events
|
// Update Caps Lock state
|
||||||
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
self.caps_lock = modifiers.caps_lock;
|
||||||
return InputAction::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Convert keysym to character
|
// Handle special keys first using keysym
|
||||||
let ch = self.keysym_to_char(keysym, modifiers);
|
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||||
|
match keysym {
|
||||||
match ch {
|
Keysym::BackSpace => {
|
||||||
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)
|
||||||
@@ -143,17 +71,6 @@ impl InputHandler {
|
|||||||
self.password_buffer.chars().map(|_| '•').collect()
|
self.password_buffer.chars().map(|_| '•').collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the actual password (for authentication)
|
|
||||||
pub fn get_password(&self) -> Zeroizing<String> {
|
|
||||||
self.password_buffer.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clear the password buffer (e.g., after wrong password)
|
|
||||||
pub fn clear_password(&mut self) {
|
|
||||||
self.password_buffer.clear();
|
|
||||||
self.cursor_position = 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set wrong password feedback timer
|
/// Set wrong password feedback timer
|
||||||
pub fn set_wrong_password_feedback(&mut self) {
|
pub fn set_wrong_password_feedback(&mut self) {
|
||||||
self.wrong_password_timer = Some(std::time::Instant::now());
|
self.wrong_password_timer = Some(std::time::Instant::now());
|
||||||
@@ -183,40 +100,11 @@ 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)
|
/// Get the current Caps Lock state
|
||||||
pub fn activate_temp_screenshot(&mut self) {
|
pub fn caps_lock(&self) -> bool {
|
||||||
self.temp_screenshot_timer = Some(std::time::Instant::now());
|
self.caps_lock
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if temp screenshot is currently active
|
|
||||||
pub fn is_temp_screenshot_active(&self) -> bool {
|
|
||||||
self.temp_screenshot_active
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -227,5 +115,4 @@ pub enum InputAction {
|
|||||||
PasswordChanged,
|
PasswordChanged,
|
||||||
SubmitPassword(Zeroizing<String>),
|
SubmitPassword(Zeroizing<String>),
|
||||||
Cancel,
|
Cancel,
|
||||||
TempScreenshot,
|
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-266
@@ -2,29 +2,28 @@ use cairo::ImageSurface;
|
|||||||
use std::error::Error;
|
use std::error::Error;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
||||||
use wayland_client::Proxy;
|
|
||||||
|
|
||||||
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::screenshot::Screenshot;
|
use crate::system::SystemStatus;
|
||||||
|
use smithay_client_toolkit::seat::keyboard::KeyEvent;
|
||||||
use smithay_client_toolkit::shm::slot::SlotPool;
|
use smithay_client_toolkit::shm::slot::SlotPool;
|
||||||
|
|
||||||
/// Manages a locked surface for a single output
|
/// Manages a locked surface for a single output
|
||||||
pub struct LockedSurface {
|
pub struct LockedSurface {
|
||||||
width: i32,
|
|
||||||
height: i32,
|
|
||||||
config: Config,
|
config: Config,
|
||||||
pub renderer: Renderer,
|
pub renderer: Renderer,
|
||||||
input_handler: InputHandler,
|
input_handler: InputHandler,
|
||||||
background: Option<ImageSurface>,
|
background: Option<ImageSurface>,
|
||||||
|
background_applied: bool,
|
||||||
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,
|
||||||
last_update: Instant,
|
|
||||||
wayland_surface: Option<wl_surface::WlSurface>,
|
wayland_surface: Option<wl_surface::WlSurface>,
|
||||||
output: wl_output::WlOutput,
|
output: wl_output::WlOutput,
|
||||||
|
configured: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockedSurface {
|
impl LockedSurface {
|
||||||
@@ -42,123 +41,104 @@ impl LockedSurface {
|
|||||||
let renderer = Renderer::new(width, height, config.clone());
|
let renderer = Renderer::new(width, height, config.clone());
|
||||||
let input_handler = InputHandler::new(config.clone());
|
let input_handler = InputHandler::new(config.clone());
|
||||||
|
|
||||||
// Background will be set later when screenshot is captured (if screenshots enabled)
|
|
||||||
let background = None;
|
|
||||||
|
|
||||||
Some(Self {
|
Some(Self {
|
||||||
width,
|
|
||||||
height,
|
|
||||||
config: config.clone(),
|
config: config.clone(),
|
||||||
renderer,
|
renderer,
|
||||||
input_handler,
|
input_handler,
|
||||||
background,
|
background: None,
|
||||||
|
background_applied: false,
|
||||||
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(),
|
||||||
last_update: Instant::now(),
|
|
||||||
wayland_surface: None,
|
wayland_surface: None,
|
||||||
output,
|
output,
|
||||||
|
configured: false,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Set the configured state
|
||||||
|
pub fn set_configured(&mut self) {
|
||||||
|
log::debug!("LockedSurface: Configured, starting animation");
|
||||||
|
self.configured = true;
|
||||||
|
self.start_time = Instant::now();
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if this surface matches the given Wayland surface
|
/// Check if this surface matches the given Wayland surface
|
||||||
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
||||||
use wayland_client::Proxy;
|
use wayland_client::Proxy;
|
||||||
self.wayland_surface
|
self.wayland_surface
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.map_or(false, |ws| ws.id() == surface.id())
|
.is_some_and(|ws| ws.id() == surface.id())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update the surface state (called on each frame)
|
/// Update the surface state (called on each frame)
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
log::debug!(
|
|
||||||
"LockedSurface::update() called, background: {}",
|
|
||||||
self.background.is_some()
|
|
||||||
);
|
|
||||||
|
|
||||||
// Update timers
|
|
||||||
self.input_handler.update();
|
self.input_handler.update();
|
||||||
|
|
||||||
|
if !self.configured {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Update fade animation
|
// Update fade animation
|
||||||
if self.fade_alpha < 1.0 {
|
if self.fade_alpha < 1.0 {
|
||||||
let elapsed = self.last_update.elapsed();
|
let elapsed = self.start_time.elapsed();
|
||||||
let fade_duration = std::time::Duration::from_secs_f32(self.config.fade_in);
|
let fade_duration = std::time::Duration::from_secs_f32(self.config.fade_in);
|
||||||
self.fade_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
let new_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
if (new_alpha - self.fade_alpha).abs() > 0.001 {
|
||||||
log::debug!("Fade alpha updated: {}", self.fade_alpha);
|
self.fade_alpha = new_alpha;
|
||||||
|
self.renderer.set_fade_alpha(self.fade_alpha);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update visual feedback
|
// Check if we should show/hide wrong password feedback
|
||||||
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
|
if self.input_handler.should_show_wrong_password() && !self.wrong_password_shown {
|
||||||
self.renderer.show_wrong_password();
|
self.renderer.show_wrong_password();
|
||||||
self.wrong_password_shown = true;
|
self.wrong_password_shown = true;
|
||||||
log::debug!("Showing wrong password feedback");
|
|
||||||
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
||||||
self.wrong_password_shown = false;
|
self.wrong_password_shown = false;
|
||||||
log::debug!("Hiding wrong password feedback");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if we should show/hide key highlight feedback
|
||||||
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
|
if self.input_handler.should_show_key_highlight() && !self.key_highlight_shown {
|
||||||
self.renderer.show_key_highlight();
|
self.renderer.show_key_highlight();
|
||||||
self.key_highlight_shown = true;
|
self.key_highlight_shown = true;
|
||||||
log::debug!("Showing key highlight");
|
|
||||||
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
||||||
self.key_highlight_shown = false;
|
self.key_highlight_shown = false;
|
||||||
log::debug!("Hiding key highlight");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle temp screenshot (peek feature)
|
// Update caps lock state in renderer
|
||||||
if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown {
|
self.renderer.caps_lock = self.input_handler.caps_lock();
|
||||||
// When temp screenshot is active, we should show the actual screen
|
|
||||||
// For now, we'll just set a different background alpha
|
|
||||||
self.renderer.set_fade_alpha(0.3); // Semi-transparent
|
|
||||||
self.temp_screenshot_shown = true;
|
|
||||||
log::debug!("Showing temp screenshot (peek)");
|
|
||||||
} else if !self.input_handler.should_show_temp_screenshot() && self.temp_screenshot_shown {
|
|
||||||
// Restore normal fade alpha
|
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
|
||||||
self.temp_screenshot_shown = false;
|
|
||||||
log::debug!("Restored normal fade alpha after peek");
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set background if available
|
// Set background if available and not already applied
|
||||||
if let Some(ref background) = self.background {
|
if !self.background_applied {
|
||||||
let size = (background.width(), background.height());
|
if let Some(ref background) = self.background {
|
||||||
log::info!(
|
log::info!("Applying background image to renderer");
|
||||||
"✓ Applying background from self.background: {}x{}",
|
self.renderer.set_background(background.clone());
|
||||||
size.0,
|
self.background_applied = true;
|
||||||
size.1
|
}
|
||||||
);
|
|
||||||
self.renderer.set_background(background.clone());
|
|
||||||
} else {
|
|
||||||
log::warn!("✗ No background in self.background - will render solid color!");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
self.renderer
|
self.renderer
|
||||||
.set_password_display(self.input_handler.get_display_password());
|
.set_password_display(self.input_handler.get_display_password());
|
||||||
|
|
||||||
// Render the frame
|
|
||||||
self.renderer.render();
|
self.renderer.render();
|
||||||
|
|
||||||
self.last_update = Instant::now();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Commit the rendered frame to the Wayland surface
|
/// Commit the rendered frame to the Wayland surface
|
||||||
pub fn commit(&self, pool: &mut SlotPool) -> Result<(), Box<dyn Error>> {
|
pub fn commit(&self, pool: &mut SlotPool) -> Result<(), Box<dyn Error>> {
|
||||||
// Get pixel data from renderer
|
if !self.configured {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
let pixel_data = self.renderer.get_pixel_data()?;
|
let pixel_data = self.renderer.get_pixel_data()?;
|
||||||
let (width, height, stride) = self.renderer.surface_info();
|
let (width, height, stride) = self.renderer.surface_info();
|
||||||
|
|
||||||
// Create buffer from pool
|
|
||||||
let (buffer, canvas) =
|
let (buffer, canvas) =
|
||||||
pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?;
|
pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?;
|
||||||
|
|
||||||
// Copy pixel data to buffer
|
|
||||||
let copy_len = pixel_data.len().min(canvas.len());
|
let copy_len = pixel_data.len().min(canvas.len());
|
||||||
canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]);
|
canvas[..copy_len].copy_from_slice(&pixel_data[..copy_len]);
|
||||||
|
|
||||||
// Attach buffer to Wayland surface and commit
|
|
||||||
if let Some(wl_surface) = &self.wayland_surface {
|
if let Some(wl_surface) = &self.wayland_surface {
|
||||||
buffer.attach_to(wl_surface)?;
|
buffer.attach_to(wl_surface)?;
|
||||||
wl_surface.damage_buffer(0, 0, width, height);
|
wl_surface.damage_buffer(0, 0, width, height);
|
||||||
@@ -168,204 +148,73 @@ impl LockedSurface {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle resize event from Wayland
|
|
||||||
pub fn resize(&mut self, width: i32, height: i32) {
|
pub fn resize(&mut self, width: i32, height: i32) {
|
||||||
if width <= 0 || height <= 0 {
|
if width <= 0 || height <= 0 {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
self.width = width;
|
|
||||||
self.height = height;
|
|
||||||
self.renderer.resize(width, height);
|
self.renderer.resize(width, height);
|
||||||
|
self.background_applied = false;
|
||||||
// TODO: Re-capture screenshot if screenshots are enabled
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set fade alpha for animation
|
|
||||||
pub fn set_fade_alpha(&mut self, alpha: f64) {
|
|
||||||
self.fade_alpha = alpha.clamp(0.0, 1.0);
|
|
||||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Show wrong password feedback
|
|
||||||
pub fn show_wrong_password(&mut self) {
|
pub fn show_wrong_password(&mut self) {
|
||||||
self.input_handler.set_wrong_password_feedback();
|
self.input_handler.set_wrong_password_feedback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Show key highlight feedback
|
|
||||||
pub fn show_key_highlight(&mut self) {
|
|
||||||
self.input_handler.set_key_highlight();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Handle a key event from Wayland
|
|
||||||
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> {
|
||||||
// Convert to our input handler format
|
|
||||||
// Note: KeyEvent has fields: time, raw_code, keysym, utf8
|
|
||||||
// We need to determine state and modifiers from context (not available in this demo)
|
|
||||||
// For demonstration, we'll assume key press with no modifiers
|
|
||||||
let keysym = event.keysym;
|
|
||||||
let state = wayland_client::protocol::wl_keyboard::KeyState::Pressed;
|
|
||||||
let modifiers = smithay_client_toolkit::seat::keyboard::Modifiers::default();
|
|
||||||
|
|
||||||
let action = self
|
let action = self
|
||||||
.input_handler
|
.input_handler
|
||||||
.handle_key_event(keysym, state, modifiers);
|
.handle_key_event(event.keysym, event.utf8, modifiers);
|
||||||
|
|
||||||
match action {
|
match action {
|
||||||
InputAction::SubmitPassword(password) => {
|
InputAction::PasswordChanged => {
|
||||||
// Show key highlight for visual feedback
|
self.input_handler.set_key_highlight();
|
||||||
self.show_key_highlight();
|
self.key_highlight_shown = false;
|
||||||
Some(InputAction::SubmitPassword(password))
|
|
||||||
}
|
}
|
||||||
InputAction::Cancel => Some(InputAction::Cancel),
|
InputAction::SubmitPassword(_) => {
|
||||||
InputAction::TempScreenshot => Some(InputAction::TempScreenshot),
|
self.input_handler.set_key_highlight();
|
||||||
InputAction::PasswordChanged => Some(InputAction::PasswordChanged),
|
self.key_highlight_shown = false;
|
||||||
InputAction::None => None,
|
}
|
||||||
}
|
_ => {}
|
||||||
}
|
|
||||||
|
|
||||||
/// Authenticate a password using PAM
|
|
||||||
pub fn authenticate_password(&self, password: zeroize::Zeroizing<String>) -> bool {
|
|
||||||
// Create a simple PAM conversation that provides the password
|
|
||||||
struct SimpleConversation {
|
|
||||||
password: Option<zeroize::Zeroizing<String>>,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl pam_client::ConversationHandler for SimpleConversation {
|
Some(action)
|
||||||
fn init(&mut self, _default_user: Option<impl AsRef<str>>) {}
|
|
||||||
|
|
||||||
fn prompt_echo_on(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
|
|
||||||
Err(pam_client::ErrorCode::ABORT)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn prompt_echo_off(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<std::ffi::CString, pam_client::ErrorCode> {
|
|
||||||
if let Some(pwd) = self.password.take() {
|
|
||||||
std::ffi::CString::new(pwd.as_str()).map_err(|_| pam_client::ErrorCode::ABORT)
|
|
||||||
} else {
|
|
||||||
Err(pam_client::ErrorCode::ABORT)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn text_info(&mut self, _msg: &std::ffi::CStr) {}
|
|
||||||
fn error_msg(&mut self, _msg: &std::ffi::CStr) {}
|
|
||||||
fn radio_prompt(
|
|
||||||
&mut self,
|
|
||||||
_msg: &std::ffi::CStr,
|
|
||||||
) -> Result<bool, pam_client::ErrorCode> {
|
|
||||||
Ok(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get username
|
|
||||||
let username = match users::get_current_username() {
|
|
||||||
Some(name) => name.to_string_lossy().into_owned(),
|
|
||||||
None => {
|
|
||||||
log::error!("Failed to get current username");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create PAM context
|
|
||||||
let service_name = &self.config.pam_service;
|
|
||||||
let conversation = SimpleConversation {
|
|
||||||
password: Some(password),
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut context =
|
|
||||||
match pam_client::Context::new(service_name, Some(username.as_str()), conversation) {
|
|
||||||
Ok(ctx) => ctx,
|
|
||||||
Err(e) => {
|
|
||||||
log::error!("Failed to initialize PAM context: {:?}", e);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Authenticate
|
|
||||||
match context.authenticate(pam_client::Flag::NONE) {
|
|
||||||
Ok(()) => {
|
|
||||||
log::info!("PAM authentication successful for user {}", username);
|
|
||||||
true
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
log::warn!("PAM authentication failed: {:?}", e);
|
|
||||||
false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the input handler for this locked surface
|
|
||||||
pub fn input_handler(&self) -> &InputHandler {
|
|
||||||
&self.input_handler
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the rendered image surface for this locked surface
|
|
||||||
pub fn as_image_surface(&self) -> &ImageSurface {
|
|
||||||
self.renderer.as_image_surface()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the current display password (masked)
|
|
||||||
pub fn get_display_password(&self) -> String {
|
|
||||||
self.input_handler.get_display_password()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output dimensions
|
|
||||||
pub fn dimensions(&self) -> (i32, i32) {
|
|
||||||
(self.width, self.height)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the Wayland surface for this locked surface
|
|
||||||
pub fn set_wayland_surface(&mut self, surface: wl_surface::WlSurface) {
|
pub fn set_wayland_surface(&mut self, surface: wl_surface::WlSurface) {
|
||||||
self.wayland_surface = Some(surface);
|
self.wayland_surface = Some(surface);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the Wayland surface for this locked surface
|
|
||||||
pub fn wayland_surface(&self) -> Option<&wl_surface::WlSurface> {
|
|
||||||
self.wayland_surface.as_ref()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the output associated with this locked surface
|
|
||||||
pub fn output(&self) -> &wl_output::WlOutput {
|
pub fn output(&self) -> &wl_output::WlOutput {
|
||||||
&self.output
|
&self.output
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Check if this surface has a Wayland surface attached
|
|
||||||
pub fn has_wayland_surface(&self) -> bool {
|
|
||||||
self.wayland_surface.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set the background image for this locked surface
|
|
||||||
pub fn set_background(&mut self, surface: ImageSurface) {
|
pub fn set_background(&mut self, surface: ImageSurface) {
|
||||||
self.background = Some(surface);
|
self.background = Some(surface);
|
||||||
|
self.background_applied = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||||
|
self.renderer.system_status = status;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Manager for all locked surfaces (multiple outputs)
|
|
||||||
pub struct LockManager {
|
pub struct LockManager {
|
||||||
pub surfaces: Vec<LockedSurface>,
|
pub surfaces: Vec<LockedSurface>,
|
||||||
config: Config,
|
config: Config,
|
||||||
locked: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl LockManager {
|
impl LockManager {
|
||||||
/// Create a new lock manager
|
|
||||||
pub fn new(config: Config) -> Self {
|
pub fn new(config: Config) -> Self {
|
||||||
Self {
|
Self {
|
||||||
surfaces: Vec::new(),
|
surfaces: Vec::new(),
|
||||||
config,
|
config,
|
||||||
locked: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Add a locked surface for an output
|
|
||||||
pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool {
|
pub fn add_surface(&mut self, width: i32, height: i32, output: wl_output::WlOutput) -> bool {
|
||||||
match LockedSurface::new(width, height, &self.config, output) {
|
match LockedSurface::new(width, height, &self.config, output) {
|
||||||
Some(surface) => {
|
Some(surface) => {
|
||||||
@@ -376,69 +225,20 @@ impl LockManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update all locked surfaces
|
|
||||||
pub fn update(&mut self) {
|
pub fn update(&mut self) {
|
||||||
for surface in &mut self.surfaces {
|
for surface in &mut self.surfaces {
|
||||||
surface.update();
|
surface.update();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handle a key event and return any action that needs processing
|
|
||||||
/// Returns the first non-None action from any surface
|
|
||||||
pub fn handle_key_event(
|
|
||||||
&mut self,
|
|
||||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
|
||||||
) -> Option<InputAction> {
|
|
||||||
// Distribute key event to all surfaces and collect first action
|
|
||||||
let mut action = None;
|
|
||||||
for surface in &mut self.surfaces {
|
|
||||||
if let Some(a) = surface.handle_key_event(event.clone()) {
|
|
||||||
action = Some(a);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
action
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if session is locked
|
|
||||||
pub fn is_locked(&self) -> bool {
|
|
||||||
self.locked
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Lock the session
|
|
||||||
pub fn lock(&mut self) {
|
|
||||||
self.locked = true;
|
|
||||||
// TODO: Implement actual Wayland session locking
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Unlock the session
|
|
||||||
pub fn unlock(&mut self) {
|
|
||||||
self.locked = false;
|
|
||||||
// TODO: Implement actual Wayland session unlocking
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get the number of locked surfaces
|
|
||||||
pub fn surface_count(&self) -> usize {
|
pub fn surface_count(&self) -> usize {
|
||||||
self.surfaces.len()
|
self.surfaces.len()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to a locked surface by index
|
|
||||||
pub fn get_surface(&self, index: usize) -> Option<&LockedSurface> {
|
|
||||||
self.surfaces.get(index)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get a mutable reference to a locked surface by index
|
|
||||||
pub fn get_surface_mut(&mut self, index: usize) -> Option<&mut LockedSurface> {
|
pub fn get_surface_mut(&mut self, index: usize) -> Option<&mut LockedSurface> {
|
||||||
self.surfaces.get_mut(index)
|
self.surfaces.get_mut(index)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Toggle temp screenshot peek mode
|
|
||||||
pub fn toggle_peek(&mut self) {
|
|
||||||
for surface in &mut self.surfaces {
|
|
||||||
surface.input_handler.update_temp_screenshot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Find a locked surface by Wayland surface
|
|
||||||
pub fn find_surface_by_wayland_surface(
|
pub fn find_surface_by_wayland_surface(
|
||||||
&mut self,
|
&mut self,
|
||||||
wayland_surface: &wl_surface::WlSurface,
|
wayland_surface: &wl_surface::WlSurface,
|
||||||
@@ -448,13 +248,23 @@ impl LockManager {
|
|||||||
.find(|surface| surface.matches_surface(wayland_surface))
|
.find(|surface| surface.matches_surface(wayland_surface))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Find a locked surface by output
|
pub fn handle_key_event(
|
||||||
pub fn find_surface_by_output(
|
|
||||||
&mut self,
|
&mut self,
|
||||||
output: &wl_output::WlOutput,
|
event: KeyEvent,
|
||||||
) -> Option<&mut LockedSurface> {
|
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||||
self.surfaces
|
) -> Option<InputAction> {
|
||||||
.iter_mut()
|
let mut action = None;
|
||||||
.find(|surface| surface.output().id() == output.id())
|
for surface in &mut self.surfaces {
|
||||||
|
if let Some(a) = surface.handle_key_event(event.clone(), modifiers) {
|
||||||
|
action = Some(a);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
action
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn set_system_status(&mut self, status: SystemStatus) {
|
||||||
|
for surface in &mut self.surfaces {
|
||||||
|
surface.set_system_status(status.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+686
-970
File diff suppressed because it is too large
Load Diff
+598
-163
@@ -2,7 +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::util::Color;
|
use crate::system::SystemStatus;
|
||||||
|
|
||||||
/// Cairo-based renderer for the lock screen
|
/// Cairo-based renderer for the lock screen
|
||||||
pub struct Renderer {
|
pub struct Renderer {
|
||||||
@@ -16,31 +16,36 @@ pub struct Renderer {
|
|||||||
key_highlight_shown: bool,
|
key_highlight_shown: bool,
|
||||||
wrong_password_start: Option<Instant>,
|
wrong_password_start: Option<Instant>,
|
||||||
key_highlight_start: Option<Instant>,
|
key_highlight_start: Option<Instant>,
|
||||||
|
key_highlight_angle: f64,
|
||||||
background: Option<ImageSurface>,
|
background: Option<ImageSurface>,
|
||||||
password_display: String,
|
password_display: String,
|
||||||
|
uptime_cache: String,
|
||||||
|
last_uptime_update: Option<Instant>,
|
||||||
|
pub 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 {
|
||||||
/// Convert color tuple to Color struct
|
|
||||||
fn tuple_to_color(&self, color: (f64, f64, f64, f64)) -> Color {
|
|
||||||
Color {
|
|
||||||
r: (color.0 * 255.0) as u8,
|
|
||||||
g: (color.1 * 255.0) as u8,
|
|
||||||
b: (color.2 * 255.0) as u8,
|
|
||||||
a: (color.3 * 255.0) as u8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create a new renderer with the given dimensions and configuration
|
/// Create a new renderer with the given dimensions and configuration
|
||||||
pub fn new(width: i32, height: i32, config: Config) -> Self {
|
pub fn new(width: i32, height: i32, config: Config) -> Self {
|
||||||
|
log::debug!("Renderer::new({}, {}, ...) called", width, height);
|
||||||
let surface = ImageSurface::create(Format::ARgb32, width, height)
|
let surface = ImageSurface::create(Format::ARgb32, width, height)
|
||||||
.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,13 +53,97 @@ impl Renderer {
|
|||||||
key_highlight_shown: false,
|
key_highlight_shown: false,
|
||||||
wrong_password_start: None,
|
wrong_password_start: None,
|
||||||
key_highlight_start: None,
|
key_highlight_start: None,
|
||||||
|
key_highlight_angle: 0.0,
|
||||||
background: None,
|
background: None,
|
||||||
password_display: String::new(),
|
password_display: String::new(),
|
||||||
|
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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resize the renderer to new dimensions
|
/// Resize the renderer to new dimensions
|
||||||
pub fn resize(&mut self, width: i32, height: i32) {
|
pub fn resize(&mut self, width: i32, height: i32) {
|
||||||
|
log::debug!("Renderer::resize({}, {}) called", width, height);
|
||||||
self.width = width;
|
self.width = width;
|
||||||
self.height = height;
|
self.height = height;
|
||||||
|
|
||||||
@@ -83,6 +172,15 @@ impl Renderer {
|
|||||||
pub fn show_key_highlight(&mut self) {
|
pub fn show_key_highlight(&mut self) {
|
||||||
self.key_highlight_shown = true;
|
self.key_highlight_shown = true;
|
||||||
self.key_highlight_start = Some(Instant::now());
|
self.key_highlight_start = Some(Instant::now());
|
||||||
|
|
||||||
|
// Generate ONE random angle for this highlight
|
||||||
|
use std::time::SystemTime;
|
||||||
|
let seed = SystemTime::now()
|
||||||
|
.duration_since(SystemTime::UNIX_EPOCH)
|
||||||
|
.unwrap()
|
||||||
|
.as_nanos() as u64;
|
||||||
|
let random_val = seed.wrapping_mul(6364136223846793005).wrapping_add(1);
|
||||||
|
self.key_highlight_angle = ((random_val % 360) as f64).to_radians();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the password display string (masked)
|
/// Set the password display string (masked)
|
||||||
@@ -92,61 +190,62 @@ impl Renderer {
|
|||||||
|
|
||||||
/// Render the current frame
|
/// Render the current frame
|
||||||
pub fn render(&mut self) {
|
pub fn render(&mut self) {
|
||||||
log::info!(
|
// Clear the surface
|
||||||
"Renderer::render() called, background: {}",
|
self.context.new_path();
|
||||||
self.background.is_some()
|
self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0);
|
||||||
);
|
|
||||||
|
|
||||||
// Clear the surface - draw a VISIBLE color (dark gray) instead of black
|
|
||||||
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
|
|
||||||
self.context.paint().expect("Failed to clear surface");
|
self.context.paint().expect("Failed to clear surface");
|
||||||
|
|
||||||
// Draw background if available
|
// Draw background
|
||||||
if let Some(ref background) = self.background {
|
if let Some(ref background) = self.background {
|
||||||
let size = (background.width(), background.height());
|
self.context.new_path();
|
||||||
log::info!(
|
|
||||||
"✓ Drawing background ({}x{}, fade_alpha: {})",
|
|
||||||
size.0,
|
|
||||||
size.1,
|
|
||||||
self.fade_alpha
|
|
||||||
);
|
|
||||||
self.context
|
self.context
|
||||||
.set_source_surface(background, 0.0, 0.0)
|
.set_source_surface(background, 0.0, 0.0)
|
||||||
.expect("Failed to set background source");
|
.expect("Failed to set source");
|
||||||
self.context
|
self.context
|
||||||
.paint_with_alpha(self.fade_alpha)
|
.paint_with_alpha(self.fade_alpha)
|
||||||
.expect("Failed to draw background");
|
.expect("Failed to paint");
|
||||||
log::info!("✓ Background drawn successfully");
|
|
||||||
} else {
|
|
||||||
// Draw solid color background (dark gray visible color)
|
|
||||||
log::warn!("✗ No background available - rendering solid gray!");
|
|
||||||
self.context.set_source_rgba(0.15, 0.15, 0.15, 1.0);
|
|
||||||
self.context
|
|
||||||
.paint()
|
|
||||||
.expect("Failed to draw solid background");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw clock if enabled
|
|
||||||
if self.config.clock {
|
|
||||||
self.draw_clock();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw indicator if enabled
|
|
||||||
if self.config.indicator {
|
if self.config.indicator {
|
||||||
self.draw_indicator();
|
self.draw_indicator();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw password display (if not empty)
|
if self.config.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.config.show_keyboard_layout {
|
||||||
|
self.draw_keyboard_layout();
|
||||||
|
}
|
||||||
|
|
||||||
if !self.password_display.is_empty() {
|
if !self.password_display.is_empty() {
|
||||||
self.draw_password_display();
|
self.draw_password_display();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw wrong password feedback if active
|
if self.caps_lock && self.config.show_caps_lock_text {
|
||||||
|
self.draw_caps_lock_indicator();
|
||||||
|
}
|
||||||
|
|
||||||
if self.wrong_password_shown {
|
if self.wrong_password_shown {
|
||||||
self.draw_wrong_password_feedback();
|
self.draw_wrong_password_feedback();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Draw key highlight feedback if active
|
|
||||||
if self.key_highlight_shown {
|
if self.key_highlight_shown {
|
||||||
self.draw_key_highlight_feedback();
|
self.draw_key_highlight_feedback();
|
||||||
}
|
}
|
||||||
@@ -154,96 +253,79 @@ impl Renderer {
|
|||||||
self.update_feedback_timers();
|
self.update_feedback_timers();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the rendered image surface
|
|
||||||
pub fn as_image_surface(&self) -> &ImageSurface {
|
|
||||||
&self.surface
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Get raw pixel data from the surface (ARGB32 format)
|
/// Get raw pixel data from the surface (ARGB32 format)
|
||||||
pub fn get_pixel_data(&self) -> Result<Vec<u8>, cairo::BorrowError> {
|
pub fn get_pixel_data(&self) -> Result<Vec<u8>, cairo::BorrowError> {
|
||||||
let stride = self.surface.stride() as usize;
|
let stride = self.surface.stride() as usize;
|
||||||
let height = self.height as usize;
|
let height = self.height as usize;
|
||||||
|
|
||||||
let mut data = vec![0u8; stride * height];
|
let mut data = vec![0u8; stride * height];
|
||||||
self.surface.with_data(|src| {
|
self.surface.with_data(|src| {
|
||||||
data.copy_from_slice(src);
|
data.copy_from_slice(src);
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(data)
|
Ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get surface dimensions and stride
|
|
||||||
pub fn surface_info(&self) -> (i32, i32, i32) {
|
pub fn surface_info(&self) -> (i32, i32, i32) {
|
||||||
(self.width, self.height, self.surface.stride())
|
(self.width, self.height, self.surface.stride())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the clock in the center of the screen
|
fn update_uptime(&mut self) {
|
||||||
|
let now = Instant::now();
|
||||||
|
if let Some(last) = self.last_uptime_update {
|
||||||
|
if now.duration_since(last).as_secs() < 10 {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let uptime_secs = std::fs::read_to_string("/proc/uptime")
|
||||||
|
.ok()
|
||||||
|
.and_then(|s| s.split_whitespace().next()?.parse::<f64>().ok())
|
||||||
|
.unwrap_or(0.0) as u64;
|
||||||
|
self.uptime_cache = format!("up {}h {}m", uptime_secs / 3600, (uptime_secs % 3600) / 60);
|
||||||
|
self.last_uptime_update = Some(now);
|
||||||
|
}
|
||||||
|
|
||||||
fn draw_clock(&self) {
|
fn draw_clock(&self) {
|
||||||
use chrono::Local;
|
use chrono::Local;
|
||||||
|
|
||||||
let now = Local::now();
|
let now = Local::now();
|
||||||
let time_str = now.format("%H:%M").to_string();
|
let time_str = now.format("%H:%M").to_string();
|
||||||
let date_str = now.format("%A, %B %d").to_string();
|
let date_str = now.format("%A, %B %d").to_string();
|
||||||
|
let center_x = self.width as f64 / 2.0;
|
||||||
|
let center_y = self.height as f64 / 2.0;
|
||||||
|
|
||||||
self.context.set_font_size(72.0);
|
self.context.new_path();
|
||||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
self.context.set_font_size(48.0);
|
||||||
// Center the text
|
let te = self.context.text_extents(&time_str).unwrap();
|
||||||
let extents = self
|
|
||||||
.context
|
|
||||||
.text_extents(&time_str)
|
|
||||||
.expect("Failed to get text extents");
|
|
||||||
let x = (self.width as f64 - extents.width()) / 2.0;
|
|
||||||
let y = (self.height as f64 / 2.0) - extents.height() / 2.0;
|
|
||||||
|
|
||||||
self.context.move_to(x, y);
|
|
||||||
self.context
|
self.context
|
||||||
.show_text(&time_str)
|
.move_to(center_x - te.width() / 2.0, center_y + te.height() / 4.0);
|
||||||
.expect("Failed to draw time");
|
self.context.show_text(&time_str).unwrap();
|
||||||
|
|
||||||
// Draw date below time
|
self.context.new_path();
|
||||||
self.context.set_font_size(24.0);
|
self.context.set_font_size(14.0);
|
||||||
let date_extents = self
|
let de = self.context.text_extents(&date_str).unwrap();
|
||||||
.context
|
self.context.move_to(
|
||||||
.text_extents(&date_str)
|
center_x - de.width() / 2.0,
|
||||||
.expect("Failed to get date extents");
|
center_y + te.height() / 4.0 + 25.0,
|
||||||
let date_x = (self.width as f64 - date_extents.width()) / 2.0;
|
);
|
||||||
let date_y = y + extents.height() + 20.0;
|
self.context.show_text(&date_str).unwrap();
|
||||||
|
|
||||||
self.context.move_to(date_x, date_y);
|
self.context.new_path();
|
||||||
self.context
|
let ue = self.context.text_extents(&self.uptime_cache).unwrap();
|
||||||
.show_text(&date_str)
|
self.context.move_to(
|
||||||
.expect("Failed to draw date");
|
center_x - ue.width() / 2.0,
|
||||||
|
center_y + te.height() / 4.0 + 43.0,
|
||||||
|
);
|
||||||
|
self.context.show_text(&self.uptime_cache).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the password indicator ring
|
|
||||||
fn draw_indicator(&self) {
|
fn draw_indicator(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
let thickness = self.config.indicator_thickness as f64;
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
|
|
||||||
// Draw outer ring
|
self.context.new_path();
|
||||||
let ring_color = self.tuple_to_color(self.config.ring_color);
|
let (r, g, b, a) = self.config.inside_color;
|
||||||
self.context.set_source_rgba(
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
ring_color.r as f64 / 255.0,
|
|
||||||
ring_color.g as f64 / 255.0,
|
|
||||||
ring_color.b as f64 / 255.0,
|
|
||||||
ring_color.a as f64 / 255.0 * self.fade_alpha,
|
|
||||||
);
|
|
||||||
self.context.set_line_width(thickness);
|
|
||||||
self.context
|
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
|
||||||
self.context.stroke().expect("Failed to draw ring");
|
|
||||||
|
|
||||||
// Draw inside fill
|
|
||||||
let inside_color = self.tuple_to_color(self.config.inside_color);
|
|
||||||
self.context.set_source_rgba(
|
|
||||||
inside_color.r as f64 / 255.0,
|
|
||||||
inside_color.g as f64 / 255.0,
|
|
||||||
inside_color.b as f64 / 255.0,
|
|
||||||
inside_color.a as f64 / 255.0 * self.fade_alpha,
|
|
||||||
);
|
|
||||||
self.context.arc(
|
self.context.arc(
|
||||||
center_x,
|
center_x,
|
||||||
center_y,
|
center_y,
|
||||||
@@ -251,62 +333,84 @@ impl Renderer {
|
|||||||
0.0,
|
0.0,
|
||||||
2.0 * std::f64::consts::PI,
|
2.0 * std::f64::consts::PI,
|
||||||
);
|
);
|
||||||
self.context.fill().expect("Failed to fill inside");
|
self.context.fill().unwrap();
|
||||||
|
|
||||||
// Draw separator line
|
let (lr, lg, lb, la) = self.config.line_color;
|
||||||
let separator_color = self.tuple_to_color(self.config.separator_color);
|
if la > 0.0 {
|
||||||
if separator_color.a > 0 {
|
self.context.new_path();
|
||||||
self.context.set_source_rgba(
|
self.context
|
||||||
separator_color.r as f64 / 255.0,
|
.set_source_rgba(lr, lg, lb, la * self.fade_alpha);
|
||||||
separator_color.g as f64 / 255.0,
|
self.context.set_line_width(1.0);
|
||||||
separator_color.b as f64 / 255.0,
|
self.context.arc(
|
||||||
separator_color.a as f64 / 255.0 * self.fade_alpha,
|
center_x,
|
||||||
|
center_y,
|
||||||
|
radius - thickness / 2.0,
|
||||||
|
0.0,
|
||||||
|
2.0 * std::f64::consts::PI,
|
||||||
);
|
);
|
||||||
|
self.context.stroke().unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Use caps lock color when caps lock is on and indicator is enabled, otherwise ring color
|
||||||
|
let (r, g, b, a) = if self.caps_lock {
|
||||||
|
self.config.caps_lock_color
|
||||||
|
} else {
|
||||||
|
self.config.ring_color
|
||||||
|
};
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
|
self.context.set_line_width(thickness);
|
||||||
|
self.context
|
||||||
|
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||||
|
self.context.stroke().unwrap();
|
||||||
|
|
||||||
|
let (r, g, b, a) = self.config.separator_color;
|
||||||
|
if a > 0.0 {
|
||||||
|
self.context.new_path();
|
||||||
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
self.context.set_line_width(1.0);
|
self.context.set_line_width(1.0);
|
||||||
self.context.move_to(center_x - radius, center_y);
|
self.context.move_to(center_x - radius, center_y);
|
||||||
self.context.line_to(center_x + radius, center_y);
|
self.context.line_to(center_x + radius, center_y);
|
||||||
self.context.stroke().expect("Failed to draw separator");
|
self.context.stroke().unwrap();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw the password display (masked characters)
|
|
||||||
fn draw_password_display(&self) {
|
fn draw_password_display(&self) {
|
||||||
if self.password_display.is_empty() {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Position: below the indicator ring (or centered if no indicator)
|
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
let thickness = self.config.indicator_thickness as f64;
|
self.context.new_path();
|
||||||
|
self.context.set_font_size(32.0);
|
||||||
// Place password text below the ring
|
|
||||||
let text_y = center_y + radius + thickness + 40.0; // 40px below ring
|
|
||||||
|
|
||||||
self.context.set_font_size(36.0);
|
|
||||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||||
|
let te = self.context.text_extents(&self.password_display).unwrap();
|
||||||
// Center the text
|
|
||||||
let extents = self
|
|
||||||
.context
|
|
||||||
.text_extents(&self.password_display)
|
|
||||||
.expect("Failed to get password text extents");
|
|
||||||
let text_x = center_x - extents.width() / 2.0;
|
|
||||||
|
|
||||||
self.context.move_to(text_x, text_y);
|
|
||||||
self.context
|
self.context
|
||||||
.show_text(&self.password_display)
|
.move_to(center_x - te.width() / 2.0, center_y + radius / 1.1);
|
||||||
.expect("Failed to draw password");
|
self.context.show_text(&self.password_display).unwrap();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn draw_caps_lock_indicator(&self) {
|
||||||
|
let center_x = self.width as f64 / 2.0;
|
||||||
|
let center_y = self.height as f64 / 2.0;
|
||||||
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
self.context.new_path();
|
||||||
|
// Use configurable caps lock text color
|
||||||
|
let (r, g, b, a) = self.config.caps_lock_text_color;
|
||||||
|
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||||
|
// Increase font size for bigger letters
|
||||||
|
self.context.set_font_size(24.0);
|
||||||
|
let text = "Caps Lock";
|
||||||
|
let te = self.context.text_extents(text).unwrap();
|
||||||
|
// Position above the ring
|
||||||
|
self.context
|
||||||
|
.move_to(center_x - te.width() / 2.0, center_y - radius - 10.0);
|
||||||
|
self.context.show_text(text).unwrap();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw wrong password feedback (red flash)
|
|
||||||
fn draw_wrong_password_feedback(&self) {
|
fn draw_wrong_password_feedback(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
// Calculate flash intensity based on time
|
|
||||||
let intensity = if let Some(start) = self.wrong_password_start {
|
let intensity = if let Some(start) = self.wrong_password_start {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let duration = std::time::Duration::from_millis(500);
|
let duration = std::time::Duration::from_millis(500);
|
||||||
@@ -320,26 +424,24 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if intensity > 0.0 {
|
if intensity > 0.0 {
|
||||||
|
self.context.new_path();
|
||||||
self.context
|
self.context
|
||||||
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
||||||
|
self.context.set_line_width(thickness + 2.0);
|
||||||
self.context
|
self.context
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||||
self.context
|
self.context.stroke().unwrap();
|
||||||
.fill()
|
|
||||||
.expect("Failed to draw wrong password feedback");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Draw key highlight feedback (green flash)
|
|
||||||
fn draw_key_highlight_feedback(&self) {
|
fn draw_key_highlight_feedback(&self) {
|
||||||
let center_x = self.width as f64 / 2.0;
|
let center_x = self.width as f64 / 2.0;
|
||||||
let center_y = self.height as f64 / 2.0;
|
let center_y = self.height as f64 / 2.0;
|
||||||
let radius = self.config.indicator_radius as f64;
|
let radius = self.config.indicator_radius as f64;
|
||||||
|
let thickness = self.config.indicator_thickness as f64;
|
||||||
// Calculate flash intensity based on time
|
|
||||||
let intensity = if let Some(start) = self.key_highlight_start {
|
let intensity = if let Some(start) = self.key_highlight_start {
|
||||||
let elapsed = start.elapsed();
|
let elapsed = start.elapsed();
|
||||||
let duration = std::time::Duration::from_millis(200);
|
let duration = std::time::Duration::from_millis(300);
|
||||||
if elapsed < duration {
|
if elapsed < duration {
|
||||||
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
||||||
} else {
|
} else {
|
||||||
@@ -350,37 +452,370 @@ impl Renderer {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if intensity > 0.0 {
|
if intensity > 0.0 {
|
||||||
let key_hl_color = self.tuple_to_color(self.config.key_hl_color);
|
let (r, g, b, a) = if self.caps_lock {
|
||||||
self.context.set_source_rgba(
|
self.config.caps_lock_key_hl_color
|
||||||
key_hl_color.r as f64 / 255.0,
|
} else {
|
||||||
key_hl_color.g as f64 / 255.0,
|
self.config.key_hl_color
|
||||||
key_hl_color.b as f64 / 255.0,
|
};
|
||||||
key_hl_color.a as f64 / 255.0 * intensity * self.fade_alpha,
|
self.context
|
||||||
|
.set_source_rgba(r, g, b, a * intensity * self.fade_alpha);
|
||||||
|
self.context.set_line_width(thickness + 1.5);
|
||||||
|
|
||||||
|
// Draw ONLY ONE segment that rotates based on password length
|
||||||
|
let global_offset = (self.password_display.len() as f64 * 45.0).to_radians();
|
||||||
|
self.context.new_path();
|
||||||
|
let actual_start = global_offset + self.key_highlight_angle;
|
||||||
|
self.context.arc(
|
||||||
|
center_x,
|
||||||
|
center_y,
|
||||||
|
radius,
|
||||||
|
actual_start,
|
||||||
|
actual_start + (40.0_f64).to_radians(),
|
||||||
);
|
);
|
||||||
self.context
|
self.context.stroke().unwrap();
|
||||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
|
||||||
self.context
|
|
||||||
.fill()
|
|
||||||
.expect("Failed to draw key highlight feedback");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Update feedback timers and reset expired feedback
|
|
||||||
fn update_feedback_timers(&mut self) {
|
fn update_feedback_timers(&mut self) {
|
||||||
// Check wrong password feedback timeout
|
self.update_uptime();
|
||||||
if let Some(start) = self.wrong_password_start {
|
if let Some(start) = self.wrong_password_start {
|
||||||
if start.elapsed() > std::time::Duration::from_millis(500) {
|
if start.elapsed() > std::time::Duration::from_millis(500) {
|
||||||
self.wrong_password_shown = false;
|
self.wrong_password_shown = false;
|
||||||
self.wrong_password_start = None;
|
self.wrong_password_start = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check key highlight feedback timeout
|
|
||||||
if let Some(start) = self.key_highlight_start {
|
if let Some(start) = self.key_highlight_start {
|
||||||
if start.elapsed() > std::time::Duration::from_millis(200) {
|
if start.elapsed() > std::time::Duration::from_millis(300) {
|
||||||
self.key_highlight_shown = false;
|
self.key_highlight_shown = false;
|
||||||
self.key_highlight_start = None;
|
self.key_highlight_start = None;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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 && 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 {
|
||||||
|
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(ssid).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_keyboard_layout(&self) {
|
||||||
|
if self.config.show_keyboard_layout {
|
||||||
|
if let Some(ref layout) = self.system_status.keyboard_layout {
|
||||||
|
let margin = 20.0;
|
||||||
|
let x = margin;
|
||||||
|
let y = margin + 80.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);
|
||||||
|
let text = format!("Layout: {}", layout);
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+158
-14
@@ -28,11 +28,6 @@ impl Screenshot {
|
|||||||
Self { surface }
|
Self { surface }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a reference to the underlying surface.
|
|
||||||
pub fn surface(&self) -> &ImageSurface {
|
|
||||||
&self.surface
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Consume the screenshot and return the underlying Cairo surface.
|
/// Consume the screenshot and return the underlying Cairo surface.
|
||||||
pub fn into_inner(self) -> ImageSurface {
|
pub fn into_inner(self) -> ImageSurface {
|
||||||
self.surface
|
self.surface
|
||||||
@@ -46,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 {
|
||||||
@@ -171,11 +318,6 @@ impl ScreenshotManager {
|
|||||||
Ok(Self { manager })
|
Ok(Self { manager })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns `true` if the wlr-screencopy protocol is available.
|
|
||||||
pub fn is_available(&self) -> bool {
|
|
||||||
self.manager.is_some()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Initiate a screencopy operation for the given output.
|
/// Initiate a screencopy operation for the given output.
|
||||||
///
|
///
|
||||||
/// This method sends a screencopy request and returns the frame object.
|
/// This method sends a screencopy request and returns the frame object.
|
||||||
@@ -252,24 +394,24 @@ impl ScreenshotManager {
|
|||||||
flipped[dst_offset..dst_offset + src_stride]
|
flipped[dst_offset..dst_offset + src_stride]
|
||||||
.copy_from_slice(&converted_data[src_offset..src_offset + src_stride]);
|
.copy_from_slice(&converted_data[src_offset..src_offset + src_stride]);
|
||||||
}
|
}
|
||||||
return Ok(ImageSurface::create_for_data(
|
return ImageSurface::create_for_data(
|
||||||
flipped,
|
flipped,
|
||||||
cairo::Format::ARgb32,
|
cairo::Format::ARgb32,
|
||||||
info.width as i32,
|
info.width as i32,
|
||||||
info.height as i32,
|
info.height as i32,
|
||||||
src_stride as i32,
|
src_stride as i32,
|
||||||
)
|
)
|
||||||
.context("Failed to create flipped Cairo surface")?);
|
.context("Failed to create flipped Cairo surface");
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(ImageSurface::create_for_data(
|
ImageSurface::create_for_data(
|
||||||
converted_data,
|
converted_data,
|
||||||
cairo::Format::ARgb32,
|
cairo::Format::ARgb32,
|
||||||
info.width as i32,
|
info.width as i32,
|
||||||
info.height as i32,
|
info.height as i32,
|
||||||
pixel_width as i32,
|
pixel_width as i32,
|
||||||
)
|
)
|
||||||
.context("Failed to create Cairo surface")?)
|
.context("Failed to create Cairo surface")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -314,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 {
|
||||||
@@ -324,6 +467,7 @@ impl CaptureData {
|
|||||||
info: Mutex::new(None),
|
info: Mutex::new(None),
|
||||||
flags: Mutex::new(None),
|
flags: Mutex::new(None),
|
||||||
buffer: Mutex::new(None),
|
buffer: Mutex::new(None),
|
||||||
|
pool: Mutex::new(None),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+277
@@ -0,0 +1,277 @@
|
|||||||
|
use log::{debug, error};
|
||||||
|
use mpris::PlayerFinder;
|
||||||
|
use std::sync::{Arc, Mutex};
|
||||||
|
use tokio::sync::mpsc;
|
||||||
|
use zbus::Connection;
|
||||||
|
|
||||||
|
#[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<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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 result.is_err() {
|
||||||
|
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,26 +0,0 @@
|
|||||||
use std::time::Duration;
|
|
||||||
|
|
||||||
pub struct FadeTimer {
|
|
||||||
duration: Duration,
|
|
||||||
start_time: std::time::Instant,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl FadeTimer {
|
|
||||||
pub fn new(duration: Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
duration,
|
|
||||||
start_time: std::time::Instant::now(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn update(&mut self) -> bool {
|
|
||||||
let elapsed = self.start_time.elapsed();
|
|
||||||
let progress = elapsed.as_secs_f64() / self.duration.as_secs_f64();
|
|
||||||
progress >= 1.0
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn current_alpha(&self) -> f64 {
|
|
||||||
let elapsed = self.start_time.elapsed();
|
|
||||||
(elapsed.as_secs_f64() / self.duration.as_secs_f64()).min(1.0)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
-20
@@ -38,23 +38,3 @@ pub fn parse_vignette_effect(s: &str) -> Result<(f32, f32), String> {
|
|||||||
let factor = parts[1].parse().map_err(|_| "Invalid factor")?;
|
let factor = parts[1].parse().map_err(|_| "Invalid factor")?;
|
||||||
Ok((base, factor))
|
Ok((base, factor))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert hex color string to RGBA color struct
|
|
||||||
pub fn hex_to_rgba(hex: &str) -> Color {
|
|
||||||
let (r, g, b, a) = parse_hex_color(hex).unwrap_or((0.0, 0.0, 0.0, 1.0));
|
|
||||||
Color {
|
|
||||||
r: (r * 255.0) as u8,
|
|
||||||
g: (g * 255.0) as u8,
|
|
||||||
b: (b * 255.0) as u8,
|
|
||||||
a: (a * 255.0) as u8,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// RGBA color struct
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub struct Color {
|
|
||||||
pub r: u8,
|
|
||||||
pub g: u8,
|
|
||||||
pub b: u8,
|
|
||||||
pub a: u8,
|
|
||||||
}
|
|
||||||
|
|||||||
Reference in New Issue
Block a user