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]
|
||||
name = "wayrustlock"
|
||||
name = "rustlock"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
smithay-client-toolkit = { version = "0.19", features = ["calloop"] }
|
||||
wayland-client = "0.31"
|
||||
wayland-protocols = { version = "0.32", features = ["client", "unstable"] }
|
||||
smithay-client-toolkit = { version = "0.19", default-features = false, features = ["calloop", "xkbcommon"] }
|
||||
wayland-client = { version = "0.31" }
|
||||
wayland-protocols = { version = "0.32", features = ["client"] }
|
||||
wayland-protocols-wlr = { version = "0.3", features = ["client"] }
|
||||
anyhow = "1.0"
|
||||
futures = "0.3"
|
||||
cairo-rs = { version = "0.20", features = ["png"] }
|
||||
image = "0.25"
|
||||
fastblur = "0.1"
|
||||
xkbcommon = "0.7"
|
||||
pam-client = "0.5"
|
||||
secstr = "0.5"
|
||||
clap = { version = "4.5", features = ["derive"] }
|
||||
toml = "1.0"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
futures = { version = "0.3", default-features = false, features = ["std"] }
|
||||
cairo-rs = { version = "0.20", default-features = false, features = ["png"] }
|
||||
gdk-pixbuf = { version = "0.20", default-features = false, features = ["v2_40"] }
|
||||
gio = { version = "0.20" }
|
||||
pangocairo = { version = "0.20" }
|
||||
clap = { version = "4.4", default-features = false, features = ["derive", "std", "help", "usage", "error-context"] }
|
||||
toml = { version = "1.0", default-features = false, features = ["parse", "display", "serde"] }
|
||||
serde = { version = "1.0", default-features = false, features = ["derive", "std"] }
|
||||
zeroize = "1.7"
|
||||
log = "0.4"
|
||||
env_logger = "0.11"
|
||||
chrono = "0.4"
|
||||
users = "0.11"
|
||||
env_logger = { version = "0.11", default-features = false, features = ["color", "humantime"] }
|
||||
chrono = { version = "0.4", default-features = false, features = ["clock", "std"] }
|
||||
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:**
|
||||
- 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
|
||||
## ✨ Features
|
||||
|
||||
**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
|
||||
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
|
||||
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
|
||||
- Session locking via ext-session-lock-v1 protocol (tested on sway)
|
||||
- Buffer creation from Cairo surfaces (wl_shm)
|
||||
- CLI argument parsing with all swaylock-effects options
|
||||
- PAM authentication infrastructure (using pam-client crate)
|
||||
- Keyboard handler with proper KeyEvent processing
|
||||
- Module architecture (auth, input, lock, render, screenshot, timer, util)
|
||||
When locked, use function keys to control the system:
|
||||
- **F1**: Suspend to RAM
|
||||
- **F2**: Reboot
|
||||
- **F3**: Power Off
|
||||
|
||||
### 🔄 In Progress
|
||||
- Screenshot capture (wlr-screencopy protocol not yet integrated)
|
||||
- Full PAM integration with auth loop
|
||||
---
|
||||
|
||||
### ❌ Not Yet Implemented
|
||||
- Real screenshot capture (currently shows solid color background)
|
||||
- Grace period and fade-in animations
|
||||
## ⚙️ Configuration
|
||||
|
||||
## 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
|
||||
cargo build --release
|
||||
```
|
||||
|
||||
## Usage
|
||||
The binary will be available at `target/release/rustlock`.
|
||||
|
||||
Basic usage:
|
||||
```bash
|
||||
wayrustlock
|
||||
```
|
||||
### Build Options
|
||||
|
||||
With all options from swaylock-effects compatibility:
|
||||
```bash
|
||||
wayrustlock \
|
||||
--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
|
||||
```
|
||||
- **With networking** (default): Includes reqwest for album art fetching
|
||||
```bash
|
||||
cargo build --release --features networking
|
||||
```
|
||||
|
||||
## 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.)
|
||||
- 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
|
||||
AGPL v3+
|
||||
|
||||
+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
|
||||
# Install this file to /etc/pam.d/wayrustlock
|
||||
# PAM configuration for rustlock
|
||||
# Install this file to /etc/pam.d/rustlock
|
||||
|
||||
# Use the standard login service authentication
|
||||
auth include login
|
||||
|
||||
+41
-39
@@ -4,10 +4,10 @@ use std::thread;
|
||||
use log::{debug, error};
|
||||
use pam_client::{Context, ErrorCode, Flag};
|
||||
use smithay_client_toolkit::reexports::{calloop::channel, calloop::EventLoop};
|
||||
use users::get_current_username;
|
||||
use whoami::username;
|
||||
use zeroize::Zeroizing;
|
||||
|
||||
const SERVICE_NAME: &str = "wayrustlock";
|
||||
const SERVICE_NAME: &str = "rustlock";
|
||||
|
||||
pub struct LockConversation {
|
||||
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>) {
|
||||
struct AuthLoopState {
|
||||
auth_res_send: channel::Sender<bool>,
|
||||
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();
|
||||
pub fn create_and_run_auth_loop(
|
||||
) -> Option<(channel::Sender<Zeroizing<String>>, channel::Channel<bool>)> {
|
||||
let username = username();
|
||||
|
||||
let conversation = LockConversation { password: None };
|
||||
let context = Context::new(SERVICE_NAME, Some(username.as_str()), conversation)
|
||||
.expect("Failed to initialize PAM context");
|
||||
debug!("Prepared to authenticate user '{}'", username);
|
||||
match Context::new(SERVICE_NAME, Some(username.as_str()), conversation) {
|
||||
Ok(_) => {
|
||||
debug!("Prepared to authenticate user '{}'", username);
|
||||
}
|
||||
Err(err) => {
|
||||
error!("Failed to initialize PAM context: {:?}", err);
|
||||
error!(
|
||||
"Ensure that the PAM service '{}' is correctly configured.",
|
||||
SERVICE_NAME
|
||||
);
|
||||
return None;
|
||||
}
|
||||
}
|
||||
|
||||
let (auth_req_send, auth_req_recv) = channel::channel::<Zeroizing<String>>();
|
||||
let (auth_res_send, auth_res_recv) = channel::channel::<bool>();
|
||||
|
||||
thread::spawn(move || {
|
||||
let mut event_loop: EventLoop<AuthLoopState> = EventLoop::try_new().unwrap();
|
||||
let mut event_loop: EventLoop<()> = EventLoop::try_new().unwrap();
|
||||
event_loop
|
||||
.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) => {
|
||||
state.context.conversation_mut().password = Some(password);
|
||||
let status = match state.context.authenticate(Flag::NONE) {
|
||||
Ok(()) => true,
|
||||
Err(err) => {
|
||||
error!("Pam authenticate failed with {:?}", err);
|
||||
false
|
||||
}
|
||||
let conversation = LockConversation {
|
||||
password: Some(password),
|
||||
};
|
||||
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();
|
||||
|
||||
let mut state = AuthLoopState {
|
||||
auth_res_send,
|
||||
main_closed: false,
|
||||
context,
|
||||
};
|
||||
|
||||
while !state.main_closed {
|
||||
event_loop
|
||||
.dispatch(None, &mut state)
|
||||
.expect("Failed to run");
|
||||
loop {
|
||||
event_loop.dispatch(None, &mut ()).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)]
|
||||
#[command(author, version, about, long_about = None)]
|
||||
pub struct Config {
|
||||
#[arg(long)]
|
||||
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||
pub screenshots: bool,
|
||||
|
||||
#[arg(long)]
|
||||
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "false", default_missing_value = "true")]
|
||||
pub clock: bool,
|
||||
|
||||
#[arg(long, default_value = "true")]
|
||||
#[arg(long, action = clap::ArgAction::Set, num_args = 0..=1, default_value = "true", default_missing_value = "true")]
|
||||
pub indicator: bool,
|
||||
|
||||
#[arg(long, default_value = "100")]
|
||||
@@ -27,12 +27,36 @@ pub struct Config {
|
||||
#[arg(long, value_parser = util::parse_vignette_effect)]
|
||||
pub effect_vignette: Option<(f32, f32)>,
|
||||
|
||||
#[arg(long)]
|
||||
pub effect_pixelate: Option<u32>,
|
||||
|
||||
#[arg(long)]
|
||||
pub effect_swirl: Option<f32>,
|
||||
|
||||
#[arg(long)]
|
||||
pub effect_melting: Option<f32>,
|
||||
|
||||
#[arg(long, default_value = "785412", value_parser = util::parse_hex_color)]
|
||||
pub ring_color: (f64, f64, f64, f64),
|
||||
|
||||
#[arg(long, default_value = "4EAC41", value_parser = util::parse_hex_color)]
|
||||
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)]
|
||||
pub line_color: (f64, f64, f64, f64),
|
||||
|
||||
@@ -57,27 +81,223 @@ pub struct Config {
|
||||
#[arg(long)]
|
||||
pub debug: bool,
|
||||
|
||||
/// Show screen temporarily when a key is pressed (like swaylock-effects peek)
|
||||
/// Write verbose logs to ~/.rustlock.log
|
||||
#[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 {
|
||||
pub fn load() -> Self {
|
||||
let mut config = Config::parse();
|
||||
use clap::CommandFactory;
|
||||
|
||||
if let Some(config_path) = &config.config {
|
||||
if let Ok(file_content) = std::fs::read_to_string(config_path) {
|
||||
if let Ok(file_config) = toml::from_str::<Config>(&file_content) {
|
||||
config = file_config;
|
||||
} else {
|
||||
eprintln!(
|
||||
"Warning: Failed to parse config file {}",
|
||||
config_path.display()
|
||||
);
|
||||
let mut config = Config::parse();
|
||||
let cmd = Config::command();
|
||||
let matches = cmd.get_matches();
|
||||
|
||||
// Helper to check if a value was explicitly set on command line
|
||||
let is_cli =
|
||||
|key: &str| matches.value_source(key) == Some(clap::parser::ValueSource::CommandLine);
|
||||
|
||||
// 1. Config file layer (overrides defaults and themes)
|
||||
let config_path = config.config.clone().unwrap_or_else(|| {
|
||||
let mut path = std::path::PathBuf::from(std::env::var("HOME").unwrap_or_default());
|
||||
path.push(".config/rustlock/config.toml");
|
||||
path
|
||||
});
|
||||
|
||||
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 {
|
||||
password_buffer: Zeroizing<String>,
|
||||
cursor_position: usize,
|
||||
config: crate::config::Config,
|
||||
wrong_password_timer: Option<std::time::Instant>,
|
||||
key_highlight_timer: Option<std::time::Instant>,
|
||||
temp_screenshot_timer: Option<std::time::Instant>,
|
||||
temp_screenshot_active: bool,
|
||||
caps_lock: bool,
|
||||
}
|
||||
|
||||
impl InputHandler {
|
||||
pub fn new(config: crate::config::Config) -> Self {
|
||||
pub fn new(_config: crate::config::Config) -> Self {
|
||||
Self {
|
||||
password_buffer: Zeroizing::new(String::new()),
|
||||
cursor_position: 0,
|
||||
config,
|
||||
wrong_password_timer: None,
|
||||
key_highlight_timer: None,
|
||||
temp_screenshot_timer: None,
|
||||
temp_screenshot_active: false,
|
||||
caps_lock: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,114 +24,46 @@ impl InputHandler {
|
||||
pub fn handle_key_event(
|
||||
&mut self,
|
||||
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||
state: wayland_client::protocol::wl_keyboard::KeyState,
|
||||
utf8: Option<String>,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> InputAction {
|
||||
// Only process key press events
|
||||
if state != wayland_client::protocol::wl_keyboard::KeyState::Pressed {
|
||||
return InputAction::None;
|
||||
}
|
||||
// Update Caps Lock state
|
||||
self.caps_lock = modifiers.caps_lock;
|
||||
|
||||
// Convert keysym to character
|
||||
let ch = self.keysym_to_char(keysym, modifiers);
|
||||
|
||||
match ch {
|
||||
Some('\x08') | Some('\x7f') => {
|
||||
// Backspace or Delete
|
||||
// Handle special keys first using keysym
|
||||
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||
match keysym {
|
||||
Keysym::BackSpace => {
|
||||
if !self.password_buffer.is_empty() && self.cursor_position > 0 {
|
||||
self.cursor_position -= 1;
|
||||
self.password_buffer.remove(self.cursor_position);
|
||||
}
|
||||
InputAction::PasswordChanged
|
||||
return InputAction::PasswordChanged;
|
||||
}
|
||||
Some('\r') | Some('\n') => {
|
||||
// Enter key - submit password
|
||||
Keysym::Return | Keysym::KP_Enter => {
|
||||
let password = self.password_buffer.clone();
|
||||
self.password_buffer.clear();
|
||||
self.cursor_position = 0;
|
||||
InputAction::SubmitPassword(password)
|
||||
return InputAction::SubmitPassword(password);
|
||||
}
|
||||
Some('\x1b') => {
|
||||
// Escape key - cancel
|
||||
InputAction::Cancel
|
||||
Keysym::Escape => {
|
||||
return InputAction::Cancel;
|
||||
}
|
||||
Some('p') | Some('P') if self.config.temp_screenshot => {
|
||||
// 'p' key for temp screenshot peek
|
||||
self.activate_temp_screenshot();
|
||||
InputAction::TempScreenshot
|
||||
}
|
||||
Some(c) if c.is_ascii() && !c.is_control() => {
|
||||
// Printable ASCII character
|
||||
self.password_buffer.insert(self.cursor_position, c);
|
||||
self.cursor_position += 1;
|
||||
InputAction::PasswordChanged
|
||||
}
|
||||
_ => {
|
||||
// Other keys (function keys, arrows, etc.)
|
||||
InputAction::None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a keysym to a character, considering modifiers
|
||||
fn keysym_to_char(
|
||||
&self,
|
||||
keysym: smithay_client_toolkit::seat::keyboard::Keysym,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> Option<char> {
|
||||
use smithay_client_toolkit::seat::keyboard::Keysym;
|
||||
|
||||
// Handle special keys first
|
||||
match keysym {
|
||||
Keysym::BackSpace => return Some('\x08'),
|
||||
Keysym::Delete => return Some('\x7f'),
|
||||
Keysym::Return => return Some('\r'),
|
||||
Keysym::KP_Enter => return Some('\n'),
|
||||
Keysym::Escape => return Some('\x1b'),
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Convert keysym to character
|
||||
let keysym_value = keysym.raw();
|
||||
|
||||
// Basic ASCII conversion (simplified - real implementation would use xkbcommon)
|
||||
// This is a simplified mapping for demonstration
|
||||
if keysym_value >= 0x20 && keysym_value <= 0x7e {
|
||||
let mut ch = keysym_value as u8 as char;
|
||||
|
||||
// Apply shift modifier
|
||||
if modifiers.shift {
|
||||
ch = match ch {
|
||||
'`' => '~',
|
||||
'1' => '!',
|
||||
'2' => '@',
|
||||
'3' => '#',
|
||||
'4' => '$',
|
||||
'5' => '%',
|
||||
'6' => '^',
|
||||
'7' => '&',
|
||||
'8' => '*',
|
||||
'9' => '(',
|
||||
'0' => ')',
|
||||
'-' => '_',
|
||||
'=' => '+',
|
||||
'[' => '{',
|
||||
']' => '}',
|
||||
'\\' => '|',
|
||||
';' => ':',
|
||||
'\'' => '"',
|
||||
',' => '<',
|
||||
'.' => '>',
|
||||
'/' => '?',
|
||||
c if c.is_ascii_lowercase() => c.to_ascii_uppercase(),
|
||||
_ => ch,
|
||||
};
|
||||
// Use the UTF-8 string provided by SCTK for character input
|
||||
if let Some(txt) = utf8 {
|
||||
for c in txt.chars() {
|
||||
if c.is_ascii() && !c.is_control() {
|
||||
self.password_buffer.insert(self.cursor_position, c);
|
||||
self.cursor_position += 1;
|
||||
}
|
||||
}
|
||||
|
||||
Some(ch)
|
||||
} else {
|
||||
None
|
||||
return InputAction::PasswordChanged;
|
||||
}
|
||||
|
||||
InputAction::None
|
||||
}
|
||||
|
||||
/// Get the current password (for display purposes only - returns masked version)
|
||||
@@ -143,17 +71,6 @@ impl InputHandler {
|
||||
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
|
||||
pub fn set_wrong_password_feedback(&mut self) {
|
||||
self.wrong_password_timer = Some(std::time::Instant::now());
|
||||
@@ -183,40 +100,11 @@ impl InputHandler {
|
||||
}
|
||||
|
||||
/// Update timers (should be called periodically)
|
||||
pub fn update(&mut self) {
|
||||
// Update temp screenshot state
|
||||
self.update_temp_screenshot();
|
||||
}
|
||||
pub fn update(&mut self) {}
|
||||
|
||||
/// Activate temporary screenshot display (peek feature)
|
||||
pub fn activate_temp_screenshot(&mut self) {
|
||||
self.temp_screenshot_timer = Some(std::time::Instant::now());
|
||||
self.temp_screenshot_active = true;
|
||||
}
|
||||
|
||||
/// Check if temporary screenshot should be shown
|
||||
pub fn should_show_temp_screenshot(&self) -> bool {
|
||||
if let Some(timer) = self.temp_screenshot_timer {
|
||||
let elapsed = timer.elapsed();
|
||||
// Show for 2 seconds
|
||||
if elapsed < std::time::Duration::from_secs(2) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// 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;
|
||||
}
|
||||
/// Get the current Caps Lock state
|
||||
pub fn caps_lock(&self) -> bool {
|
||||
self.caps_lock
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,5 +115,4 @@ pub enum InputAction {
|
||||
PasswordChanged,
|
||||
SubmitPassword(Zeroizing<String>),
|
||||
Cancel,
|
||||
TempScreenshot,
|
||||
}
|
||||
|
||||
+76
-266
@@ -2,29 +2,28 @@ use cairo::ImageSurface;
|
||||
use std::error::Error;
|
||||
use std::time::Instant;
|
||||
use wayland_client::protocol::{wl_output, wl_shm, wl_surface};
|
||||
use wayland_client::Proxy;
|
||||
|
||||
use crate::config::Config;
|
||||
use crate::input::{InputAction, InputHandler};
|
||||
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;
|
||||
|
||||
/// Manages a locked surface for a single output
|
||||
pub struct LockedSurface {
|
||||
width: i32,
|
||||
height: i32,
|
||||
config: Config,
|
||||
pub renderer: Renderer,
|
||||
input_handler: InputHandler,
|
||||
background: Option<ImageSurface>,
|
||||
background_applied: bool,
|
||||
fade_alpha: f64,
|
||||
wrong_password_shown: bool,
|
||||
key_highlight_shown: bool,
|
||||
temp_screenshot_shown: bool,
|
||||
last_update: Instant,
|
||||
start_time: Instant,
|
||||
wayland_surface: Option<wl_surface::WlSurface>,
|
||||
output: wl_output::WlOutput,
|
||||
configured: bool,
|
||||
}
|
||||
|
||||
impl LockedSurface {
|
||||
@@ -42,123 +41,104 @@ impl LockedSurface {
|
||||
let renderer = Renderer::new(width, height, 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 {
|
||||
width,
|
||||
height,
|
||||
config: config.clone(),
|
||||
renderer,
|
||||
input_handler,
|
||||
background,
|
||||
background: None,
|
||||
background_applied: false,
|
||||
fade_alpha: 0.0,
|
||||
wrong_password_shown: false,
|
||||
key_highlight_shown: false,
|
||||
temp_screenshot_shown: false,
|
||||
last_update: Instant::now(),
|
||||
start_time: Instant::now(),
|
||||
wayland_surface: None,
|
||||
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
|
||||
pub fn matches_surface(&self, surface: &wl_surface::WlSurface) -> bool {
|
||||
use wayland_client::Proxy;
|
||||
self.wayland_surface
|
||||
.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)
|
||||
pub fn update(&mut self) {
|
||||
log::debug!(
|
||||
"LockedSurface::update() called, background: {}",
|
||||
self.background.is_some()
|
||||
);
|
||||
|
||||
// Update timers
|
||||
self.input_handler.update();
|
||||
|
||||
if !self.configured {
|
||||
return;
|
||||
}
|
||||
|
||||
// Update fade animation
|
||||
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);
|
||||
self.fade_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
||||
self.renderer.set_fade_alpha(self.fade_alpha);
|
||||
log::debug!("Fade alpha updated: {}", self.fade_alpha);
|
||||
let new_alpha = (elapsed.as_secs_f64() / fade_duration.as_secs_f64()).min(1.0);
|
||||
if (new_alpha - self.fade_alpha).abs() > 0.001 {
|
||||
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 {
|
||||
self.renderer.show_wrong_password();
|
||||
self.wrong_password_shown = true;
|
||||
log::debug!("Showing wrong password feedback");
|
||||
} else if !self.input_handler.should_show_wrong_password() && self.wrong_password_shown {
|
||||
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 {
|
||||
self.renderer.show_key_highlight();
|
||||
self.key_highlight_shown = true;
|
||||
log::debug!("Showing key highlight");
|
||||
} else if !self.input_handler.should_show_key_highlight() && self.key_highlight_shown {
|
||||
self.key_highlight_shown = false;
|
||||
log::debug!("Hiding key highlight");
|
||||
}
|
||||
|
||||
// Handle temp screenshot (peek feature)
|
||||
if self.input_handler.should_show_temp_screenshot() && !self.temp_screenshot_shown {
|
||||
// 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");
|
||||
}
|
||||
// Update caps lock state in renderer
|
||||
self.renderer.caps_lock = self.input_handler.caps_lock();
|
||||
|
||||
// Set background if available
|
||||
if let Some(ref background) = self.background {
|
||||
let size = (background.width(), background.height());
|
||||
log::info!(
|
||||
"✓ Applying background from self.background: {}x{}",
|
||||
size.0,
|
||||
size.1
|
||||
);
|
||||
self.renderer.set_background(background.clone());
|
||||
} else {
|
||||
log::warn!("✗ No background in self.background - will render solid color!");
|
||||
// Set background if available and not already applied
|
||||
if !self.background_applied {
|
||||
if let Some(ref background) = self.background {
|
||||
log::info!("Applying background image to renderer");
|
||||
self.renderer.set_background(background.clone());
|
||||
self.background_applied = true;
|
||||
}
|
||||
}
|
||||
|
||||
self.renderer
|
||||
.set_password_display(self.input_handler.get_display_password());
|
||||
|
||||
// Render the frame
|
||||
self.renderer.render();
|
||||
|
||||
self.last_update = Instant::now();
|
||||
}
|
||||
|
||||
/// Commit the rendered frame to the Wayland surface
|
||||
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 (width, height, stride) = self.renderer.surface_info();
|
||||
|
||||
// Create buffer from pool
|
||||
let (buffer, canvas) =
|
||||
pool.create_buffer(width, height, stride, wl_shm::Format::Argb8888)?;
|
||||
|
||||
// Copy pixel data to buffer
|
||||
let copy_len = pixel_data.len().min(canvas.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 {
|
||||
buffer.attach_to(wl_surface)?;
|
||||
wl_surface.damage_buffer(0, 0, width, height);
|
||||
@@ -168,204 +148,73 @@ impl LockedSurface {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Handle resize event from Wayland
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
if width <= 0 || height <= 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
self.renderer.resize(width, height);
|
||||
|
||||
// TODO: Re-capture screenshot if screenshots are enabled
|
||||
self.background_applied = false;
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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(
|
||||
&mut self,
|
||||
event: smithay_client_toolkit::seat::keyboard::KeyEvent,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> 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
|
||||
.input_handler
|
||||
.handle_key_event(keysym, state, modifiers);
|
||||
.handle_key_event(event.keysym, event.utf8, modifiers);
|
||||
|
||||
match action {
|
||||
InputAction::SubmitPassword(password) => {
|
||||
// Show key highlight for visual feedback
|
||||
self.show_key_highlight();
|
||||
Some(InputAction::SubmitPassword(password))
|
||||
InputAction::PasswordChanged => {
|
||||
self.input_handler.set_key_highlight();
|
||||
self.key_highlight_shown = false;
|
||||
}
|
||||
InputAction::Cancel => Some(InputAction::Cancel),
|
||||
InputAction::TempScreenshot => Some(InputAction::TempScreenshot),
|
||||
InputAction::PasswordChanged => Some(InputAction::PasswordChanged),
|
||||
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>>,
|
||||
InputAction::SubmitPassword(_) => {
|
||||
self.input_handler.set_key_highlight();
|
||||
self.key_highlight_shown = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
impl pam_client::ConversationHandler for SimpleConversation {
|
||||
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
|
||||
}
|
||||
}
|
||||
Some(action)
|
||||
}
|
||||
|
||||
/// 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) {
|
||||
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 {
|
||||
&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) {
|
||||
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 surfaces: Vec<LockedSurface>,
|
||||
config: Config,
|
||||
locked: bool,
|
||||
}
|
||||
|
||||
impl LockManager {
|
||||
/// Create a new lock manager
|
||||
pub fn new(config: Config) -> Self {
|
||||
Self {
|
||||
surfaces: Vec::new(),
|
||||
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 {
|
||||
match LockedSurface::new(width, height, &self.config, output) {
|
||||
Some(surface) => {
|
||||
@@ -376,69 +225,20 @@ impl LockManager {
|
||||
}
|
||||
}
|
||||
|
||||
/// Update all locked surfaces
|
||||
pub fn update(&mut self) {
|
||||
for surface in &mut self.surfaces {
|
||||
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 {
|
||||
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> {
|
||||
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(
|
||||
&mut self,
|
||||
wayland_surface: &wl_surface::WlSurface,
|
||||
@@ -448,13 +248,23 @@ impl LockManager {
|
||||
.find(|surface| surface.matches_surface(wayland_surface))
|
||||
}
|
||||
|
||||
/// Find a locked surface by output
|
||||
pub fn find_surface_by_output(
|
||||
pub fn handle_key_event(
|
||||
&mut self,
|
||||
output: &wl_output::WlOutput,
|
||||
) -> Option<&mut LockedSurface> {
|
||||
self.surfaces
|
||||
.iter_mut()
|
||||
.find(|surface| surface.output().id() == output.id())
|
||||
event: KeyEvent,
|
||||
modifiers: smithay_client_toolkit::seat::keyboard::Modifiers,
|
||||
) -> Option<InputAction> {
|
||||
let mut action = None;
|
||||
for surface in &mut self.surfaces {
|
||||
if let Some(a) = surface.handle_key_event(event.clone(), 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 crate::config::Config;
|
||||
use crate::util::Color;
|
||||
use crate::system::SystemStatus;
|
||||
|
||||
/// Cairo-based renderer for the lock screen
|
||||
pub struct Renderer {
|
||||
@@ -16,31 +16,36 @@ pub struct Renderer {
|
||||
key_highlight_shown: bool,
|
||||
wrong_password_start: Option<Instant>,
|
||||
key_highlight_start: Option<Instant>,
|
||||
key_highlight_angle: f64,
|
||||
background: Option<ImageSurface>,
|
||||
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 {
|
||||
/// 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
|
||||
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)
|
||||
.expect("Failed to create Cairo surface");
|
||||
let context = Context::new(&surface).expect("Failed to create Cairo context");
|
||||
|
||||
Self {
|
||||
let mut renderer = Self {
|
||||
width,
|
||||
height,
|
||||
config,
|
||||
config: config.clone(),
|
||||
surface,
|
||||
context,
|
||||
fade_alpha: 0.0,
|
||||
@@ -48,13 +53,97 @@ impl Renderer {
|
||||
key_highlight_shown: false,
|
||||
wrong_password_start: None,
|
||||
key_highlight_start: None,
|
||||
key_highlight_angle: 0.0,
|
||||
background: None,
|
||||
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
|
||||
pub fn resize(&mut self, width: i32, height: i32) {
|
||||
log::debug!("Renderer::resize({}, {}) called", width, height);
|
||||
self.width = width;
|
||||
self.height = height;
|
||||
|
||||
@@ -83,6 +172,15 @@ impl Renderer {
|
||||
pub fn show_key_highlight(&mut self) {
|
||||
self.key_highlight_shown = true;
|
||||
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)
|
||||
@@ -92,61 +190,62 @@ impl Renderer {
|
||||
|
||||
/// Render the current frame
|
||||
pub fn render(&mut self) {
|
||||
log::info!(
|
||||
"Renderer::render() called, background: {}",
|
||||
self.background.is_some()
|
||||
);
|
||||
|
||||
// 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);
|
||||
// Clear the surface
|
||||
self.context.new_path();
|
||||
self.context.set_source_rgba(0.0, 0.0, 0.0, 1.0);
|
||||
self.context.paint().expect("Failed to clear surface");
|
||||
|
||||
// Draw background if available
|
||||
// Draw background
|
||||
if let Some(ref background) = self.background {
|
||||
let size = (background.width(), background.height());
|
||||
log::info!(
|
||||
"✓ Drawing background ({}x{}, fade_alpha: {})",
|
||||
size.0,
|
||||
size.1,
|
||||
self.fade_alpha
|
||||
);
|
||||
self.context.new_path();
|
||||
self.context
|
||||
.set_source_surface(background, 0.0, 0.0)
|
||||
.expect("Failed to set background source");
|
||||
.expect("Failed to set source");
|
||||
self.context
|
||||
.paint_with_alpha(self.fade_alpha)
|
||||
.expect("Failed to draw background");
|
||||
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");
|
||||
.expect("Failed to paint");
|
||||
}
|
||||
|
||||
// Draw clock if enabled
|
||||
if self.config.clock {
|
||||
self.draw_clock();
|
||||
}
|
||||
|
||||
// Draw indicator if enabled
|
||||
if self.config.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() {
|
||||
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 {
|
||||
self.draw_wrong_password_feedback();
|
||||
}
|
||||
|
||||
// Draw key highlight feedback if active
|
||||
if self.key_highlight_shown {
|
||||
self.draw_key_highlight_feedback();
|
||||
}
|
||||
@@ -154,96 +253,79 @@ impl Renderer {
|
||||
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)
|
||||
pub fn get_pixel_data(&self) -> Result<Vec<u8>, cairo::BorrowError> {
|
||||
let stride = self.surface.stride() as usize;
|
||||
let height = self.height as usize;
|
||||
|
||||
let mut data = vec![0u8; stride * height];
|
||||
self.surface.with_data(|src| {
|
||||
data.copy_from_slice(src);
|
||||
})?;
|
||||
|
||||
Ok(data)
|
||||
}
|
||||
|
||||
/// Get surface dimensions and stride
|
||||
pub fn surface_info(&self) -> (i32, i32, i32) {
|
||||
(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) {
|
||||
use chrono::Local;
|
||||
|
||||
let now = Local::now();
|
||||
let time_str = now.format("%H:%M").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);
|
||||
|
||||
// Center the text
|
||||
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.set_font_size(48.0);
|
||||
let te = self.context.text_extents(&time_str).unwrap();
|
||||
self.context
|
||||
.show_text(&time_str)
|
||||
.expect("Failed to draw time");
|
||||
.move_to(center_x - te.width() / 2.0, center_y + te.height() / 4.0);
|
||||
self.context.show_text(&time_str).unwrap();
|
||||
|
||||
// Draw date below time
|
||||
self.context.set_font_size(24.0);
|
||||
let date_extents = self
|
||||
.context
|
||||
.text_extents(&date_str)
|
||||
.expect("Failed to get date extents");
|
||||
let date_x = (self.width as f64 - date_extents.width()) / 2.0;
|
||||
let date_y = y + extents.height() + 20.0;
|
||||
self.context.new_path();
|
||||
self.context.set_font_size(14.0);
|
||||
let de = self.context.text_extents(&date_str).unwrap();
|
||||
self.context.move_to(
|
||||
center_x - de.width() / 2.0,
|
||||
center_y + te.height() / 4.0 + 25.0,
|
||||
);
|
||||
self.context.show_text(&date_str).unwrap();
|
||||
|
||||
self.context.move_to(date_x, date_y);
|
||||
self.context
|
||||
.show_text(&date_str)
|
||||
.expect("Failed to draw date");
|
||||
self.context.new_path();
|
||||
let ue = self.context.text_extents(&self.uptime_cache).unwrap();
|
||||
self.context.move_to(
|
||||
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) {
|
||||
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;
|
||||
let thickness = self.config.indicator_thickness as f64;
|
||||
|
||||
// Draw outer ring
|
||||
let ring_color = self.tuple_to_color(self.config.ring_color);
|
||||
self.context.set_source_rgba(
|
||||
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.new_path();
|
||||
let (r, g, b, a) = self.config.inside_color;
|
||||
self.context.set_source_rgba(r, g, b, a * self.fade_alpha);
|
||||
self.context.arc(
|
||||
center_x,
|
||||
center_y,
|
||||
@@ -251,62 +333,84 @@ impl Renderer {
|
||||
0.0,
|
||||
2.0 * std::f64::consts::PI,
|
||||
);
|
||||
self.context.fill().expect("Failed to fill inside");
|
||||
self.context.fill().unwrap();
|
||||
|
||||
// Draw separator line
|
||||
let separator_color = self.tuple_to_color(self.config.separator_color);
|
||||
if separator_color.a > 0 {
|
||||
self.context.set_source_rgba(
|
||||
separator_color.r as f64 / 255.0,
|
||||
separator_color.g as f64 / 255.0,
|
||||
separator_color.b as f64 / 255.0,
|
||||
separator_color.a as f64 / 255.0 * self.fade_alpha,
|
||||
let (lr, lg, lb, la) = self.config.line_color;
|
||||
if la > 0.0 {
|
||||
self.context.new_path();
|
||||
self.context
|
||||
.set_source_rgba(lr, lg, lb, la * self.fade_alpha);
|
||||
self.context.set_line_width(1.0);
|
||||
self.context.arc(
|
||||
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.move_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) {
|
||||
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_y = self.height as f64 / 2.0;
|
||||
let radius = self.config.indicator_radius as f64;
|
||||
let thickness = self.config.indicator_thickness as f64;
|
||||
|
||||
// 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.new_path();
|
||||
self.context.set_font_size(32.0);
|
||||
self.context.set_source_rgba(1.0, 1.0, 1.0, self.fade_alpha);
|
||||
|
||||
// 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);
|
||||
let te = self.context.text_extents(&self.password_display).unwrap();
|
||||
self.context
|
||||
.show_text(&self.password_display)
|
||||
.expect("Failed to draw password");
|
||||
.move_to(center_x - te.width() / 2.0, center_y + radius / 1.1);
|
||||
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) {
|
||||
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;
|
||||
|
||||
// Calculate flash intensity based on time
|
||||
let thickness = self.config.indicator_thickness as f64;
|
||||
let intensity = if let Some(start) = self.wrong_password_start {
|
||||
let elapsed = start.elapsed();
|
||||
let duration = std::time::Duration::from_millis(500);
|
||||
@@ -320,26 +424,24 @@ impl Renderer {
|
||||
};
|
||||
|
||||
if intensity > 0.0 {
|
||||
self.context.new_path();
|
||||
self.context
|
||||
.set_source_rgba(1.0, 0.0, 0.0, intensity * self.fade_alpha);
|
||||
self.context.set_line_width(thickness + 2.0);
|
||||
self.context
|
||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||
self.context
|
||||
.fill()
|
||||
.expect("Failed to draw wrong password feedback");
|
||||
self.context.stroke().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Draw key highlight feedback (green flash)
|
||||
fn draw_key_highlight_feedback(&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;
|
||||
|
||||
// Calculate flash intensity based on time
|
||||
let thickness = self.config.indicator_thickness as f64;
|
||||
let intensity = if let Some(start) = self.key_highlight_start {
|
||||
let elapsed = start.elapsed();
|
||||
let duration = std::time::Duration::from_millis(200);
|
||||
let duration = std::time::Duration::from_millis(300);
|
||||
if elapsed < duration {
|
||||
1.0 - (elapsed.as_secs_f64() / duration.as_secs_f64())
|
||||
} else {
|
||||
@@ -350,37 +452,370 @@ impl Renderer {
|
||||
};
|
||||
|
||||
if intensity > 0.0 {
|
||||
let key_hl_color = self.tuple_to_color(self.config.key_hl_color);
|
||||
self.context.set_source_rgba(
|
||||
key_hl_color.r as f64 / 255.0,
|
||||
key_hl_color.g as f64 / 255.0,
|
||||
key_hl_color.b as f64 / 255.0,
|
||||
key_hl_color.a as f64 / 255.0 * intensity * self.fade_alpha,
|
||||
let (r, g, b, a) = if self.caps_lock {
|
||||
self.config.caps_lock_key_hl_color
|
||||
} else {
|
||||
self.config.key_hl_color
|
||||
};
|
||||
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
|
||||
.arc(center_x, center_y, radius, 0.0, 2.0 * std::f64::consts::PI);
|
||||
self.context
|
||||
.fill()
|
||||
.expect("Failed to draw key highlight feedback");
|
||||
self.context.stroke().unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Update feedback timers and reset expired feedback
|
||||
fn update_feedback_timers(&mut self) {
|
||||
// Check wrong password feedback timeout
|
||||
self.update_uptime();
|
||||
if let Some(start) = self.wrong_password_start {
|
||||
if start.elapsed() > std::time::Duration::from_millis(500) {
|
||||
self.wrong_password_shown = false;
|
||||
self.wrong_password_start = None;
|
||||
}
|
||||
}
|
||||
|
||||
// Check key highlight feedback timeout
|
||||
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_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 }
|
||||
}
|
||||
|
||||
/// Get a reference to the underlying surface.
|
||||
pub fn surface(&self) -> &ImageSurface {
|
||||
&self.surface
|
||||
}
|
||||
|
||||
/// Consume the screenshot and return the underlying Cairo surface.
|
||||
pub fn into_inner(self) -> ImageSurface {
|
||||
self.surface
|
||||
@@ -46,9 +41,161 @@ impl Screenshot {
|
||||
if let Some((base, factor)) = config.effect_vignette {
|
||||
self.apply_vignette(base, factor);
|
||||
}
|
||||
if let Some(pixel_size) = config.effect_pixelate {
|
||||
self.apply_pixelate(pixel_size);
|
||||
}
|
||||
if let Some(angle) = config.effect_swirl {
|
||||
self.apply_swirl(angle);
|
||||
}
|
||||
if let Some(factor) = config.effect_melting {
|
||||
self.apply_melting(factor);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Apply a swirl effect.
|
||||
pub fn apply_swirl(&mut self, angle: f32) {
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
let center_x = width as f32 / 2.0;
|
||||
let center_y = height as f32 / 2.0;
|
||||
let radius = center_x.min(center_y);
|
||||
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.surface
|
||||
.with_data(|src| data.copy_from_slice(src))
|
||||
.unwrap();
|
||||
let original = data.clone();
|
||||
|
||||
for y in 0..height {
|
||||
for x in 0..width {
|
||||
let dx = x as f32 - center_x;
|
||||
let dy = y as f32 - center_y;
|
||||
let d = (dx * dx + dy * dy).sqrt();
|
||||
|
||||
if d < radius {
|
||||
let percent = (radius - d) / radius;
|
||||
let theta = percent * percent * angle;
|
||||
let s = theta.sin();
|
||||
let c = theta.cos();
|
||||
|
||||
let nx = (c * dx - s * dy + center_x) as i32;
|
||||
let ny = (s * dx + c * dy + center_y) as i32;
|
||||
|
||||
if nx >= 0 && nx < width && ny >= 0 && ny < height {
|
||||
let src_idx = (ny as usize * stride) + (nx as usize * 4);
|
||||
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
||||
data[dst_idx..dst_idx + 4].copy_from_slice(&original[src_idx..src_idx + 4]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut surface_data = self.surface.data().unwrap();
|
||||
surface_data.copy_from_slice(&data);
|
||||
}
|
||||
|
||||
/// Apply a melting effect (vertical smear).
|
||||
pub fn apply_melting(&mut self, factor: f32) {
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.surface
|
||||
.with_data(|src| data.copy_from_slice(src))
|
||||
.unwrap();
|
||||
|
||||
use rand::Rng;
|
||||
let mut rng = rand::thread_rng();
|
||||
|
||||
for x in 0..width {
|
||||
let mut melt_amount = 0.0;
|
||||
for y in 0..height {
|
||||
melt_amount += rng.gen_range(0.0..factor);
|
||||
let src_y = (y as f32 - melt_amount).max(0.0) as i32;
|
||||
|
||||
let src_idx = (src_y as usize * stride) + (x as usize * 4);
|
||||
let dst_idx = (y as usize * stride) + (x as usize * 4);
|
||||
|
||||
// Copy the pixel from above to create a smear
|
||||
let pixel = [
|
||||
data[src_idx],
|
||||
data[src_idx + 1],
|
||||
data[src_idx + 2],
|
||||
data[src_idx + 3],
|
||||
];
|
||||
data[dst_idx..dst_idx + 4].copy_from_slice(&pixel);
|
||||
}
|
||||
}
|
||||
|
||||
let mut surface_data = self.surface.data().unwrap();
|
||||
surface_data.copy_from_slice(&data);
|
||||
}
|
||||
|
||||
/// Pixelate the surface.
|
||||
pub fn apply_pixelate(&mut self, pixel_size: u32) {
|
||||
if pixel_size <= 1 {
|
||||
return;
|
||||
}
|
||||
|
||||
let width = self.surface.width();
|
||||
let height = self.surface.height();
|
||||
let stride = self.surface.stride() as usize;
|
||||
let mut data = vec![0u8; stride * height as usize];
|
||||
self.surface
|
||||
.with_data(|src| data.copy_from_slice(src))
|
||||
.unwrap();
|
||||
|
||||
for y in (0..height).step_by(pixel_size as usize) {
|
||||
for x in (0..width).step_by(pixel_size as usize) {
|
||||
let mut r = 0u32;
|
||||
let mut g = 0u32;
|
||||
let mut b = 0u32;
|
||||
let mut count = 0u32;
|
||||
|
||||
// Average pixels in the block
|
||||
for py in 0..pixel_size {
|
||||
for px in 0..pixel_size {
|
||||
let cur_x = x + px as i32;
|
||||
let cur_y = y + py as i32;
|
||||
if cur_x < width && cur_y < height {
|
||||
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
||||
r += data[index] as u32;
|
||||
g += data[index + 1] as u32;
|
||||
b += data[index + 2] as u32;
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
let r = (r / count) as u8;
|
||||
let g = (g / count) as u8;
|
||||
let b = (b / count) as u8;
|
||||
|
||||
// Fill the block
|
||||
for py in 0..pixel_size {
|
||||
for px in 0..pixel_size {
|
||||
let cur_x = x + px as i32;
|
||||
let cur_y = y + py as i32;
|
||||
if cur_x < width && cur_y < height {
|
||||
let index = (cur_y as usize * stride) + (cur_x as usize * 4);
|
||||
data[index] = r;
|
||||
data[index + 1] = g;
|
||||
data[index + 2] = b;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut surface_data = self.surface.data().unwrap();
|
||||
surface_data.copy_from_slice(&data);
|
||||
}
|
||||
|
||||
/// Apply a Gaussian blur effect.
|
||||
pub fn apply_blur(&mut self, radius: u32, times: u32) -> Result<()> {
|
||||
if radius == 0 || times == 0 {
|
||||
@@ -171,11 +318,6 @@ impl ScreenshotManager {
|
||||
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.
|
||||
///
|
||||
/// This method sends a screencopy request and returns the frame object.
|
||||
@@ -252,24 +394,24 @@ impl ScreenshotManager {
|
||||
flipped[dst_offset..dst_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,
|
||||
cairo::Format::ARgb32,
|
||||
info.width as i32,
|
||||
info.height 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,
|
||||
cairo::Format::ARgb32,
|
||||
info.width as i32,
|
||||
info.height 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 flags: Mutex<Option<Flags>>,
|
||||
pub buffer: Mutex<Option<Buffer>>,
|
||||
pub pool: Mutex<Option<SlotPool>>,
|
||||
}
|
||||
|
||||
impl CaptureData {
|
||||
@@ -324,6 +467,7 @@ impl CaptureData {
|
||||
info: Mutex::new(None),
|
||||
flags: Mutex::new(None),
|
||||
buffer: Mutex::new(None),
|
||||
pool: Mutex::new(None),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+277
@@ -0,0 +1,277 @@
|
||||
use 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")?;
|
||||
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