87 Commits
Author SHA1 Message Date
Bendik Aagaard LynghaugandClaude Fable 5.1 bf16d427b8 release: cargo-release config for tag-driven releases
`cargo release patch --execute` now does what the last three releases
did by hand: bump Cargo.toml/Cargo.lock, rewrite PKGBUILD's pkgver,
stamp the "## Unreleased" changelog section with version and date,
commit as "release X.Y.Z", tag vX.Y.Z and push commit and tag to the
gitea remote (not `origin`, which is the GitHub upstream). The tag then
triggers .gitea/workflows/release.yml as before. No crates.io publish.

CHANGELOG.md gets a standing "## Unreleased" section to collect entries
between releases; README documents the flow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-07 15:48:45 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 4f055935d4 backlight: run without a backlight device instead of panicking (0.4.3)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 43s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m20s
A config carrying a !backlight widget on a machine with nothing under
/sys/class/backlight (a laptop config on a desktop) hit
expect("did not find backlight device") in Backlight::new, which with
panic = "abort" killed wldash before a window appeared.

The device path is now Option<PathBuf>. A missing device, or a
configured `device` name that does not exist, prints one line on stderr
and leaves the widget in place so layout indices stay aligned: it
renders empty, skips sysfs reads and writes, and ignores clicks.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-05 13:54:13 +02:00
Bendik Aagaard LynghaugandClaude Fable 5.1 63179b9c47 fonts: fall back to fontconfig when a pinned font path is missing (0.4.2)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Failing after 0s
Release / package (aarch64, , aarch64, , , ) (push) Failing after 1s
A config generated on one machine pinned
/usr/share/fonts/google-noto/NotoSans-Regular.ttf; on another host the
same package installs under fonts/noto, so the shared config made the
fontloader thread panic ("Font file not found") and, with panic =
"abort", kill wldash before a window appeared.

resolve_font_path() now uses a pinned path only if the file exists,
otherwise asks fontconfig for the name (what fc-match does) and says so
on stderr. find_font() and FontLoader::from_path() return errors
instead of unwrapping, and an unresolvable font exits with one clear
line. Generated configs leave font_paths unset so names resolve per
machine; pinning remains possible by hand.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:50:02 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 d1df30d6d0 release 0.4.1
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 42s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m14s
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:42:49 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 9953865128 launcher: scan desktop files directly, keep state in XDG_STATE_HOME
The desktop-entry cache lived in $XDG_CACHE_HOME/wldash/desktop.json.
On an NFS-shared home that is a remote read and write on every start
(unless the session happens to export a machine-local XDG_CACHE_HOME),
and it went stale whenever a package was installed until someone ran
--desktop-refresh. Scanning the applications directories takes a few
milliseconds over local /usr/share and already happened on a background
thread, so the cache bought nothing: drop it, along with simd-json and
the serde derives on Desktop. --desktop-refresh stays as a no-op so old
key bindings keep working.

Calculator history and the prompt saved on Escape are state, not cache:
they move to $XDG_STATE_HOME/wldash (~/.local/state), written only on
Enter/Escape, and the old cache location is still read as a fallback.

With the serde derives gone, two Desktop fields were unused; use them:
Type=Link entries now open their URL with xdg-open, and Terminal=true
entries run under $TERMINAL -e when it is set.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:33:12 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 03a3c681b0 packaging: PKGBUILD, changelog and tag-driven release workflow (0.4.0)
Release / package (x86_64, /var/local/cargo-target/iris-cargo-home, bare, /usr/bin/sccache, /var/local/rustup, /var/local/cargo-target) (push) Successful in 1m12s
Release / package (aarch64, , aarch64, , , ) (push) Successful in 2m31s
Same shape as uhhm/gdo: a v* tag runs makepkg on the x86_64 (ergo,
"bare") and aarch64 (klokka) runners, uploads to the [uhhm] Arch
registry under namespace bl, and creates a release with the changelog
section as notes. The tag must agree with PKGBUILD's pkgver and
Cargo.toml's version. Without a REGISTRY_TOKEN secret the publish step
warns and skips so the build is still verified.

Version jumps to 0.4.0 because upstream already tagged v0.2.1 and
v0.3.0 on this history before the rewrite reset Cargo.toml to 0.2.0.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:28:22 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 1c6a536026 clippy: clean up all warnings
Mostly `cargo clippy --fix`: needless borrows, redundant field names,
match -> if let, io::Error::other, Option::filter/map, let chains, c""
literals. By hand: drop unused impl lifetimes, a type alias for the
font preload map, Iterator::find in next_buffer, contains_key, and
allow new_ret_no_self where constructors intentionally return the
boxed/generic wrapper type.

No functional change; tests and release build unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:20:25 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 42ffe9c492 cargo fmt
Pure formatting, no functional change.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:16:15 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 856f8d8bf4 readme: document the fork and /etc/wldash/config.yml precedence
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:57 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 eea3a78140 main: clear error messages and exit codes instead of panics
Missing/invalid config, unwritable config target, failed Wayland connect
and bad arguments now print one line to stderr and exit 1 rather than
panicking with "uh" or a serde backtrace.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:56 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 8c319cffc6 keyboard/state: handle repeat rate 0, let widgets see Escape
- wl_keyboard.repeat_info with rate 0 means "no repeat"; we divided by it.
- an unknown keymap format hit unreachable!() instead of being ignored.
- raw keycode 1 (Escape) returned before widgets were dispatched, so the
  launcher's Escape handler (which saves the prompt for Ctrl+R) never ran.
  Dispatch first, then fall back to quitting.
- always lay out on the first layer-surface configure, even when the
  compositor hands us exactly the placeholder dimensions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:56 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 450a20bf63 widget: fix Geometry::expand using other.height twice
The bounding-box height was computed from other.height + other.height
instead of other.height + other.y, so the combined damage rectangle sent
to the compositor was wrong whenever a widget's drawn area changed.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:55 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 6af81b587c buffer: switch to memmap2, destroy shm pools, never underflow refcounts
memmap 0.7 is unmaintained (RUSTSEC-2020-0077); memmap2 is a drop-in
replacement. Drop the unused shlex dependency.

Each resize created a new wl_shm_pool that was never destroyed; keep the
pool on the buffer and destroy it with it. A wl_buffer.release for a
buffer we didn't consider acquired used to underflow refcnt and wedge
rendering. Fix the lifetime-elision warnings while here.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:54 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 c033335912 widgets: stop aborting on machines without backlight/battery/pulseaudio
The release profile builds with panic = "abort", so a panic in any helper
thread took the whole dashboard down:

- backlight: WalkDir yields /sys/class/backlight itself, so detect() was
  always true and new() picked the class dir as the device, then panicked
  reading brightness. Use min_depth(1) and tolerate read errors.
- battery: detect() only checked that UPower's DisplayDevice answered,
  which it does on every machine; use IsPresent. The monitor thread now
  returns an error instead of unwrapping, and publishes its first reading.
- pulseaudio: a failed connection panicked ("damn"); clicks before the
  first sink query unwrapped a None channel map. Both are now handled, and
  the connect loop no longer busy-spins.
- bar widget: don't underflow when narrower than the label column.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:54 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 01af00b5ea desktop: hash entries the same way they compare, only parse *.desktop
Desktop derived Hash over every field but PartialEq compared only the
name, violating the Hash/Eq contract the launcher's HashMap relies on
(clippy denies this by default, so `cargo clippy` failed to build).

Also skip directories and files without a .desktop extension (mimeinfo
cache, .directory files, ...) and stop unwrapping non-UTF-8 paths.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:53 +00:00
Bendik Aagaard LynghaugandClaude Fable 5.1 2bed8b7fac launcher: make the prompt cursor consistently grapheme-based
set(), end() and move_cursor() used the byte length of the input as the
cursor bound while append/backspace/delete/clear_* index a grapheme table.
Any non-ASCII input (æøå, emoji) followed by End or typing past the end
indexed out of bounds and, with panic = "abort" in release, killed wldash.

While here:
- clamp Up so the selection can't run past the last row (every widget now
  reports its row count), and make mouse selection underflow-safe
- strip all desktop-entry field codes from Exec (%i %c %k ... and %% -> %),
  not just %f/%F/%u/%U
- report a failed spawn instead of panicking
- size the highlight colour table by chars, matching the drawing code

Adds unit tests for the prompt and the field-code stripping.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-03 09:15:53 +00:00
Bendik Aagaard LynghaugandClaude Opus 4.8 1367bd8591 config: prefer machine-local /etc/wldash/config.yml when present
On a shared (NFS-roamed) home, ~/.config/wldash/config.yml is identical on
every machine. Prefer a host-specific /etc/wldash/config.yml when it exists so
each machine can keep its own layout without conflicting; --config still
overrides both. Enables the 'factor host-specific config to /etc' pattern for
shared-home fleets.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-09-16 14:16:22 +02:00
Alejandro Visiedo 1156b3503a fix: ensure dirs exist
Ensure ~/.config/wldash and ~/.cache/wldash exist.
Provide hints when no configuration exists.

Signed-off-by: Alejandro Visiedo <alejandro.visiedo@gmail.com>
2025-05-08 12:59:41 +02:00
Kenny Levinsen d80a64444f cargo: Update lock 2025-04-08 11:51:15 +02:00
Kenny Levinsen 456d61de01 cargo: Set binary name back to wldash
During v2 development, the name was temporarily set to wldash2. Set it
back to wldash so that things work.
2025-03-20 16:57:58 +01:00
Kenny Levinsen ab2655556f readme: Make note of rewrite 2025-02-27 17:42:09 +01:00
Kenny Levinsen f0331e65b1 cargo: Bump to 2024 edition 2025-02-27 17:41:08 +01:00
Kenny Levinsen 25c7322c4b cargo: Bump version to 0.2 2025-02-27 17:40:56 +01:00
Kenny Levinsen 7af5c34856 readme: Update for v2 2025-02-27 17:39:49 +01:00
Kenny Levinsen e8def40fae cargo: Make fontconfig a default feature 2025-02-27 17:39:40 +01:00
Kenny Levinsen 34591f2677 ci: Add libxkbcommon 2025-02-27 16:53:24 +01:00
Kenny Levinsen 91c26b9d54 Merge new implementation
This implementation is a rewrite of wldash, implemented from scratch.
This also means that it is an unrelated history, hence the history now
having two initial commits.
2025-02-27 16:41:33 +01:00
Kenny Levinsen a8cd777ca9 Clean up command-line arguments, usage text 2025-02-27 16:38:52 +01:00
Kenny Levinsen 83179d2c7e state: Increase xdg_toplevel default dimensions
The previous values are too small for the default toplevel layout.
2025-02-27 16:38:52 +01:00
Kenny Levinsen 7e112ddd1f keyboard: Update to xkbcommon 0.8 2025-02-27 16:38:52 +01:00
Kenny Levinsen 9fd054fc2c Fix damage issue when multiple buffers are in use
wldash was originally written to expect server-side shm buffer
optimizations, which make it so only a single buffer has to be in use.
This simplifies damage tracking and repaint needs.

Support for using multiple buffers was added to support e.g., jay, but
the damage handling was not in place for this to work. Fix that by
tracking damage per buffer.
2025-02-27 16:38:52 +01:00
Kenny Levinsen 40aec97f79 Cargo format 2025-02-27 14:48:41 +01:00
Kenny Levinsen 9031361dd1 Cargo update 2025-02-27 14:48:41 +01:00
Kenny Levinsen f337ce6e95 layer-shell: Change to overlay 2025-01-12 23:08:24 +01:00
Kenny Levinsen bc9ba0045c launcher: Support launch_cmd config 2025-01-12 22:51:45 +01:00
Kenny Levinsen 2b3048faca Rework the initial wayland init a bit 2025-01-12 22:50:00 +01:00
Kenny Levinsen f09abf1d53 backlight: Auto-detect type 2025-01-12 22:47:16 +01:00
Kenny Levinsen 5776886884 keyboard: Fix repeat deadline handling
The calloop timer is only responsible for ensuring wakeup at the right
time, but we can also be woken up before time so we need to check the
deadline.

At the same time, We may end up being significantly delayed past the
deadline if something else was slow. If we are too far behind, reset the
base of the repeat timer by just offsetting from the current time.
2024-01-30 00:56:12 +01:00
Kenny Levinsen 7ee58d8d68 server: Make optional 2023-11-10 15:22:37 +01:00
Kenny Levinsen c169843bcb Add basic server operation 2023-11-10 15:10:10 +01:00
Kenny Levinsen 1d1f7d3a8e draw: Fix a bug I forgot the details of 2023-11-10 14:01:14 +01:00
Kenny Levinsen a882e26e2b widgets/backlight: Set support 2023-11-10 14:00:48 +01:00
Kenny Levinsen 84c241a90e keyboard: Use xkb_common::keysyms 2023-11-10 14:00:06 +01:00
Kenny Levinsen 921938cc0d Resolve keyboard after first draw 2023-11-10 13:59:09 +01:00
Kenny Levinsen 8835708b31 Make background configurable 2023-11-10 13:58:56 +01:00
Kenny Levinsen c01c810c10 Configurable config file 2023-09-28 23:40:20 +02:00
Kenny Levinsen b451a2de31 Fix keyboard resolve on sway 2023-08-22 14:39:20 +02:00
Kenny Levinsen ebeb21f3c2 Fix fontmap resolution on sway 2023-08-22 14:38:36 +02:00
Kenny Levinsen 558d528f70 Fix xdg_toplevel background 2023-05-20 17:34:52 +02:00
Kenny Levinsen 6351cc242d launcher: Use XDG_ACTIVATION_TOKEN for shell cmds 2023-04-04 01:53:29 +02:00
Kenny Levinsen 66e66e19ad launcher: Cache desktop files 2023-04-04 01:53:02 +02:00
Kenny Levinsen 3a77696569 calendar: Missed preload of 'N' 2023-03-01 11:48:25 +01:00
Kenny Levinsen a975da5074 config: Apply margins to default 2023-02-14 23:52:54 +01:00
Kenny Levinsen 9d1f310f12 cargo fmt 2023-02-14 10:23:20 +01:00
Kenny Levinsen 54e4ba16dd Add InvertedVerticalLayout 2023-02-14 10:23:12 +01:00
Kenny Levinsen 764a1e3444 config: Remove default configuration 2023-02-14 00:30:46 +01:00
Kenny Levinsen 6fac465c3a calendar: Highlight current week 2023-02-14 00:28:40 +01:00
Kenny Levinsen 5097faab66 launcher: Save prompt on exit 2023-02-14 00:28:40 +01:00
Kenny Levinsen 8202963a6f Add vertical line 2023-02-14 00:28:40 +01:00
Kenny Levinsen b09f642fd6 Avoid panic on very long prompt lines 2023-02-14 00:28:40 +01:00
Kenny Levinsen 59e2f658db keyboard: Rename realize to resolve 2023-02-13 19:43:15 +01:00
Kenny Levinsen 022f6a719a cargo fmt 2023-02-13 18:49:44 +01:00
Kenny Levinsen 8826cab89d keyboard: Add keyrepeat handling 2023-02-13 17:39:54 +01:00
Kenny Levinsen 9e48eb053d config: Generate configs 2023-02-12 17:02:44 +01:00
Kenny Levinsen db085168dc config: Add config support 2023-02-11 18:24:34 +01:00
Kenny Levinsen 353ed1f189 launcher: Shrink margin 2023-02-08 22:57:06 +01:00
Kenny Levinsen 45e5a750f9 calendar: Fix hiding when shrunk 2023-02-08 22:56:52 +01:00
Kenny Levinsen 6e90b1f0e8 More better 2023-02-08 22:53:41 +01:00
Kenny Levinsen 3c69a31030 calc: Store history 2023-02-07 00:36:06 +01:00
Kenny Levinsen b9f4e74062 calc: Improve history 2023-02-06 18:57:34 +01:00
Kenny Levinsen 6f0ca36826 Moar speed 2023-02-06 18:34:07 +01:00
Kenny Levinsen 69959e86dc Fix memset hack 2023-02-04 16:52:32 +01:00
Kenny Levinsen 74591902e9 Support toggle between V1 and V2 UIs 2023-02-04 16:52:18 +01:00
Kenny Levinsen 3a2badbfd9 wlr-layer-shell support 2023-02-04 14:42:29 +01:00
Kenny Levinsen b49b9e9764 More stuff 2023-02-02 23:52:59 +01:00
Kenny Levinsen 7593cd48a4 Add calendar, refactor more 2023-01-16 00:25:34 +01:00
Kenny Levinsen 3a40bb18c5 Refactor slightly 2023-01-13 21:53:07 +01:00
Kenny Levinsen b2a9475e4c cargo fmt 2023-01-12 00:13:27 +01:00
Kenny Levinsen be6ee9899c cargo update 2023-01-12 00:13:15 +01:00
Kenny Levinsen d3002d2180 Exit when no longer activated 2023-01-12 00:13:08 +01:00
Kenny Levinsen 25644c2b15 backlight: Add 2023-01-11 23:54:52 +01:00
Kenny Levinsen dd790468ad battery: Minor cleanup 2023-01-11 23:54:39 +01:00
Kenny Levinsen 0a4544a608 launcher: Add ctrl k/u handling 2023-01-11 23:54:14 +01:00
Kenny Levinsen 8e80c0a69c Initial commit 2023-01-11 00:28:54 +01:00
Kenny Levinsen 70e53c1246 cargo: Update dependencies 2022-12-04 13:58:14 +01:00
Kenny Levinsen b2ae30bab4 Update fontconfig 2022-05-02 11:21:47 +02:00
Kenny Levinsen 2538e41f00 Update Cargo.lock 2022-01-16 23:16:30 +01:00
51 changed files with 6167 additions and 5829 deletions
+1
View File
@@ -4,6 +4,7 @@ packages:
- libpulse - libpulse
- alsa-lib - alsa-lib
- fontconfig - fontconfig
- libxkbcommon
sources: sources:
- https://git.sr.ht/~kennylevinsen/wldash - https://git.sr.ht/~kennylevinsen/wldash
tasks: tasks:
+114
View File
@@ -0,0 +1,114 @@
name: Release
# A `v*` tag builds the package on each architecture's own runner
# (x86_64 on ergo's bare runner, aarch64 on klokka's) and uploads it
# to the [uhhm] Arch registry, like uhhm/gdo and bl/djdeck. Hosts then
# `pacman -S wldash`. The tag has to agree with PKGBUILD's pkgver and
# Cargo.toml's version, and CHANGELOG.md has to have the section; a
# release with the tag's notes is created from the x86_64 job.
on:
push:
tags: ["v*"]
jobs:
package:
strategy:
matrix:
include:
- arch: x86_64
runs-on: bare
# The bare runner's shared toolchain and caches, with the
# writable cargo home uhhm/iris and uhhm/gdo use.
cargo_home: /var/local/cargo-target/iris-cargo-home
rustc_wrapper: /usr/bin/sccache
rustup_home: /var/local/rustup
target_dir: /var/local/cargo-target
- arch: aarch64
runs-on: aarch64
# klokka's host runner: rustup's stable (step below), defaults everywhere.
cargo_home: ""
rustc_wrapper: ""
rustup_home: ""
target_dir: ""
runs-on: ${{ matrix.runs-on }}
env:
CARGO_HOME: ${{ matrix.cargo_home }}
RUSTC_WRAPPER: ${{ matrix.rustc_wrapper }}
RUSTUP_HOME: ${{ matrix.rustup_home }}
CARGO_TARGET_DIR: ${{ matrix.target_dir }}
PATH: /var/local/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/bin
SCCACHE_DIR: /var/local/sccache
SCCACHE_SERVER_PORT: "4228"
steps:
# v4 built-in checkout: native Go, needs no Node (the aarch64 host runner
# has none) and keeps the token out of a fetch URL.
- name: Checkout
uses: builtin:checkout
# klokka's runner has rustup but no default toolchain, so its cargo
# proxy refuses to run without this.
- name: Install Rust stable (aarch64)
if: matrix.arch == 'aarch64'
uses: dtolnay/rust-toolchain@stable
- name: Check tag matches pkgver and Cargo.toml
run: |
want="${GITHUB_REF_NAME#v}"
have=$(sed -n 's/^pkgver=//p' PKGBUILD)
[ "$want" = "$have" ] || { echo "tag $GITHUB_REF_NAME != pkgver $have" >&2; exit 1; }
cargo_ver=$(sed -n 's/^version = "\(.*\)"/\1/p' Cargo.toml | head -1)
[ "$want" = "$cargo_ver" ] || { echo "tag $GITHUB_REF_NAME != Cargo.toml version $cargo_ver" >&2; exit 1; }
notes=$(awk -v v="$want" '/^## /{p = ($2 == v)} p' CHANGELOG.md | tail -n +2)
[ -n "$(printf "%s" "$notes" | tr -d "[:space:]")" ] || { echo "CHANGELOG.md has no section for $GITHUB_REF_NAME" >&2; exit 1; }
# Bare runners are never mutated by CI: wldash's system libraries
# (dbus, libpulse, libxkbcommon, fontconfig, wayland) must already be
# installed on the host. Fail loud if one is missing.
- name: Check system build deps
run: |
missing=""
for lib in dbus-1 libpulse xkbcommon fontconfig wayland-client; do
pkg-config --exists "$lib" || missing="$missing $lib"
done
[ -z "$missing" ] || { echo "::error::bare runner missing:$missing — install once on the host" >&2; exit 1; }
# -d: cargo is the shared toolchain, not a pacman package the
# runner could verify. Builds and tests from the checkout. Empty
# env values above are unset here so cargo's defaults apply.
- name: Build package
run: |
for v in CARGO_HOME RUSTC_WRAPPER RUSTUP_HOME CARGO_TARGET_DIR; do
eval "[ -n \"\$$v\" ]" || unset "$v"
done
makepkg -f -d --noconfirm
# REGISTRY_TOKEN is a write:package token for bl, a secret on this
# repo. The registry namespace is always bl: pacman.conf's [uhhm]
# points at /api/packages/bl/arch/uhhm. Without the secret the build
# still runs so the tag is verified; re-run the job once it is set.
- name: Publish to the Arch package registry
run: |
set -euo pipefail
if [ -z "${{ secrets.REGISTRY_TOKEN }}" ]; then
echo "::warning::REGISTRY_TOKEN not set — skipping Arch registry publish"
exit 0
fi
for pkg in $(makepkg --packagelist); do
echo "==> $pkg"
curl --fail-with-body --user "bl:${{ secrets.REGISTRY_TOKEN }}" \
--upload-file "$pkg" \
"${{ gitea.server_url }}/api/packages/bl/arch/uhhm"
done
- name: Release notes
if: matrix.arch == 'x86_64'
run: |
set -euo pipefail
tag="${{ github.ref_name }}"
api="${{ github.server_url }}/api/v1/repos/${{ github.repository }}"
auth="Authorization: token ${{ secrets.GITHUB_TOKEN }}"
notes=$(awk -v v="${tag#v}" '/^## /{p = ($2 == v)} p' CHANGELOG.md | tail -n +2)
body=$(jq -n --arg tag "$tag" --arg notes "$notes" '{tag_name: $tag, name: $tag, body: $notes}')
curl -sf -X POST -H "$auth" -H 'Content-Type: application/json' -d "$body" "$api/releases" > /dev/null \
|| echo "release $tag exists"
-1
View File
@@ -1,2 +1 @@
/target /target
**/*.rs.bk
+33
View File
@@ -0,0 +1,33 @@
# Changelog
## Unreleased
## 0.4.3 (2026-10-05)
- Backlight: a config with a `!backlight` widget on a machine without `/sys/class/backlight` entries (or with a `device` name that does not exist) panicked at start ("did not find backlight device"), fatal with `panic = "abort"`. The widget now warns once on stderr, renders empty and ignores clicks, so one config can be shared between a laptop and a desktop.
## 0.4.2 (2026-10-03)
- Fonts: a `font_paths` entry pointing at a file that does not exist on this machine made the font-loader thread panic and abort wldash ("Font file not found"). Pinned paths that are missing now fall back to fontconfig by name, with one line on stderr; a font that cannot be resolved at all exits with a clear message instead of a backtrace.
- `--write-default-config` no longer writes absolute font paths. Configs shared between machines with different font package layouts (`fonts/noto` vs `fonts/google-noto`) were breaking on the second host; names are resolved per machine instead. Existing configs keep working, and `font_paths` can still pin a file on purpose.
## 0.4.1 (2026-10-03)
- No more desktop-file cache: wldash scanned `~/.cache/wldash/desktop.json` (on NFS for shared homes, and stale after every package install) and needed a manual `--desktop-refresh`. Desktop files are now scanned on a background thread at every start, a few milliseconds over local `/usr/share`. `--desktop-refresh` is a no-op kept for key bindings; `simd-json` is gone.
- Calculator history and the Escape-saved prompt moved from `$XDG_CACHE_HOME/wldash` to `$XDG_STATE_HOME/wldash` (`~/.local/state`), where small roaming state belongs; old files are still read.
- Launcher: `Type=Link` entries open their URL with `xdg-open`; `Terminal=true` entries run inside `$TERMINAL -e` when that variable is set.
## 0.4.0 (2026-10-03)
First release of the project.uhhm.no fork, packaged for the `[uhhm]` Arch registry. Numbering: upstream's old code base was tagged up to v0.3.0 and the rewrite called itself 0.2.0; this fork continues above both so pacman sees a monotonic version.
- Launcher: the prompt cursor was a byte offset in some places and a grapheme index in others, so typing æøå and then End or another key crashed wldash. The cursor is grapheme-based everywhere, Up stops at the last row, mouse selection can't underflow, and all desktop Exec field codes (`%i`, `%c`, `%k`, `%%`, ...) are stripped.
- No more aborting on machines without a backlight, battery or PulseAudio: `/sys/class/backlight` itself was taken for a device, UPower's always-present DisplayDevice for a battery, and a failed audio connection panicked a thread (fatal with `panic = "abort"`).
- Redraws: `Geometry::expand` computed the damage height from the wrong field; buffer refcounts no longer underflow; `wl_shm_pool`s are destroyed on resize.
- Input: a key-repeat rate of 0 divided by zero; Escape is now seen by the launcher first so the prompt is saved for Ctrl+R; the first layer-surface configure always lays out.
- Config: `/etc/wldash/config.yml` takes precedence over `~/.config/wldash/config.yml` when present (machine-local layout on NFS-shared homes); `--config` overrides both. Errors print one line and exit 1 instead of panicking.
- Housekeeping: `memmap` → `memmap2`, unused `shlex` dropped, zero rustc/clippy warnings, rustfmt applied, unit tests for the prompt, Exec parsing and geometry.
## 0.2.0
Upstream (kennylevinsen/wldash) rewrite, the state this fork started from.
Generated
+608 -338
View File
File diff suppressed because it is too large Load Diff
+26 -35
View File
@@ -1,50 +1,41 @@
[package] [package]
name = "wldash" name = "wldash"
version = "0.3.0" version = "0.4.3"
authors = ["Kenny Levinsen <kl@kl.wtf>"] edition = "2024"
edition = "2018"
[profile.release] [profile.release]
#debug = true
lto = "fat" lto = "fat"
panic = "abort" panic = "abort"
opt-level = "z"
[features] [features]
default = ["alsa-widget", "pulseaudio-widget", "yaml-cfg", "json-cfg"] default = ["fontconfig"]
damage_debug = []
yaml-cfg = ["serde_yaml"]
json-cfg = ["serde_json"]
alsa-widget = ["alsa"]
pulseaudio-widget = ["libpulse-binding"]
[dependencies] [dependencies]
byteorder = "1.3" wayland-client = { version = "0.30", features = ["calloop", "log"] }
bitflags = "1.0" wayland-protocols = { version = "0.30", features = ["client", "unstable", "staging"] }
dlib = "0.4" wayland-protocols-wlr = { version = "0.1", features = ["client"] }
rusttype = "^0.9" nix = "0.26" # 0.27 is breaking
memmap2 = "0.9"
fontconfig = { version = "0.8.0", optional = true }
rusttype = "0.9"
# fontdue = "0.7"
calloop = { version = "0.10" } # update breaks
chrono = "0.4" chrono = "0.4"
nix = "0.16"
memmap = "0.7" walkdir = "2.4"
os_pipe = "0.9"
wayland-client = { version = "0.28"} unicode-segmentation = "1.10"
wayland-protocols = { version = "0.28", features = ["client", "unstable_protocols"] }
dbus = "0.6"
fuzzy-matcher = "0.3" fuzzy-matcher = "0.3"
lazy_static = "1.4"
rcalc_lib = "0.9" xkbcommon = { version = "0.8", features = ["wayland"] }
rust-ini = "0.14"
shlex = "0.1" rcalc_lib = "1.0"
timerfd = "1.0.0"
walkdir = "2.3.2" #alsa = { version = "0.7", optional = true }
itertools = "0.10.3" dbus = "0.9"
libpulse-binding = "2.28"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = { version = "1.0", optional = true } serde_yaml = "0.9"
serde_yaml = { version = "0.8", optional = true }
libpulse-binding = { version = "2.22", optional = true }
alsa = { version = "0.4.0", optional = true }
fontconfig = "0.2.0"
unicode-segmentation = "1.6.0"
+40
View File
@@ -0,0 +1,40 @@
# Maintainer: Bendik Aagaard Lynghaug <bendik.lynghaug@gmail.com>
# Built from the tagged checkout by .gitea/workflows/release.yml, once
# per architecture, and uploaded to the [uhhm] registry on
# project.uhhm.no (namespace bl). pkgver is the version; the tag has
# to agree with it and with Cargo.toml.
pkgname=wldash
pkgver=0.4.3
pkgrel=1
pkgdesc="Dashboard/launcher/control-panel for wlroots-style Wayland compositors (layer-shell)"
arch=('x86_64' 'aarch64')
url="https://project.uhhm.no/bl/wldash"
license=('GPL-3.0-only')
depends=('glibc' 'gcc-libs' 'dbus' 'libpulse' 'libxkbcommon' 'fontconfig' 'wayland')
makedepends=('cargo')
optdepends=('upower: battery widget'
'pipewire-pulse: volume widget (or pulseaudio)')
# !lto: Cargo.toml already does lto = "fat"; makepkg's -flto in RUSTFLAGS
# would only fight it. !debug: panic = "abort" release, nothing to strip.
options=('!debug' '!lto')
# Sources are the checkout itself: makepkg resolves local sources by
# basename next to the PKGBUILD, so the tagged tree CI builds from is
# the source of truth, as in uhhm/gdo.
build() {
cd "$startdir"
cargo build --release --locked
}
check() {
cd "$startdir"
cargo test --release --locked
}
package() {
cd "$startdir"
install -Dm755 "${CARGO_TARGET_DIR:-target}/release/wldash" "$pkgdir/usr/bin/wldash"
install -Dm644 README.md "$pkgdir/usr/share/doc/$pkgname/README.md"
install -Dm644 LICENSE "$pkgdir/usr/share/licenses/$pkgname/LICENSE"
}
+29 -6
View File
@@ -6,16 +6,31 @@ A dashboard/launcher/control-panel thing for Wayland. Requires wlr-layer-shell-u
Consider this alpha-quality: While it works, *everything* is subject to change with a moments notice. You may end up pulling in the latest changes, and *poof*, wldash suddenly turns into an artisan espresso machine. Consider this alpha-quality: While it works, *everything* is subject to change with a moments notice. You may end up pulling in the latest changes, and *poof*, wldash suddenly turns into an artisan espresso machine.
## Rewrite note
wldash has received a significant rewrite. The old version is available in the wldash1 branch for now.
## This fork
This is a maintained fork of [kennylevinsen/wldash](https://github.com/kennylevinsen/wldash)
living at <https://project.uhhm.no/bl/wldash>. On top of upstream it fixes a number of
crashes (non-ASCII input in the launcher, machines without a backlight, battery or
PulseAudio, key-repeat rate 0, ...), corrects redraw damage tracking, and prefers a
machine-local `/etc/wldash/config.yml` over the per-user config when present, which keeps
per-host layouts apart on NFS-shared homes.
## How to use ## How to use
1. Checkout: `git clone https://github.com/kennylevinsen/wldash` 1. Checkout: `git clone https://project.uhhm.no/bl/wldash`
2. Build: `cargo build --release` 2. Build: `cargo build --release`
3. Put somewhere: `cp target/release/wldash /usr/local/bin/wldash` 3. Put somewhere: `cp target/release/wldash /usr/local/bin/wldash`
4. Run: `wldash` 4. Run: `wldash`
To see the default configuration, run `wldash print-config`. To configure, place a file in `$XDG_CONFIG_HOME/wldash/config.yaml` (or if `XDG_CONFIG_HOME` is not set, `~/.config/wldash/config.yaml`). JSON is also currently supported. To generate a new default configuration, run `wldash --write-default-config`. To configure, edit `$XDG_CONFIG_HOME/wldash/config.yml` (or if `XDG_CONFIG_HOME` is not set, `~/.config/wldash/config.yml`). If `/etc/wldash/config.yml` exists it takes precedence over the per-user file; `--config FILE` overrides both.
Notable settings: `outptuMode` can be `active` or `all`, `scale` can be set to `2` to half the buffer size, and the widgets (and their layout) can be configured. Fonts are named (`sans`, `monospace`, or any fontconfig name) and resolved through fontconfig on the machine wldash runs on. `font_paths` in the config can pin a name to a file; if that file does not exist on the current machine, wldash says so on stderr and falls back to fontconfig rather than failing, so one config can roam between hosts with different font package layouts.
Desktop files are scanned every time wldash starts (on a background thread), so there is no cache to refresh; `--desktop-refresh` is kept as a no-op for old key bindings. Calculator history and the prompt saved on Escape live in `$XDG_STATE_HOME/wldash` (`~/.local/state/wldash`); older installs' files under `~/.cache/wldash` are still read.
For more info, look in `src/config.rs`. For more info, look in `src/config.rs`.
@@ -63,9 +78,17 @@ The launcher also accepts prefix operators to change its mode:
## Notable missing features: ## Notable missing features:
- Scaling of any kind - all sizes are currently hardcoded in pixels - Scaling of any kind - all sizes are currently hardcoded in pixels
- Configurability
- Cleanup and reorganization ## Releasing
- Proper line editor for the launcher
Releases are cut with [cargo-release](https://github.com/crate-ci/cargo-release) (`cargo install cargo-release`), configured in `release.toml`. Put a line for each change under `## Unreleased` in `CHANGELOG.md` as you go, then on a clean `master`:
```
cargo release patch # dry run: shows the bump, replacements and tag
cargo release patch --execute # do it (minor/major or an explicit version also work)
```
This bumps `Cargo.toml`, `Cargo.lock` and `PKGBUILD` to the same version, stamps the changelog section with the version and date, commits as `release X.Y.Z`, tags `vX.Y.Z` and pushes commit and tag to the `gitea` remote. The tag triggers `.gitea/workflows/release.yml`, which builds the package per architecture and uploads it to the `[uhhm]` registry. The workflow refuses a tag whose version disagrees with `PKGBUILD` or `Cargo.toml`, which cannot happen when the tag comes from cargo-release.
## How to discuss ## How to discuss
-55
View File
@@ -1,55 +0,0 @@
{
"widget": {
"margin": {
"margins": [20,20,20,20],
"widget": {
"verticalLayout": [{
"horizontalLayout": [{
"margin": {
"margins": [0,88,0,32],
"widget": {
"verticalLayout": [
{"date": {"font_size": 64.0}},
{"clock": {"font_size": 256.0}}
]
}
}
},{
"verticalLayout": [{
"margin": {
"margins": [0,0,0,8],
"widget": {"battery": {"font_size": 24.0,"length": 600}}
}
},{
"margin": {
"margins": [0,0,0,8],
"widget": {"backlight": {"device": "intel_backlight", "font_size": 24.0,"length": 600}}
}
},{
"margin": {
"margins": [0,0,0,8],
"widget": {"pulseAudio": {"font_size": 24.0,"length": 600}}
}
}]
}
]},
{"calendar": {"font_size": 16.0,"sections": 3}},
{"launcher": {
"font_size": 32.0,
"length": 1200,
"app_opener": "swaymsg exec -- ",
"term_opener": "foot --",
"url_opener": "xdg-open "
}}
]}
}
},
"outputMode": "active",
"scale": 1,
"background": {
"red": 0,
"green": 0,
"blue": 0,
"opacity": 0.9
}
}
-50
View File
@@ -1,50 +0,0 @@
outputMode: active
scale: 1
background:
red: 0.0
green: 0.0
blue: 0.0
opacity: 0.9
widget:
margin:
margins: [20, 20, 20, 20]
widget:
verticalLayout:
- horizontalLayout:
- margin:
margins: [0, 88, 0, 32]
widget:
verticalLayout:
- date:
font_size: 64.0
- clock:
font_size: 256.0
- verticalLayout:
- margin:
margins: [0, 0, 0, 8]
widget:
battery:
font_size: 24.0
length: 600
- margin:
margins: [0, 0, 0, 8]
widget:
backlight:
device: "intel_backlight"
font_size: 24.0
length: 600
- margin:
margins: [0, 0, 0, 8]
widget:
pulseAudio:
font_size: 24.0
length: 600
- calendar:
font_size: 16.0
sections: 3
- launcher:
font_size: 32.0
length: 1200
app_opener: "swaymsg exec --"
term_opener: "foot --"
url_opener: "xdg-open "
+26
View File
@@ -0,0 +1,26 @@
# cargo-release settings. Cut a release with
#
# cargo release patch # dry run, shows what would change
# cargo release patch --execute
#
# (or `minor`, `major`, or an explicit version). It bumps Cargo.toml and
# Cargo.lock, rewrites PKGBUILD's pkgver, turns the "## Unreleased"
# section of CHANGELOG.md into "## X.Y.Z (date)", commits, tags
# vX.Y.Z and pushes commit + tag to the gitea remote. That tag is what
# .gitea/workflows/release.yml builds and uploads to the [uhhm] registry.
# Not on crates.io; the package goes to the Arch registry via the tag.
publish = false
# `origin` is the upstream kennylevinsen fork on GitHub; releases go to
# project.uhhm.no, and the workflow only runs there.
push-remote = "gitea"
allow-branch = ["master"]
tag-name = "v{{version}}"
tag-message = "wldash {{version}}"
pre-release-commit-message = "release {{version}}"
pre-release-replacements = [
{ file = "PKGBUILD", search = "^pkgver=.*", replace = "pkgver={{version}}", exactly = 1 },
{ file = "PKGBUILD", search = "^pkgrel=.*", replace = "pkgrel=1", exactly = 1 },
{ file = "CHANGELOG.md", search = "^## Unreleased$", replace = "## {{version}} ({{date}})", exactly = 1 },
]
-600
View File
@@ -1,600 +0,0 @@
use std::collections::VecDeque;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use chrono::{Duration, Local, NaiveDateTime};
use crate::keyboard::{keysyms, map_keyboard, Event as KbEvent, KeyState, ModifiersState};
use wayland_client::protocol::{wl_compositor, wl_output, wl_pointer, wl_seat, wl_shm, wl_surface};
use wayland_client::{Display, EventQueue, GlobalEvent, GlobalManager, Main};
use wayland_protocols::wlr::unstable::layer_shell::v1::client::{
zwlr_layer_shell_v1, zwlr_layer_surface_v1,
};
use crate::buffer::Buffer;
use crate::color::Color;
use crate::widget::{DrawContext, WaitContext, Widget};
use crate::cmd::Cmd;
use crate::doublemempool::DoubleMemPool;
#[derive(Debug)]
pub enum OutputMode {
Active,
All,
}
struct AppInner {
compositor: Option<Main<wl_compositor::WlCompositor>>,
surfaces: Vec<Main<wl_surface::WlSurface>>,
shell_surfaces: Vec<Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>>,
configured_surfaces: Arc<Mutex<usize>>,
outputs: Vec<(u32, Main<wl_output::WlOutput>)>,
shell: Option<Main<zwlr_layer_shell_v1::ZwlrLayerShellV1>>,
draw_tx: Sender<Cmd>,
output_mode: OutputMode,
visible: bool,
scale: u32,
}
impl AppInner {
fn new(tx: Sender<Cmd>, output_mode: OutputMode, scale: u32) -> AppInner {
AppInner {
compositor: None,
surfaces: Vec::new(),
shell_surfaces: Vec::new(),
configured_surfaces: Arc::new(Mutex::new(0)),
outputs: Vec::new(),
shell: None,
draw_tx: tx,
output_mode,
visible: true,
scale,
}
}
fn add_shell_surface(
compositor: &wl_compositor::WlCompositor,
shell: &zwlr_layer_shell_v1::ZwlrLayerShellV1,
scale: u32,
configured_surfaces: Arc<Mutex<usize>>,
tx: Sender<Cmd>,
output: Option<&wl_output::WlOutput>,
) -> (
Main<wl_surface::WlSurface>,
Main<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
) {
let surface = compositor.create_surface();
let this_is_stupid = AtomicBool::new(false);
let shell_surface = shell.get_layer_surface(
&surface,
output,
zwlr_layer_shell_v1::Layer::Overlay,
"".to_string(),
);
shell_surface.quick_assign(move |layer, event, _| match event {
zwlr_layer_surface_v1::Event::Configure { serial, .. } => {
if this_is_stupid
.compare_exchange(false, true, Ordering::SeqCst, Ordering::SeqCst)
.is_ok()
{
*(configured_surfaces.lock().unwrap()) += 1;
layer.ack_configure(serial);
tx.send(Cmd::ForceDraw).unwrap();
}
}
_ => unreachable!(),
});
shell_surface.set_keyboard_interactivity(1);
shell_surface.set_size(1, 1);
surface.set_buffer_scale(scale as i32);
surface.commit();
(surface, shell_surface)
}
fn outputs_changed(&mut self) {
let shell = match self.shell {
Some(ref shell) => shell.to_owned(),
None => return,
};
let compositor = match self.compositor {
Some(ref c) => c.to_owned(),
None => return,
};
for shell_surface in self.shell_surfaces.iter() {
shell_surface.destroy();
}
for surface in self.surfaces.iter() {
surface.destroy();
}
self.configured_surfaces = Arc::new(Mutex::new(0));
if !self.visible {
self.surfaces = Vec::new();
self.shell_surfaces = Vec::new();
return;
}
match self.output_mode {
OutputMode::Active => {
if !self.shell_surfaces.is_empty() {
return;
}
let (surface, shell_surface) = AppInner::add_shell_surface(
&compositor,
&shell,
self.scale,
self.configured_surfaces.clone(),
self.draw_tx.clone(),
None,
);
self.surfaces = vec![surface];
self.shell_surfaces = vec![shell_surface];
}
OutputMode::All => {
let mut surfaces = Vec::new();
let mut shell_surfaces = Vec::new();
for output in self.outputs.iter() {
let (surface, shell_surface) = AppInner::add_shell_surface(
&compositor,
&shell,
self.scale,
self.configured_surfaces.clone(),
self.draw_tx.clone(),
Some(&output.1),
);
surfaces.push(surface);
shell_surfaces.push(shell_surface);
}
self.surfaces = surfaces;
self.shell_surfaces = shell_surfaces;
}
}
self.draw_tx.send(Cmd::ForceDraw).unwrap();
}
fn add_output(&mut self, id: u32, output: Main<wl_output::WlOutput>) {
self.outputs.push((id, output));
self.outputs_changed();
}
fn remove_output(&mut self, id: u32) {
let old_output = self.outputs.iter().find(|(output_id, _)| *output_id == id);
if let Some(output) = old_output {
let new_outputs = self
.outputs
.iter()
.filter(|(output_id, _)| *output_id != id)
.map(|(x, y)| (*x, y.clone()))
.collect();
if output.1.as_ref().version() >= 3 {
output.1.release()
}
self.outputs = new_outputs;
self.outputs_changed();
}
}
fn set_compositor(&mut self, compositor: Option<Main<wl_compositor::WlCompositor>>) {
self.compositor = compositor
}
fn set_shell(&mut self, shell: Option<Main<zwlr_layer_shell_v1::ZwlrLayerShellV1>>) {
self.shell = shell
}
}
struct AppKeyboard {
current: Option<Cmd>,
delay: i32,
rate: i32,
next: Option<NaiveDateTime>,
}
pub struct App<'a> {
pools: DoubleMemPool,
display: Display,
event_queue: EventQueue,
cmd_queue: Arc<Mutex<VecDeque<Cmd>>>,
widget: Option<Box<dyn Widget + Send + 'a>>,
bg: Color,
inner: Arc<Mutex<AppInner>>,
last_damage: Option<Vec<(i32, i32, i32, i32)>>,
last_dim: (u32, u32),
keyboard: Arc<Mutex<AppKeyboard>>,
}
impl<'a> App<'a> {
pub fn redraw(&mut self, force: bool) -> Result<(), ::std::io::Error> {
let widget = match self.widget {
Some(ref mut widget) => widget,
None => return Ok(()),
};
let inner = self.inner.lock().unwrap();
let time = Local::now().naive_local();
if !inner.visible
|| inner.shell_surfaces.len() != *inner.configured_surfaces.lock().unwrap()
|| inner.surfaces.is_empty()
{
// Not ready yet
return Ok(());
}
let (last, pool) = match self.pools.pool() {
Some((last, pool)) => (last, pool),
None => {
self.pools.never_mind();
return Ok(());
}
};
let size = widget.size();
let size_changed = self.last_dim != size;
let force = force | size_changed;
// resize the pool if relevant
pool.resize((4 * size.0 * size.1) as usize)
.expect("Failed to resize the memory pool.");
let mmap = pool.mmap();
let mut buf = Buffer::new(mmap, size);
// Copy old damage
let force = match (force, &self.last_damage) {
(false, Some(d)) => {
let lastmmap = last.mmap();
let last = Buffer::new(lastmmap, size);
if cfg!(feature = "damage_debug") {
buf.memset(&Color::new(0.5, 0.75, 0.75, 1.0));
}
for d in d {
last.copy_to(&mut buf, d.clone());
}
false
}
_ => true,
};
if force {
buf.memset(&self.bg);
}
let report = widget.draw(
&mut DrawContext {
buf: &mut buf,
bg: &self.bg,
time,
force,
},
(0, 0),
size,
)?;
mmap.flush().unwrap();
if !force && !report.full_damage && report.damage.is_empty() {
// Nothing to do
self.pools.never_mind();
return Ok(());
}
// get a buffer and attach it
let new_buffer = pool.buffer(
0,
report.width as i32,
report.height as i32,
4 * size.0 as i32,
wl_shm::Format::Argb8888,
);
if size_changed {
for shell_surface in inner.shell_surfaces.iter() {
shell_surface.set_size(size.0 / inner.scale, size.1 / inner.scale);
}
}
for surface in inner.surfaces.iter() {
surface.attach(Some(&new_buffer), 0, 0);
if cfg!(feature = "damage_debug") || force || report.full_damage {
surface.damage_buffer(0, 0, size.0 as i32, size.1 as i32);
} else {
for d in report.damage.iter() {
surface.damage_buffer(d.0, d.1, d.2, d.3);
}
}
surface.commit();
}
self.last_damage = if force || report.full_damage {
Some(vec![(0, 0, size.0 as i32, size.1 as i32)])
} else {
Some(report.damage)
};
self.last_dim = size;
Ok(())
}
pub fn hide(&mut self) {
let mut inner = self.inner.lock().unwrap();
inner.visible = false;
self.last_dim = (0, 0);
inner.outputs_changed();
}
pub fn show(&mut self) {
let mut inner = self.inner.lock().unwrap();
inner.visible = true;
self.last_dim = (0, 0);
inner.outputs_changed();
}
pub fn cmd_queue(&self) -> Arc<Mutex<VecDeque<Cmd>>> {
self.cmd_queue.clone()
}
pub fn flush_display(&mut self) {
self.display.flush().expect("unable to flush display");
}
pub fn event_queue(&mut self) -> &mut EventQueue {
&mut self.event_queue
}
pub fn get_widget(&mut self) -> &mut Box<dyn Widget + Send + 'a> {
self.widget.as_mut().unwrap()
}
pub fn set_widget(&mut self, w: Box<dyn Widget + Send + 'a>) -> Result<(), ::std::io::Error> {
self.widget = Some(w);
self.redraw(true)
}
pub fn set_keyboard_repeat(&mut self, ctx: &mut WaitContext) {
let kbd = self.keyboard.lock().unwrap();
if let Some(t) = kbd.next {
ctx.set_time(t);
}
}
pub fn key_repeat(&mut self) -> Option<Cmd> {
let time = Local::now().naive_local();
let mut kbd = self.keyboard.lock().unwrap();
if let Some(target) = kbd.next {
if time >= target {
let cmd = kbd.current.as_ref().unwrap().clone();
kbd.next = Some(time + Duration::milliseconds(kbd.rate.into()));
return Some(cmd);
}
}
None
}
pub fn new(tx: Sender<Cmd>, output_mode: OutputMode, bg: Color, scale: u32) -> App<'a> {
let inner = Arc::new(Mutex::new(AppInner::new(tx, output_mode, scale)));
//
// Set up modules
//
let cmd_queue = Arc::new(Mutex::new(VecDeque::new()));
let display = Display::connect_to_env().unwrap();
let mut event_queue = display.create_event_queue();
let display_wrapper = (*display).clone().attach(event_queue.token());
//
// Set up global manager
//
let inner_global = inner.clone();
let manager =
GlobalManager::new_with_cb(&display_wrapper, move |event, registry, _| match event {
GlobalEvent::New {
id,
ref interface,
version,
} => {
if let "wl_output" = &interface[..] {
let output = registry.bind(std::cmp::min(version, 3), id);
inner_global.lock().unwrap().add_output(id, output);
}
}
GlobalEvent::Removed { id, ref interface } => {
if let "wl_output" = &interface[..] {
inner_global.lock().unwrap().remove_output(id);
}
}
});
// double sync to retrieve the global list
// and the globals metadata
event_queue.sync_roundtrip(&mut (), |_, _, _| {}).unwrap();
event_queue.sync_roundtrip(&mut (), |_, _, _| {}).unwrap();
// wl_compositor
let compositor = manager
.instantiate_range(1, 4)
.expect("server didn't advertise `wl_compositor`");
inner.lock().unwrap().set_compositor(Some(compositor));
// wl_shm
let shm: Main<wl_shm::WlShm> = manager
.instantiate_range(1, 1)
.expect("server didn't advertise `wl_shm`");
let pools = DoubleMemPool::new(shm).expect("Failed to create a memory pool !");
//
// Get our seat
//
let seat: Main<wl_seat::WlSeat> = manager.instantiate_range(1, 6).unwrap();
event_queue.sync_roundtrip(&mut (), |_, _, _| {}).unwrap();
//
// Keyboard processing
//
let kbd_clone = cmd_queue.clone();
let modifiers_state = Arc::new(Mutex::new(ModifiersState {
ctrl: false,
alt: false,
shift: false,
caps_lock: false,
logo: false,
num_lock: false,
}));
let keyboard = Arc::new(Mutex::new(AppKeyboard {
current: None,
delay: 0,
rate: 0,
next: None,
}));
let kb2 = keyboard.clone();
map_keyboard(&seat, None, move |event: KbEvent, _, _| match event {
KbEvent::Key {
keysym,
utf8,
state,
..
} => {
if let KeyState::Pressed = state {
match keysym {
keysyms::XKB_KEY_Escape => kbd_clone.lock().unwrap().push_back(Cmd::Exit),
keysyms::XKB_KEY_c if modifiers_state.lock().unwrap().ctrl => {
kbd_clone.lock().unwrap().push_back(Cmd::Exit)
}
v => {
let ev = Cmd::Keyboard {
key: v,
key_state: state,
modifiers_state: *modifiers_state.lock().unwrap(),
interpreted: utf8,
};
let mut kbd = kb2.lock().unwrap();
kbd.current = Some(ev.clone());
kbd.next = if kbd.delay > 0 {
Some(
Local::now().naive_local()
+ Duration::milliseconds(kbd.delay.into()),
)
} else {
None
};
drop(kbd);
kbd_clone.lock().unwrap().push_back(ev);
}
}
} else {
kb2.lock().unwrap().next = None;
}
}
KbEvent::Leave { .. } => {
kb2.lock().unwrap().next = None;
}
KbEvent::RepeatInfo { delay, rate } => {
let mut kbd = kb2.lock().unwrap();
kbd.delay = delay;
kbd.rate = rate;
}
KbEvent::Modifiers { modifiers } => *modifiers_state.lock().unwrap() = modifiers,
_ => (),
})
.expect("could not map keyboard");
//
// Prepare shell so that we can create our shell surface
//
inner
.lock()
.unwrap()
.set_shell(Some(if let Ok(layer) = manager.instantiate_exact(1) {
layer
} else {
panic!("server didn't advertise `zwlr_layer_shell_v1`");
}));
event_queue.sync_roundtrip(&mut (), |_, _, _| {}).unwrap();
//
// Cursor processing
//
let pointer_clone = cmd_queue.clone();
let pointer = seat.get_pointer();
let mut pos: (u32, u32) = (0, 0);
let mut vert_scroll: f64 = 0.0;
let mut horiz_scroll: f64 = 0.0;
let mut btn: u32 = 0;
let mut btn_clicked = false;
pointer.quick_assign(move |_, event, _| match event {
wl_pointer::Event::Enter {
surface_x,
surface_y,
..
} => {
pos = (surface_x as u32, surface_y as u32);
}
wl_pointer::Event::Leave { .. } => {
pos = (0, 0);
}
wl_pointer::Event::Motion {
surface_x,
surface_y,
..
} => {
pos = (surface_x as u32 * scale, surface_y as u32 * scale);
}
wl_pointer::Event::Axis { axis, value, .. } => {
if axis == wl_pointer::Axis::VerticalScroll {
vert_scroll += value;
}
}
wl_pointer::Event::Button { button, state, .. } => {
if let wl_pointer::ButtonState::Released = state {
btn = button;
btn_clicked = true;
}
}
wl_pointer::Event::Frame => {
if vert_scroll != 0.0 || horiz_scroll != 0.0 {
pointer_clone.lock().unwrap().push_back(Cmd::MouseScroll {
scroll: (horiz_scroll, vert_scroll),
pos,
});
vert_scroll = 0.0;
horiz_scroll = 0.0;
}
if btn_clicked {
pointer_clone
.lock()
.unwrap()
.push_back(Cmd::MouseClick { btn, pos });
btn_clicked = false;
}
}
_ => {}
});
display.flush().unwrap();
App {
display,
event_queue,
cmd_queue,
pools,
widget: None,
bg,
inner,
last_damage: None,
last_dim: (0, 0),
keyboard,
}
}
}
+222 -117
View File
@@ -1,22 +1,195 @@
use memmap::MmapMut; use std::{
fs::File,
io,
os::unix::io::{FromRawFd, RawFd},
time::SystemTime,
time::UNIX_EPOCH,
};
#[cfg(target_os = "linux")]
use nix::sys::memfd;
use nix::{
errno::Errno,
fcntl,
sys::{mman, stat},
unistd,
};
use memmap2::MmapMut;
use wayland_client::{
Dispatch, QueueHandle,
protocol::{wl_buffer, wl_shm, wl_shm_pool},
};
use crate::color::Color; use crate::color::Color;
use crate::widgets::Geometry;
pub struct Buffer<'a> { fn create_shm_fd() -> io::Result<RawFd> {
buf: &'a mut MmapMut, // Only try memfd on linux
#[cfg(target_os = "linux")]
loop {
match memfd::memfd_create(c"wldash", memfd::MemFdCreateFlag::MFD_CLOEXEC) {
Ok(fd) => return Ok(fd),
Err(Errno::EINTR) => continue,
Err(Errno::ENOSYS) => break,
Err(errno) => return Err(io::Error::from(errno)),
}
}
// Fallback to using shm_open
let sys_time = SystemTime::now();
let mut mem_file_handle = format!(
"/wldash-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
loop {
match mman::shm_open(
mem_file_handle.as_str(),
fcntl::OFlag::O_CREAT
| fcntl::OFlag::O_EXCL
| fcntl::OFlag::O_RDWR
| fcntl::OFlag::O_CLOEXEC,
stat::Mode::S_IRUSR | stat::Mode::S_IWUSR,
) {
Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) {
Ok(_) => return Ok(fd),
Err(errno) => match unistd::close(fd) {
Ok(_) => return Err(io::Error::from(errno)),
Err(errno) => return Err(io::Error::from(errno)),
},
},
Err(Errno::EEXIST) => {
// If a file with that handle exists then change the handle
mem_file_handle = format!(
"/wldash-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
continue;
}
Err(Errno::EINTR) => continue,
Err(errno) => return Err(io::Error::from(errno)),
}
}
}
pub struct ShmBuffer {
// Keeps the memfd alive for as long as the mapping and pool exist.
#[allow(dead_code)]
pub file: File,
pub mmap: MmapMut,
pub pool: wl_shm_pool::WlShmPool,
pub buffer: wl_buffer::WlBuffer,
pub refcnt: u32,
pub last_damage: Vec<Geometry>,
}
impl ShmBuffer {
fn new<D>(
shm: &wl_shm::WlShm,
qh: &QueueHandle<D>,
width: i32,
height: i32,
stride: i32,
format: wl_shm::Format,
) -> io::Result<ShmBuffer>
where
D: Dispatch<wl_shm_pool::WlShmPool, ()> + Dispatch<wl_buffer::WlBuffer, ()> + 'static,
{
let mem_fd = create_shm_fd()?;
let file = unsafe { File::from_raw_fd(mem_fd) };
file.set_len((height * stride) as u64)?;
let pool = shm.create_pool(mem_fd, height * stride, qh, ());
let mmap = unsafe { MmapMut::map_mut(&file)? };
let buffer = pool.create_buffer(0, width, height, stride, format, qh, ());
Ok(ShmBuffer {
refcnt: 0,
file,
mmap,
pool,
buffer,
last_damage: Vec::new(),
})
}
pub fn acquire(&mut self) {
self.refcnt += 1;
}
pub fn release(&mut self) {
// A compositor may release a buffer we never considered acquired
// (e.g. after a resize); never underflow and wedge the buffer.
self.refcnt = self.refcnt.saturating_sub(1);
}
}
pub struct BufferManager {
pub buffers: Vec<ShmBuffer>,
}
impl BufferManager {
pub fn new() -> BufferManager {
BufferManager {
buffers: Vec::new(),
}
}
pub fn clear_buffers(&mut self) {
for buf in self.buffers.iter_mut() {
buf.buffer.destroy();
buf.pool.destroy();
}
self.buffers.clear()
}
pub fn next_buffer(&mut self) -> Option<&mut ShmBuffer> {
self.buffers.iter_mut().find(|buf| buf.refcnt == 0)
}
pub fn add_buffer<D>(
&mut self,
wl_shm: &wl_shm::WlShm,
dimensions: (i32, i32),
qh: &QueueHandle<D>,
) where
D: Dispatch<wl_shm_pool::WlShmPool, ()> + Dispatch<wl_buffer::WlBuffer, ()> + 'static,
{
let buf = ShmBuffer::new(
wl_shm,
qh,
dimensions.0,
dimensions.1,
dimensions.0 * 4,
wl_shm::Format::Argb8888,
)
.expect("unable to add buffer");
self.buffers.push(buf);
}
}
pub struct BufferView<'a> {
view: &'a mut [u32],
dimensions: (u32, u32), dimensions: (u32, u32),
subdimensions: Option<(u32, u32, u32, u32)>, subdimensions: Option<(u32, u32, u32, u32)>,
} }
impl<'a> Buffer<'a> { impl<'a> BufferView<'a> {
pub fn new(buf: &'a mut MmapMut, dimensions: (u32, u32)) -> Buffer { pub fn new(buf: &'a mut MmapMut, dimensions: (u32, u32)) -> BufferView<'a> {
Buffer { let view = unsafe {
buf, std::slice::from_raw_parts_mut(
buf.as_mut_ptr() as *mut u32,
(dimensions.0 * dimensions.1) as usize,
)
};
BufferView {
view,
dimensions, dimensions,
subdimensions: None, subdimensions: None,
} }
} }
#[inline]
pub fn get_bounds(&self) -> (u32, u32, u32, u32) { pub fn get_bounds(&self) -> (u32, u32, u32, u32) {
if let Some(subdim) = self.subdimensions { if let Some(subdim) = self.subdimensions {
subdim subdim
@@ -25,68 +198,20 @@ impl<'a> Buffer<'a> {
} }
} }
pub fn get_signed_bounds(&self) -> (i32, i32, i32, i32) { pub fn subdimensions(&mut self, subdimensions: (u32, u32, u32, u32)) -> BufferView<'_> {
if let Some(subdim) = self.subdimensions {
(
subdim.0 as i32,
subdim.1 as i32,
subdim.2 as i32,
subdim.3 as i32,
)
} else {
(0, 0, self.dimensions.0 as i32, self.dimensions.1 as i32)
}
}
pub fn copy_to(&self, other: &mut Buffer, (x, y, width, height): (i32, i32, i32, i32)) {
debug_assert!(self.dimensions == other.dimensions);
debug_assert!(self.subdimensions.is_none() && other.subdimensions.is_none());
if x == 0 && width as u32 == self.dimensions.0 {
// Full-width copy
let offset = y as isize * self.dimensions.0 as isize;
let n = height as usize * self.dimensions.0 as usize;
unsafe {
std::ptr::copy(
(self.buf.as_ptr() as *const u32).offset(offset),
(other.buf.as_mut_ptr() as *mut u32).offset(offset),
n,
);
}
} else {
// Row-by-row copy
for cur_y in y as isize..(y + height) as isize {
let offset = x as isize + cur_y as isize * self.dimensions.0 as isize;
unsafe {
std::ptr::copy(
(self.buf.as_ptr() as *const u32).offset(offset),
(other.buf.as_mut_ptr() as *mut u32).offset(offset),
width as usize,
);
}
}
}
}
pub fn subdimensions(
&mut self,
subdimensions: (u32, u32, u32, u32),
) -> Result<Buffer, ::std::io::Error> {
let bounds = self.get_bounds(); let bounds = self.get_bounds();
if subdimensions.0 + subdimensions.2 >= bounds.2 if cfg!(debug_assertions)
|| subdimensions.1 + subdimensions.3 >= bounds.3 && (subdimensions.0 + subdimensions.2 > bounds.2
|| subdimensions.1 + subdimensions.3 > bounds.3)
{ {
return Err(::std::io::Error::new( panic!(
::std::io::ErrorKind::Other,
format!(
"cannot create subdimensions larger than buffer: {:?} > {:?}", "cannot create subdimensions larger than buffer: {:?} > {:?}",
subdimensions, bounds subdimensions, bounds
), );
));
} }
Ok(Buffer { BufferView {
buf: self.buf, view: self.view,
dimensions: self.dimensions, dimensions: self.dimensions,
subdimensions: Some(( subdimensions: Some((
subdimensions.0 + bounds.0, subdimensions.0 + bounds.0,
@@ -94,23 +219,24 @@ impl<'a> Buffer<'a> {
subdimensions.2, subdimensions.2,
subdimensions.3, subdimensions.3,
)), )),
}) }
} }
pub fn offset(&mut self, offset: (u32, u32)) -> Result<Buffer, ::std::io::Error> { pub fn subgeometry(&mut self, geo: Geometry) -> BufferView<'_> {
self.subdimensions((geo.x, geo.y, geo.width, geo.height))
}
pub fn offset(&mut self, offset: (u32, u32)) -> BufferView<'_> {
let bounds = self.get_bounds(); let bounds = self.get_bounds();
if offset.0 > bounds.2 || offset.1 > bounds.3 { if cfg!(debug_assertions) && (offset.0 > bounds.2 || offset.1 > bounds.3) {
return Err(::std::io::Error::new( panic!(
::std::io::ErrorKind::Other,
format!(
"cannot create offset outside buffer: {:?} > {:?}", "cannot create offset outside buffer: {:?} > {:?}",
offset, bounds offset, bounds
), );
));
} }
Ok(Buffer { BufferView {
buf: self.buf, view: self.view,
dimensions: self.dimensions, dimensions: self.dimensions,
subdimensions: Some(( subdimensions: Some((
offset.0 + bounds.0, offset.0 + bounds.0,
@@ -118,63 +244,42 @@ impl<'a> Buffer<'a> {
bounds.2 - offset.0, bounds.2 - offset.0,
bounds.3 - offset.1, bounds.3 - offset.1,
)), )),
}) }
} }
pub fn memset(&mut self, c: &Color) { pub fn memset(&mut self, c: Color) {
if let Some(subdim) = self.subdimensions { if let Some(subdim) = self.subdimensions {
unsafe {
let ptr = self.buf.as_mut_ptr();
for y in subdim.1..(subdim.1 + subdim.3) { for y in subdim.1..(subdim.1 + subdim.3) {
for x in subdim.0..(subdim.0 + subdim.2) { let start = (subdim.0 + y * self.dimensions.0) as usize;
*((ptr as *mut u32).offset((x + y * self.dimensions.0) as isize)) = let end = start + subdim.2 as usize;
c.as_argb8888(); self.view[start..end].fill(c.0);
}
}
} }
} else { } else {
unsafe { self.view.fill(c.0);
let ptr = self.buf.as_mut_ptr();
for p in 0..(self.dimensions.0 * self.dimensions.1) {
*((ptr as *mut u32).offset(p as isize)) = c.as_argb8888();
}
}
} }
} }
pub fn put(&mut self, pos: (u32, u32), c: &Color) -> Result<(), ::std::io::Error> { pub fn clear(&mut self) {
let true_pos = if let Some(subdim) = self.subdimensions { if let Some(subdim) = self.subdimensions {
if pos.0 >= subdim.2 || pos.1 >= subdim.3 { for y in subdim.1..(subdim.1 + subdim.3) {
return Err(::std::io::Error::new( let start = (subdim.0 + y * self.dimensions.0) as usize;
::std::io::ErrorKind::Other, let end = start + subdim.2 as usize;
format!( self.view[start..end].fill(0x0);
"put({:?}) is not within subdimensions of buffer ({:?})",
pos, subdim
),
));
} }
(pos.0 + subdim.0, pos.1 + subdim.1)
} else { } else {
if pos.0 >= self.dimensions.0 || pos.1 >= self.dimensions.1 { self.view.fill(0x0);
return Err(::std::io::Error::new( }
::std::io::ErrorKind::Other,
format!(
"put({:?}) is not within dimensions of buffer ({:?})",
pos, self.dimensions
),
));
} }
pos
};
unsafe { #[inline]
let ptr = self pub fn put_raw(&mut self, pos: (u32, u32), c: Color) {
.buf self.view[(pos.0 + pos.1 * self.dimensions.0) as usize] = c.0
.as_mut_ptr() }
.offset(4 * (true_pos.0 + (true_pos.1 * self.dimensions.0)) as isize);
*(ptr as *mut u32) = c.as_argb8888();
};
Ok(()) #[inline]
pub fn put_line_raw(&mut self, pos: (u32, u32), len: u32, c: Color) {
let start = (pos.0 + (pos.1 * self.dimensions.0)) as usize;
let len = len as usize;
self.view[start..start + len].fill(c.0);
} }
} }
-24
View File
@@ -1,24 +0,0 @@
use crate::keyboard::{KeyState, ModifiersState};
#[derive(Clone)]
pub enum Cmd {
Exit,
Draw,
ForceDraw,
ToggleVisible,
MouseClick {
btn: u32,
pos: (u32, u32),
},
MouseScroll {
scroll: (f64, f64),
pos: (u32, u32),
},
KeyboardTest,
Keyboard {
key: u32,
key_state: KeyState,
modifiers_state: ModifiersState,
interpreted: Option<String>,
},
}
+32 -61
View File
@@ -1,71 +1,42 @@
use serde::{Deserialize, Serialize}; #[derive(Copy, Clone)]
pub struct Color(pub u32);
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
pub struct Color {
red: f32,
green: f32,
blue: f32,
opacity: f32,
}
impl Color { impl Color {
pub fn new(red: f32, green: f32, blue: f32, opacity: f32) -> Color { pub const WHITE: Color = Color(0xFFFFFFFF);
let (red, green, blue, opacity) = clamp(red, green, blue, opacity); pub const GREY35: Color = Color(0xFF595959);
Color { pub const GREY50: Color = Color(0xFF7F7F7F);
red, pub const GREY75: Color = Color(0xFFBFBFBF);
green, pub const GREY80: Color = Color(0xFFCCCCCC);
blue, pub const RED: Color = Color(0xFFFF0000);
opacity, pub const YELLOW: Color = Color(0xFFFFFF00);
} pub const LIGHTGREEN: Color = Color(0xFF7FFF7F);
pub const LIGHTORANGE: Color = Color(0xFFFFBF00);
pub const DARKORANGE: Color = Color(0xFFFF7F00);
pub const BUFF: Color = Color(0xFFFFBF7F);
#[allow(unused)]
pub fn new(red: u8, green: u8, blue: u8, opacity: u8) -> Color {
Color((opacity as u32) << 24 | (red as u32) << 16 | (green as u32) << 8 | (blue as u32))
} }
pub fn blend(&self, other: &Color, ratio: f32) -> Color { fn red(self) -> u8 {
let ratio = clamp_f32(ratio, 0.0, 1.0); ((self.0 >> 16) & 0xFF) as u8
Color {
red: blend_f32(self.red, other.red, ratio),
green: blend_f32(self.green, other.green, ratio),
blue: blend_f32(self.blue, other.blue, ratio),
opacity: blend_f32(self.opacity, other.opacity, ratio),
} }
fn green(self) -> u8 {
((self.0 >> 8) & 0xFF) as u8
}
fn blue(self) -> u8 {
(self.0 & 0xFF) as u8
} }
#[inline] pub fn alpha(self, alpha: f32) -> Color {
pub fn as_argb8888(&self) -> u32 { unsafe {
((255.0 * self.opacity) as u32 & 0xFF) << 24 Color(
| ((255.0 * self.red) as u32 & 0xFF) << 16 (255. * alpha).to_int_unchecked::<u32>() << 24
| ((255.0 * self.green) as u32 & 0xFF) << 8 | (self.red() as f32 * alpha).to_int_unchecked::<u32>() << 16
| ((255.0 * self.blue) as u32 & 0xFF) | (self.green() as f32 * alpha).to_int_unchecked::<u32>() << 8
} | (self.blue() as f32 * alpha).to_int_unchecked::<u32>(),
}
#[inline]
fn clamp(r: f32, g: f32, b: f32, o: f32) -> (f32, f32, f32, f32) {
clamp_naive(r, g, b, o)
}
#[inline]
fn clamp_naive(r: f32, g: f32, b: f32, o: f32) -> (f32, f32, f32, f32) {
(
clamp_f32(r, 0.0, 1.0),
clamp_f32(g, 0.0, 1.0),
clamp_f32(b, 0.0, 1.0),
clamp_f32(o, 0.0, 1.0),
) )
} }
#[inline]
fn clamp_f32(v: f32, a: f32, b: f32) -> f32 {
if v > b {
b
} else if v < a {
a
} else {
v
} }
} }
#[inline]
fn blend_f32(a: f32, b: f32, r: f32) -> f32 {
a + ((b - a) * r)
}
+303 -272
View File
@@ -1,29 +1,39 @@
use crate::cmd::Cmd; use serde::{self, Deserialize, Serialize};
use crate::color::Color;
use crate::widget; use std::collections::HashMap;
use crate::{
fonts::{FontMap, FontRef},
widgets,
};
use chrono::NaiveDateTime;
use serde::{Deserialize, Serialize};
use std::default::Default; use std::default::Default;
use std::{collections::HashMap, sync::mpsc::Sender}; use std::sync::{Arc, Mutex};
use crate::{
event::Events,
fonts::FontMap,
widgets::{
Backlight, Battery, Calendar, Clock, Date, HorizontalLayout, IndexedLayout, Interface,
InvertedHorizontalLayout, InvertedVerticalLayout, Layout, Line, Margin, PulseAudio,
VerticalLayout, Widget as RealWidget,
},
};
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "snake_case")]
pub enum OperationMode {
LayerSurface((u32, u32)),
XdgToplevel,
}
#[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "snake_case")]
pub enum Widget { pub enum Widget {
Margin { Margin {
margins: (u32, u32, u32, u32), margins: (u32, u32, u32, u32),
widget: Box<Widget>, widget: Box<Widget>,
}, },
Fixed {
width: u32,
height: u32,
widget: Box<Widget>,
},
HorizontalLayout(Vec<Widget>), HorizontalLayout(Vec<Widget>),
InvertedHorizontalLayout(Vec<Widget>),
VerticalLayout(Vec<Widget>), VerticalLayout(Vec<Widget>),
InvertedVerticalLayout(Vec<Widget>),
HorizontalLine(u32),
VerticalLine(u32),
Clock { Clock {
font: Option<String>, font: Option<String>,
font_size: f32, font_size: f32,
@@ -36,315 +46,336 @@ pub enum Widget {
font_primary: Option<String>, font_primary: Option<String>,
font_secondary: Option<String>, font_secondary: Option<String>,
font_size: f32, font_size: f32,
sections: u32, sections_x: i32,
sections_y: i32,
}, },
Launcher { Launcher {
font: Option<String>, font: Option<String>,
font_size: f32, font_size: f32,
length: u32, launch_cmd: Option<String>,
#[serde(default)]
app_opener: String,
#[serde(default)]
term_opener: String,
#[serde(default)]
url_opener: String,
}, },
Battery { Battery {
font: Option<String>, font: Option<String>,
font_size: f32, font_size: f32,
length: u32,
}, },
Backlight { Backlight {
#[serde(default)] device: Option<String>,
device: String,
font: Option<String>, font: Option<String>,
font_size: f32, font_size: f32,
length: u32,
}, },
#[cfg(feature = "pulseaudio-widget")]
PulseAudio { PulseAudio {
font: Option<String>, font: Option<String>,
font_size: f32, font_size: f32,
length: u32,
},
#[cfg(feature = "alsa-widget")]
AlsaSound {
font: Option<String>,
font_size: f32,
length: u32,
}, },
} }
impl Widget { impl Default for OperationMode {
pub fn construct<'a>( fn default() -> OperationMode {
self, OperationMode::LayerSurface((1024, 768))
time: NaiveDateTime,
tx: Sender<Cmd>,
fonts: &'a FontMap,
) -> Option<Box<dyn widget::Widget + Send + 'a>> {
match self {
Widget::Margin { margins, widget } => match widget.construct(time, tx, fonts) {
Some(w) => Some(widget::Margin::new(margins, w)),
None => None,
},
Widget::Fixed {
width,
height,
widget,
} => match widget.construct(time, tx, fonts) {
Some(w) => Some(widget::Fixed::new((width, height), w)),
None => None,
},
Widget::HorizontalLayout(widgets) => Some(widget::HorizontalLayout::new(
widgets
.into_iter()
.map(|x| x.construct(time, tx.clone(), fonts))
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.collect(),
)),
Widget::VerticalLayout(widgets) => Some(widget::VerticalLayout::new(
widgets
.into_iter()
.map(|x| x.construct(time, tx.clone(), fonts))
.filter(|x| x.is_some())
.map(|x| x.unwrap())
.collect(),
)),
Widget::Clock { font, font_size } => match widgets::clock::Clock::new(
time,
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
) {
Ok(w) => Some(w),
Err(_) => None,
},
Widget::Date { font, font_size } => match widgets::date::Date::new(
time,
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
) {
Ok(w) => Some(w),
Err(_) => None,
},
Widget::Calendar {
font_primary,
font_secondary,
font_size,
sections,
} => Some(widgets::calendar::Calendar::new(
time,
get_font(
&font_primary.or_else(|| Some("sans".to_string())).unwrap(),
&fonts,
),
get_font(
&font_secondary.or_else(|| Some("mono".to_string())).unwrap(),
&fonts,
),
font_size,
sections,
)),
Widget::Launcher {
font,
font_size,
length,
app_opener,
term_opener,
url_opener,
} => Some(widgets::launcher::Launcher::new(
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
length,
tx,
app_opener,
term_opener,
if url_opener.is_empty() {
"xdg_open ".to_string()
} else {
url_opener
},
)),
Widget::Battery {
font,
font_size,
length,
} => {
match widgets::battery::UpowerBattery::new(
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
length,
tx,
) {
Ok(w) => Some(w),
Err(_) => None,
}
}
Widget::Backlight {
device,
font,
font_size,
length,
} => {
let d = if device == "" {
"intel_backlight"
} else {
&device
};
match widgets::backlight::Backlight::new(
d,
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
length,
) {
Ok(w) => Some(w),
Err(_) => None,
}
}
#[cfg(feature = "pulseaudio-widget")]
Widget::PulseAudio {
font,
font_size,
length,
} => {
match widgets::audio::PulseAudio::new(
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
length,
tx,
) {
Ok(w) => Some(w),
Err(_) => None,
}
}
#[cfg(feature = "alsa-widget")]
Widget::AlsaSound {
font,
font_size,
length,
} => {
match widgets::audio::Alsa::new(
get_font(&font.or_else(|| Some("sans".to_string())).unwrap(), &fonts),
font_size,
length,
) {
Ok(w) => Some(w),
Err(_) => None,
}
}
}
} }
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "snake_case")]
pub enum OutputMode {
All,
Active,
}
impl Default for OutputMode {
fn default() -> Self {
OutputMode::Active
}
}
fn default_fonts() -> HashMap<String, String> {
let mut map = HashMap::with_capacity(2);
map.insert("mono".to_string(), "mono".to_string());
map.insert("sans".to_string(), "sans".to_string());
map
}
#[derive(Serialize, Deserialize, Debug)]
#[serde(rename_all = "camelCase")]
pub struct Config { pub struct Config {
pub output_mode: OutputMode, pub font_paths: Option<HashMap<String, String>>,
pub scale: u32,
pub background: Color,
pub widget: Widget, pub widget: Widget,
pub mode: OperationMode,
#[serde(default = "default_fonts")] pub background: Option<u32>,
pub fonts: HashMap<String, String>, pub server: Option<bool>,
} }
impl Default for Config { impl Config {
fn default() -> Self { pub fn generate_v1() -> Config {
Config { let has_battery = Battery::detect();
widget: Widget::Margin { let has_backlight = Backlight::detect();
margins: (20, 20, 20, 20),
widget: Box::new(Widget::VerticalLayout(vec![ let mut bars = Vec::new();
Widget::HorizontalLayout(vec![ if has_battery {
Widget::Margin { bars.push(Widget::Margin {
margins: (0, 88, 0, 32),
widget: Box::new(Widget::VerticalLayout(vec![
Widget::Date {
font: None,
font_size: 64.0,
},
Widget::Clock {
font: None,
font_size: 256.0,
},
])),
},
Widget::VerticalLayout(vec![
Widget::Margin {
margins: (0, 0, 0, 8), margins: (0, 0, 0, 8),
widget: Box::new(Widget::Battery { widget: Box::new(Widget::Battery {
font: None, font: None,
font_size: 24.0, font_size: 24.,
length: 0,
}), }),
}, });
Widget::Margin { }
if has_backlight {
bars.push(Widget::Margin {
margins: (0, 0, 0, 8), margins: (0, 0, 0, 8),
widget: Box::new(Widget::Backlight { widget: Box::new(Widget::Backlight {
device: "intel_backlight".to_string(), device: None,
font: None, font: None,
font_size: 24.0, font_size: 24.,
length: 0,
}), }),
}, });
#[cfg(feature = "pulseaudio-widget")] }
Widget::Margin { bars.push(Widget::Margin {
margins: (0, 0, 0, 8), margins: (0, 0, 0, 8),
widget: Box::new(Widget::PulseAudio { widget: Box::new(Widget::PulseAudio {
font: None, font: None,
font_size: 24.0, font_size: 24.,
length: 0,
}), }),
});
let widget = Widget::Margin {
margins: (20, 20, 20, 20),
widget: Box::new(Widget::VerticalLayout(vec![
Widget::HorizontalLayout(vec![
Widget::VerticalLayout(vec![
Widget::Date {
font: None,
font_size: 48.,
},
Widget::Clock {
font: None,
font_size: 256.,
}, },
]), ]),
Widget::Margin {
margins: (88, 0, 0, 0),
widget: Box::new(Widget::VerticalLayout(bars)),
},
]), ]),
Widget::Calendar { Widget::Calendar {
font_primary: None, font_primary: None,
font_secondary: None, font_secondary: None,
font_size: 16.0, font_size: 36.,
sections: 3, sections_x: 3,
sections_y: 1,
}, },
Widget::Launcher { Widget::Launcher {
font: None, font: None,
font_size: 32.0, font_size: 32.,
length: 0, launch_cmd: None,
app_opener: "".to_string(),
term_opener: "".to_string(),
url_opener: "".to_string(),
}, },
])), ])),
};
Config {
// Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget,
mode: OperationMode::LayerSurface((1024, 768)),
background: Some(0xFAFAFAFA),
server: None,
}
}
pub fn generate_v2(clay: bool) -> Config {
let has_battery = Battery::detect();
let has_backlight = Backlight::detect();
let mut bars = Vec::new();
if has_battery {
bars.push(Widget::Margin {
margins: (16, 8, 8, 0),
widget: Box::new(Widget::Battery {
font: None,
font_size: 24.,
}),
});
}
if has_backlight {
bars.push(Widget::Margin {
margins: (16, 8, 8, 0),
widget: Box::new(Widget::Backlight {
device: None,
font: None,
font_size: 24.,
}),
});
}
bars.push(Widget::Margin {
margins: (16, 8, 8, 0),
widget: Box::new(Widget::PulseAudio {
font: None,
font_size: 24.,
}),
});
let widget = Widget::VerticalLayout(vec![
Widget::HorizontalLayout(vec![
Widget::Margin {
margins: (8, 0, 0, 0),
widget: Box::new(Widget::Clock {
font: None,
font_size: 128.,
}),
}, },
output_mode: Default::default(), Widget::Margin {
scale: 1, margins: (16, 64, 0, 0),
background: Color::new(0.0, 0.0, 0.0, 0.9), widget: Box::new(Widget::Date {
fonts: default_fonts(), font: None,
font_size: 48.,
}),
},
Widget::VerticalLayout(bars),
]),
Widget::HorizontalLine(1),
Widget::InvertedHorizontalLayout(vec![
Widget::Margin {
margins: (16, 8, 16, 0),
widget: Box::new(Widget::Calendar {
font_primary: None,
font_secondary: None,
font_size: 24.,
sections_x: 1,
sections_y: if clay { -1 } else { 3 },
}),
},
Widget::VerticalLine(1),
Widget::Margin {
margins: (8, 0, 0, 0),
widget: Box::new(Widget::Launcher {
font: None,
font_size: 32.,
launch_cmd: None,
}),
},
]),
]);
Config {
// Left unset on purpose: "sans"/"monospace" are resolved through
// fontconfig on each machine at start. Pinning absolute paths
// here broke configs shared between hosts with different font
// package layouts. Set font_paths yourself to override.
font_paths: None,
widget,
mode: if clay {
OperationMode::XdgToplevel
} else {
OperationMode::LayerSurface((1024, 1))
},
background: Some(0xFAFAFAFA),
server: None,
} }
} }
} }
#[inline] fn leak_or_default(v: Option<String>, d: &'static str) -> &'static str {
fn get_font<'a>(name: &str, map: &'a FontMap) -> FontRef<'a> { match v {
match map.get(name) { Some(v) => Box::leak(v.into_boxed_str()),
Some(f) => f, None => d,
None => panic!("Font {} is missing from the config", name), }
}
fn leak_or_none(v: Option<String>) -> Option<&'static str> {
match v {
Some(v) => Some(Box::leak(v.into_boxed_str())),
None => None,
}
}
impl Widget {
pub fn construct_widgets(
self,
v: &mut Vec<Box<dyn RealWidget>>,
fm: &mut FontMap,
events: &Arc<Mutex<Events>>,
) {
match self {
Widget::Margin { widget, .. } => widget.construct_widgets(v, fm, events),
Widget::HorizontalLayout(widgets) => widgets
.into_iter()
.for_each(|w| w.construct_widgets(v, fm, events)),
Widget::InvertedHorizontalLayout(widgets) => widgets
.into_iter()
.for_each(|w| w.construct_widgets(v, fm, events)),
Widget::VerticalLayout(widgets) => widgets
.into_iter()
.for_each(|w| w.construct_widgets(v, fm, events)),
Widget::InvertedVerticalLayout(widgets) => widgets
.into_iter()
.for_each(|w| w.construct_widgets(v, fm, events)),
Widget::HorizontalLine(width) => v.push(Box::new(Line::new(width, false))),
Widget::VerticalLine(width) => v.push(Box::new(Line::new(width, true))),
Widget::Clock { font, font_size } => v.push(Box::new(Clock::new(
fm,
leak_or_default(font, "sans"),
font_size,
))),
Widget::Date { font, font_size } => v.push(Box::new(Date::new(
fm,
leak_or_default(font, "sans"),
font_size,
))),
Widget::Calendar {
font_primary,
font_secondary,
font_size,
sections_x,
sections_y,
} => v.push(Box::new(Calendar::new(
fm,
leak_or_default(font_primary, "monospace"),
leak_or_default(font_secondary, "sans"),
font_size,
sections_x,
sections_y,
))),
Widget::Launcher {
font,
font_size,
launch_cmd,
} => v.push(Box::new(Interface::new(
events.clone(),
fm,
leak_or_default(font, "sans"),
font_size,
launch_cmd,
))),
Widget::Battery { font, font_size } => v.push(Box::new(Battery::new(
events.clone(),
fm,
leak_or_default(font, "sans"),
font_size,
))),
Widget::Backlight {
device,
font,
font_size,
} => v.push(Box::new(Backlight::new(
leak_or_none(device),
fm,
leak_or_default(font, "sans"),
font_size,
))),
Widget::PulseAudio { font, font_size } => v.push(Box::new(PulseAudio::new(
events.clone(),
fm,
Box::leak(font.unwrap_or_else(|| "sans".to_string()).into_boxed_str()),
font_size,
))),
}
}
pub fn construct_layout(&self, idx: &mut usize) -> Box<dyn Layout> {
match self {
Widget::Margin { margins, widget } => {
Margin::new(widget.construct_layout(idx), *margins)
}
Widget::HorizontalLayout(widgets) => {
HorizontalLayout::new(widgets.iter().map(|w| w.construct_layout(idx)).collect())
}
Widget::InvertedHorizontalLayout(widgets) => InvertedHorizontalLayout::new(
widgets.iter().map(|w| w.construct_layout(idx)).collect(),
),
Widget::VerticalLayout(widgets) => {
VerticalLayout::new(widgets.iter().map(|w| w.construct_layout(idx)).collect())
}
Widget::InvertedVerticalLayout(widgets) => InvertedVerticalLayout::new(
widgets.iter().map(|w| w.construct_layout(idx)).collect(),
),
_ => IndexedLayout::new({
let i = *idx;
*idx += 1;
i
}),
}
} }
} }
-61
View File
@@ -1,61 +0,0 @@
use serde::{de::DeserializeOwned, Serialize};
use std::io::BufRead;
pub enum ConfigFmt {
#[cfg(feature = "yaml-cfg")]
Yaml,
#[cfg(feature = "json-cfg")]
Json,
}
pub const CONFIG_NAMES: &[&str] = &[
#[cfg(feature = "yaml-cfg")]
"config.yaml",
#[cfg(feature = "json-cfg")]
"config.json",
];
impl ConfigFmt {
#[inline]
pub fn new(ext: &str) -> Option<Self> {
match ext {
#[cfg(feature = "yaml-cfg")]
"yaml" => Some(ConfigFmt::Yaml),
#[cfg(feature = "json-cfg")]
"json" => Some(ConfigFmt::Json),
_ => None,
}
}
#[inline]
pub fn from_reader<B: BufRead, T: DeserializeOwned>(&self, r: B) -> T {
match self {
#[cfg(feature = "yaml-cfg")]
ConfigFmt::Yaml => serde_yaml::from_reader(r).unwrap(),
#[cfg(feature = "json-cfg")]
ConfigFmt::Json => serde_json::from_reader(r).unwrap(),
}
}
#[inline]
pub fn to_string<T: Serialize + ?Sized>(&self, src: &T) -> String {
match self {
#[cfg(feature = "yaml-cfg")]
ConfigFmt::Yaml => serde_yaml::to_string(&src).unwrap(),
#[cfg(feature = "json-cfg")]
ConfigFmt::Json => serde_json::to_string_pretty(&src).unwrap(),
}
}
}
impl Default for ConfigFmt {
fn default() -> Self {
// attributes require curly braces to work
#[cfg(feature = "yaml-cfg")]
{
ConfigFmt::Yaml
}
#[cfg(all(feature = "json-cfg", not(feature = "yaml-cfg")))]
{
ConfigFmt::Json
}
}
}
-57
View File
@@ -1,57 +0,0 @@
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::error::Error;
use std::fs::{create_dir_all, File, OpenOptions};
use std::path::PathBuf;
#[derive(Debug, PartialEq, Serialize, Deserialize, Clone, Default)]
pub struct Data {
pub entries: HashMap<String, i64>,
}
impl Data {
pub fn load() -> Result<Data, Box<dyn Error>> {
Ok(serde_yaml::from_reader(Self::read_file()?)?)
}
pub fn save(&self) -> Result<(), Box<dyn Error>> {
Ok(serde_yaml::to_writer(Self::write_file()?, self)?)
}
fn read_file() -> Result<File, Box<dyn Error>> {
Ok(OpenOptions::new()
.create(true)
.write(true)
.read(true)
.open(Self::path()?)?)
}
fn write_file() -> Result<File, Box<dyn Error>> {
Ok(OpenOptions::new()
.create(true)
.write(true)
.truncate(true)
.open(Self::path()?)?)
}
fn path() -> Result<PathBuf, Box<dyn Error>> {
let xdg_cache = match env::var_os("XDG_CACHE_HOME") {
Some(s) => s
.into_string()
.map_err(|_| "Unable to resolve $XDG_CACHE_HOME")?,
None => format!(
"{}/.cache",
env::var_os("HOME")
.ok_or("Unable to resolve $HOME")?
.into_string()
.map_err(|_| "Unable to resolve $HOME")?
),
};
let cache_dir = PathBuf::from(xdg_cache).join("wldash");
create_dir_all(&cache_dir)?;
Ok(cache_dir.join("data.yaml"))
}
}
-113
View File
@@ -1,113 +0,0 @@
extern crate ini;
use ini::{Ini, ParseOption};
use itertools::Itertools;
use std::cmp::Ordering;
use std::env;
use std::error::Error;
use std::io::Error as io_error;
use std::io::ErrorKind;
use walkdir::WalkDir;
#[derive(Clone, Debug, Eq, Hash)]
pub struct Desktop {
pub entry_type: String,
pub name: String,
pub no_display: bool,
pub hidden: bool,
pub exec: Option<String>,
pub url: Option<String>,
pub term: bool,
pub keywords: Vec<String>,
}
impl Desktop {
fn parse(f: &str) -> Result<Desktop, Box<dyn Error>> {
let file = Ini::load_from_file_opt(
f,
ParseOption {
enabled_quote: false,
enabled_escape: false,
},
)?;
match file.section(Some("Desktop Entry")) {
Some(desktop) => Ok(Desktop {
entry_type: desktop.get("Type").unwrap_or(&"".to_string()).to_string(),
name: desktop.get("Name").unwrap_or(&"".to_string()).to_string(),
term: desktop.get("Terminal").unwrap_or(&"".to_string()) == "true",
no_display: desktop.get("NoDisplay").unwrap_or(&"".to_string()) == "true",
hidden: desktop.get("Hidden").unwrap_or(&"".to_string()) == "true",
exec: desktop.get("Exec").map(|x| x.to_string()),
url: desktop.get("URL").map(|x| x.to_string()),
keywords: desktop
.get("Keywords")
.map(|x| {
x.split(';')
.map(|y| y.trim().to_string())
.filter(|z| z != "")
.collect()
})
.unwrap_or_else(|| vec![]),
}),
None => Err(Box::new(io_error::new(
ErrorKind::NotFound,
"no desktop entry in file",
))),
}
}
fn parse_dir(d: &str) -> Result<Vec<Desktop>, Box<dyn Error>> {
let mut files: Vec<Desktop> = Vec::with_capacity(16);
for entry in WalkDir::new(d) {
let entry = entry?;
let path = entry.path();
if let Ok(d) = Desktop::parse(path.to_str().unwrap()) {
files.push(d)
}
}
Ok(files)
}
}
impl Ord for Desktop {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for Desktop {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Desktop {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
pub fn load_desktop_files() -> Vec<Desktop> {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
let xdg_data_home = match env::var_os("XDG_DATA_HOME") {
Some(s) => s.into_string().unwrap(),
None => format!("{}/.local/share", home),
};
let xdg_data_dirs = match env::var_os("XDG_DATA_DIRS") {
Some(s) => s.into_string().unwrap(),
None => "/usr/local/share:/usr/share".to_string(),
};
std::iter::once(xdg_data_home.as_str())
.chain(xdg_data_dirs.split(':'))
.map(|p| Desktop::parse_dir(&format!("{}/applications", p)))
.filter_map(Result::ok)
.flatten()
.filter(|d| {
!d.hidden && !d.no_display && (d.entry_type == "Application" || d.entry_type == "Link")
})
.unique_by(|x| x.name.clone())
.collect()
}
-265
View File
@@ -1,265 +0,0 @@
use std::{
cell::RefCell,
ffi::CStr,
fs::File,
io,
os::unix::io::{FromRawFd, RawFd},
rc::Rc,
time::SystemTime,
time::UNIX_EPOCH,
};
#[cfg(target_os = "linux")]
use nix::sys::memfd;
use nix::{
errno::Errno,
fcntl,
sys::{mman, stat},
unistd,
};
use memmap::MmapMut;
use wayland_client::{
protocol::{wl_buffer, wl_shm, wl_shm_pool},
Main,
};
/// A wrapper handling an SHM memory pool backed by a shared memory file
///
/// This wrapper handles for you the creation of the shared memory file and its synchronization
/// with the protocol.
///
/// Mempool internally tracks the release of the buffers by the compositor. As such, creating a buffer
/// that is not commited to a surface (and then never released by the server) would cause the Mempool
/// to be stuck believing it is still in use.
///
/// Mempool will also handle the destruction of buffers and as such the `destroy()` method should not
/// be used on buffers created from Mempool.
///
/// Overwriting the contents of the memory pool before it is completely freed may cause graphical
/// glitches due to the possible corruption of data while the compositor is reading it.
///
/// Mempool requires a callback that will be called when the pool becomes free, this
/// happens when all the pools buffers are released by the server.
pub struct MemPool {
file: File,
len: usize,
pool: Main<wl_shm_pool::WlShmPool>,
buffer_count: Rc<RefCell<u32>>,
mmap: MmapMut,
callback: Rc<RefCell<dyn FnMut(wayland_client::DispatchData)>>,
}
impl MemPool {
/// Create a new memory pool associated with given shm
pub fn new<F>(shm: Main<wl_shm::WlShm>, callback: F) -> io::Result<MemPool>
where
F: FnMut(wayland_client::DispatchData) + 'static,
{
let mem_fd = create_shm_fd()?;
let mem_file = unsafe { File::from_raw_fd(mem_fd) };
mem_file.set_len(128)?;
let pool = shm.create_pool(mem_fd, 128);
let mmap = unsafe { MmapMut::map_mut(&mem_file).unwrap() };
Ok(MemPool {
file: mem_file,
len: 128,
pool,
buffer_count: Rc::new(RefCell::new(0)),
mmap,
callback: Rc::new(RefCell::new(callback)),
})
}
/// Resize the memory pool
///
/// This affect the size as it is seen by the wayland server. Even
/// if you extend the temporary file size by writing to it, you need to
/// call this method otherwise the server won't see the new size.
///
/// Memory pools can only be extented, as such this method will do nothing
/// if the requested new size is smaller than the current size.
///
/// This method allows you to ensure the underlying pool is large enough to
/// hold what you want to write to it.
pub fn resize(&mut self, newsize: usize) -> io::Result<()> {
if newsize > self.len {
self.file.set_len(newsize as u64)?;
self.pool.resize(newsize as i32);
self.len = newsize;
self.mmap = unsafe { MmapMut::map_mut(&self.file).unwrap() };
}
Ok(())
}
/// Create a new buffer to this pool
///
/// The parameters are:
///
/// - `offset`: the offset (in bytes) from the beginning of the pool at which this
/// buffer starts
/// - `width`: the width of this buffer (in pixels)
/// - `height`: the height of this buffer (in pixels)
/// - `stride`: distance (in bytes) between the beginning of a row and the next one
/// - `format`: the encoding format of the pixels. Using a format that was not
/// advertised to the `wl_shm` global by the server is a protocol error and will
/// terminate your connection
pub fn buffer(
&self,
offset: i32,
width: i32,
height: i32,
stride: i32,
format: wl_shm::Format,
) -> wl_buffer::WlBuffer {
*self.buffer_count.borrow_mut() += 1;
let my_buffer_count = self.buffer_count.clone();
let my_callback = self.callback.clone();
let buffer = self
.pool
.create_buffer(offset, width, height, stride, format);
buffer.quick_assign(move |buffer, event, dispatch_data| match event {
wl_buffer::Event::Release => {
buffer.destroy();
let new_count = {
// borrow the buffer_count for as short as possible, in case
// the user wants to create a new buffer from the callback
let mut my_buffer_count = my_buffer_count.borrow_mut();
*my_buffer_count -= 1;
*my_buffer_count
};
if new_count == 0 {
(&mut *my_callback.borrow_mut())(dispatch_data);
}
}
_ => unreachable!(),
});
(*buffer).clone().detach()
}
/// Uses the memmap crate to map the underlying shared memory file
pub fn mmap(&mut self) -> &mut MmapMut {
&mut self.mmap
}
/// Returns true if the pool contains buffers that are currently in use by the server
pub fn is_used(&self) -> bool {
*self.buffer_count.borrow() != 0
}
}
impl Drop for MemPool {
fn drop(&mut self) {
self.pool.destroy();
}
}
impl io::Write for MemPool {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
io::Write::write(&mut self.file, buf)
}
fn flush(&mut self) -> io::Result<()> {
io::Write::flush(&mut self.file)
}
}
impl io::Seek for MemPool {
fn seek(&mut self, pos: io::SeekFrom) -> io::Result<u64> {
io::Seek::seek(&mut self.file, pos)
}
}
fn create_shm_fd() -> io::Result<RawFd> {
// Only try memfd on linux
#[cfg(target_os = "linux")]
loop {
match memfd::memfd_create(
CStr::from_bytes_with_nul(b"smithay-client-toolkit\0").unwrap(),
memfd::MemFdCreateFlag::MFD_CLOEXEC,
) {
Ok(fd) => return Ok(fd),
Err(nix::Error::Sys(Errno::EINTR)) => continue,
Err(nix::Error::Sys(Errno::ENOSYS)) => break,
Err(nix::Error::Sys(errno)) => return Err(io::Error::from(errno)),
Err(err) => unreachable!(err),
}
}
// Fallback to using shm_open
let sys_time = SystemTime::now();
let mut mem_file_handle = format!(
"/smithay-client-toolkit-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
loop {
match mman::shm_open(
mem_file_handle.as_str(),
fcntl::OFlag::O_CREAT
| fcntl::OFlag::O_EXCL
| fcntl::OFlag::O_RDWR
| fcntl::OFlag::O_CLOEXEC,
stat::Mode::S_IRUSR | stat::Mode::S_IWUSR,
) {
Ok(fd) => match mman::shm_unlink(mem_file_handle.as_str()) {
Ok(_) => return Ok(fd),
Err(nix::Error::Sys(errno)) => match unistd::close(fd) {
Ok(_) => return Err(io::Error::from(errno)),
Err(nix::Error::Sys(errno)) => return Err(io::Error::from(errno)),
Err(err) => panic!("{}", err),
},
Err(err) => panic!("{}", err),
},
Err(nix::Error::Sys(Errno::EEXIST)) => {
// If a file with that handle exists then change the handle
mem_file_handle = format!(
"/smithay-client-toolkit-{}",
sys_time.duration_since(UNIX_EPOCH).unwrap().subsec_nanos()
);
continue;
}
Err(nix::Error::Sys(Errno::EINTR)) => continue,
Err(nix::Error::Sys(errno)) => return Err(io::Error::from(errno)),
Err(err) => unreachable!(err),
}
}
}
pub struct DoubleMemPool {
pool1: MemPool,
pool2: MemPool,
switch: bool,
}
impl DoubleMemPool {
pub fn new(shm: Main<wl_shm::WlShm>) -> ::std::io::Result<DoubleMemPool> {
Ok(DoubleMemPool {
pool1: MemPool::new(shm.clone(), move |_| {})?,
pool2: MemPool::new(shm.clone(), move |_| {})?,
switch: false,
})
}
pub fn pool(&mut self) -> Option<(&mut MemPool, &mut MemPool)> {
let switch = self.switch;
self.switch = !self.switch;
let (last, cur) = if !switch {
(&mut self.pool2, &mut self.pool1)
} else {
(&mut self.pool1, &mut self.pool2)
};
if cur.is_used() {
None
} else {
Some((last, cur))
}
}
pub fn never_mind(&mut self) {
self.switch = !self.switch;
}
}
+84 -77
View File
@@ -1,7 +1,7 @@
use crate::buffer::Buffer; use crate::buffer::BufferView;
use crate::{color::Color, fonts::FontRef}; use crate::{color::Color, fonts::FontRef};
use rusttype::{point, Scale}; use rusttype::{Scale, point};
use std::collections::HashMap; use std::collections::HashMap;
struct CachedGlyph { struct CachedGlyph {
@@ -21,7 +21,6 @@ impl CachedGlyph {
if let Some(bounding_box) = glyph.pixel_bounding_box() { if let Some(bounding_box) = glyph.pixel_bounding_box() {
let origin = (bounding_box.min.x, bounding_box.min.y); let origin = (bounding_box.min.x, bounding_box.min.y);
let dimensions = ( let dimensions = (
(bounding_box.max.x - bounding_box.min.x) as u32, (bounding_box.max.x - bounding_box.min.x) as u32,
(bounding_box.max.y - bounding_box.min.y) as u32, (bounding_box.max.y - bounding_box.min.y) as u32,
@@ -47,19 +46,26 @@ impl CachedGlyph {
} }
} }
fn draw(&self, buf: &mut Buffer, pos: (i32, i32), bg: &Color, c: &Color) { fn draw(&self, buf: &mut BufferView, pos: (i32, i32), c: Color) {
let bounds = buf.get_bounds();
if pos.0 as u32 + self.dimensions.0 > bounds.2 {
return;
}
let offset = (bounds.0 as i32, bounds.1 as i32);
let width = self.dimensions.0 as i32 - 1;
let mut x = 0; let mut x = 0;
let mut y = 0; let mut y = 0;
for v in &self.render { for v in &self.render {
let _ = buf.put( buf.put_raw(
( (
(x + pos.0 + self.origin.0) as u32, (x + pos.0 + self.origin.0 + offset.0) as u32,
(y + pos.1 + self.origin.1) as u32, (y + pos.1 + self.origin.1 + offset.1) as u32,
), ),
&bg.blend(&c, *v), c.alpha(*v),
); );
if x == self.dimensions.0 as i32 - 1 { if x == width {
y += 1; y += 1;
x = 0; x = 0;
} else { } else {
@@ -76,7 +82,7 @@ pub struct Font<'a> {
} }
impl<'a> Font<'a> { impl<'a> Font<'a> {
pub fn new(font: FontRef<'a>, size: f32) -> Font { pub fn new(font: FontRef<'a>, size: f32) -> Font<'a> {
Font { Font {
glyphs: HashMap::new(), glyphs: HashMap::new(),
font, font,
@@ -84,9 +90,15 @@ impl<'a> Font<'a> {
} }
} }
pub fn height(&self) -> f32 {
let scale = Scale::uniform(self.size);
let m = self.font.v_metrics(scale);
m.ascent - m.descent
}
pub fn add_str_to_cache(&mut self, s: &str) { pub fn add_str_to_cache(&mut self, s: &str) {
for ch in s.chars() { for ch in s.chars() {
if self.glyphs.get(&ch).is_none() { if !self.glyphs.contains_key(&ch) {
let glyph = CachedGlyph::new(self.font, self.size, ch); let glyph = CachedGlyph::new(self.font, self.size, ch);
self.glyphs.insert(ch, glyph); self.glyphs.insert(ch, glyph);
} }
@@ -95,9 +107,8 @@ impl<'a> Font<'a> {
pub fn draw_text( pub fn draw_text(
&self, &self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color, c: Color,
c: &Color,
s: &str, s: &str,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
let mut x_off = 0; let mut x_off = 0;
@@ -107,10 +118,10 @@ impl<'a> Font<'a> {
let glyph = match self.glyphs.get(&ch) { let glyph = match self.glyphs.get(&ch) {
Some(glyph) => glyph, Some(glyph) => glyph,
None => { None => {
return Err(::std::io::Error::new( return Err(::std::io::Error::other(format!(
::std::io::ErrorKind::Other, "glyph for {:} not in cache",
format!("glyph for {:} not in cache", ch), ch
)) )));
} }
}; };
glyphs.push(glyph); glyphs.push(glyph);
@@ -119,7 +130,7 @@ impl<'a> Font<'a> {
} }
} }
for glyph in glyphs { for glyph in glyphs {
glyph.draw(buf, (x_off, -off), bg, c); glyph.draw(buf, (x_off, -off), c);
x_off += glyph.advance; x_off += glyph.advance;
} }
@@ -128,9 +139,8 @@ impl<'a> Font<'a> {
pub fn draw_text_with_cursor( pub fn draw_text_with_cursor(
&self, &self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color, c: Color,
c: &Color,
s: &str, s: &str,
cursor: usize, cursor: usize,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
@@ -141,10 +151,10 @@ impl<'a> Font<'a> {
let glyph = match self.glyphs.get(&ch) { let glyph = match self.glyphs.get(&ch) {
Some(glyph) => glyph, Some(glyph) => glyph,
None => { None => {
return Err(::std::io::Error::new( return Err(::std::io::Error::other(format!(
::std::io::ErrorKind::Other, "glyph for {:} not in cache",
format!("glyph for {:} not in cache", ch), ch
)) )));
} }
}; };
glyphs.push(glyph); glyphs.push(glyph);
@@ -153,12 +163,12 @@ impl<'a> Font<'a> {
} }
} }
let height = buf.get_bounds().3; let height = self.size.ceil() as u32;
for (i, glyph) in glyphs.iter().enumerate() { for (i, glyph) in glyphs.iter().enumerate() {
if i == cursor { if i == cursor {
self.draw_cursor(buf, c, x_off as u32, height)?; self.draw_cursor(buf, c, x_off as u32, height)?;
} }
glyph.draw(buf, (x_off, -off), bg, c); glyph.draw(buf, (x_off, -off), c);
x_off += glyph.advance; x_off += glyph.advance;
} }
if cursor == glyphs.len() { if cursor == glyphs.len() {
@@ -168,16 +178,20 @@ impl<'a> Font<'a> {
Ok((x_off as u32, self.size as u32)) Ok((x_off as u32, self.size as u32))
} }
pub fn draw_cursor( fn draw_cursor(
&self, &self,
buf: &mut Buffer, buf: &mut BufferView,
c: &Color, c: Color,
offset: u32, offset: u32,
height: u32, height: u32,
) -> Result<(), ::std::io::Error> { ) -> Result<(), ::std::io::Error> {
// draw cursor // draw cursor
for i in 1..height { let bounds = buf.get_bounds();
buf.put((offset, i), c)? if offset + 1 > bounds.2 {
return Ok(());
}
for i in bounds.1 + 1..height + bounds.1 {
buf.put_raw((bounds.0 + offset, i), c);
} }
Ok(()) Ok(())
} }
@@ -189,10 +203,10 @@ impl<'a> Font<'a> {
let glyph = match self.glyphs.get(&ch) { let glyph = match self.glyphs.get(&ch) {
Some(glyph) => glyph, Some(glyph) => glyph,
None => { None => {
return Err(::std::io::Error::new( return Err(::std::io::Error::other(format!(
::std::io::ErrorKind::Other, "glyph for {:} not in cache",
format!("glyph for {:} not in cache", ch), ch
)) )));
} }
}; };
let width = glyph.advance; let width = glyph.advance;
@@ -205,32 +219,29 @@ impl<'a> Font<'a> {
pub fn auto_draw_text( pub fn auto_draw_text(
&mut self, &mut self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color, c: Color,
c: &Color,
s: &str, s: &str,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
self.add_str_to_cache(s); self.add_str_to_cache(s);
self.draw_text(buf, bg, c, s) self.draw_text(buf, c, s)
} }
pub fn auto_draw_text_with_cursor( pub fn auto_draw_text_with_cursor(
&mut self, &mut self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color, c: Color,
c: &Color,
s: &str, s: &str,
cursor: usize, cursor: usize,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
self.add_str_to_cache(s); self.add_str_to_cache(s);
self.draw_text_with_cursor(buf, bg, c, s, cursor) self.draw_text_with_cursor(buf, c, s, cursor)
} }
pub fn draw_text_fixed_width( pub fn draw_text_fixed_width(
&self, &self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color, c: Color,
c: &Color,
distances: &[u32], distances: &[u32],
s: &str, s: &str,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
@@ -241,10 +252,10 @@ impl<'a> Font<'a> {
let glyph = match self.glyphs.get(&ch) { let glyph = match self.glyphs.get(&ch) {
Some(glyph) => glyph, Some(glyph) => glyph,
None => { None => {
return Err(::std::io::Error::new( return Err(::std::io::Error::other(format!(
::std::io::ErrorKind::Other, "glyph for {:} not in cache",
format!("glyph for {:} not in cache", ch), ch
)) )));
} }
}; };
glyphs.push(glyph); glyphs.push(glyph);
@@ -253,7 +264,7 @@ impl<'a> Font<'a> {
} }
} }
for (idx, glyph) in glyphs.into_iter().enumerate() { for (idx, glyph) in glyphs.into_iter().enumerate() {
glyph.draw(buf, (x_off, -off), bg, c); glyph.draw(buf, (x_off, -off), c);
x_off += distances[idx] as i32; x_off += distances[idx] as i32;
} }
@@ -262,8 +273,7 @@ impl<'a> Font<'a> {
pub fn draw_text_individual_colors( pub fn draw_text_individual_colors(
&self, &self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color,
color: &[Color], color: &[Color],
s: &str, s: &str,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
@@ -274,10 +284,10 @@ impl<'a> Font<'a> {
let glyph = match self.glyphs.get(&ch) { let glyph = match self.glyphs.get(&ch) {
Some(glyph) => glyph, Some(glyph) => glyph,
None => { None => {
return Err(::std::io::Error::new( return Err(::std::io::Error::other(format!(
::std::io::ErrorKind::Other, "glyph for {:} not in cache",
format!("glyph for {:} not in cache", ch), ch
)) )));
} }
}; };
glyphs.push(glyph); glyphs.push(glyph);
@@ -286,7 +296,7 @@ impl<'a> Font<'a> {
} }
} }
for (idx, glyph) in glyphs.into_iter().enumerate() { for (idx, glyph) in glyphs.into_iter().enumerate() {
glyph.draw(buf, (x_off, -off), bg, &color[idx]); glyph.draw(buf, (x_off, -off), color[idx]);
x_off += glyph.advance; x_off += glyph.advance;
} }
@@ -295,32 +305,30 @@ impl<'a> Font<'a> {
pub fn auto_draw_text_individual_colors( pub fn auto_draw_text_individual_colors(
&mut self, &mut self,
buf: &mut Buffer, buf: &mut BufferView,
bg: &Color,
color: &[Color], color: &[Color],
s: &str, s: &str,
) -> Result<(u32, u32), ::std::io::Error> { ) -> Result<(u32, u32), ::std::io::Error> {
self.add_str_to_cache(s); self.add_str_to_cache(s);
self.draw_text_individual_colors(buf, bg, color, s) self.draw_text_individual_colors(buf, color, s)
} }
} }
pub fn draw_box(buf: &mut Buffer, c: &Color, dim: (u32, u32)) -> Result<(), ::std::io::Error> { pub fn draw_box(buf: &mut BufferView, c: Color, dim: (u32, u32)) -> Result<(), ::std::io::Error> {
for x in 0..dim.0 { let bounds = buf.get_bounds();
let _ = buf.put((x, 0), c); buf.put_line_raw((bounds.0, bounds.1), dim.0, c);
let _ = buf.put((x, dim.1 - 1), c); buf.put_line_raw((bounds.0, bounds.1 + dim.1 - 1), dim.0, c);
} for y in bounds.1..dim.1 + bounds.1 {
for y in 0..dim.1 { buf.put_raw((bounds.0, y), c);
buf.put((0, y), c)?; buf.put_raw((bounds.0 + dim.0 - 1, y), c);
buf.put((dim.0 - 1, y), c)?;
} }
Ok(()) Ok(())
} }
pub fn draw_bar( pub fn draw_bar(
buf: &mut Buffer, buf: &mut BufferView,
color: &Color, color: Color,
length: u32, length: u32,
height: u32, height: u32,
fill: f32, fill: f32,
@@ -329,10 +337,9 @@ pub fn draw_bar(
if fill_pos > length { if fill_pos > length {
fill_pos = length; fill_pos = length;
} }
for y in 0..height { let bounds = buf.get_bounds();
for x in 0..fill_pos { for y in bounds.1..height + bounds.1 {
let _ = buf.put((x, y), color); buf.put_line_raw((bounds.0, y), fill_pos, color);
}
} }
Ok(()) Ok(())
+61
View File
@@ -0,0 +1,61 @@
use std::sync::{Arc, Mutex};
use calloop::ping::Ping;
use crate::keyboard::KeyEvent;
#[derive(Copy, Clone, Debug)]
pub enum PointerButton {
Left,
Right,
Middle,
ScrollVertical(f64),
ScrollHorizontal(f64),
}
#[derive(Debug)]
pub struct PointerEvent {
pub button: PointerButton,
pub pos: (u32, u32),
}
#[derive(Debug)]
pub enum Event {
NewMinute,
PowerUpdate,
LauncherUpdate,
AudioUpdate,
KeyEvent(KeyEvent),
PointerEvent(PointerEvent),
TokenUpdate(String),
FocusLost,
}
pub struct Events {
dirty: bool,
events: Vec<Event>,
ping: Ping,
}
impl Events {
pub fn new(ping: Ping) -> Arc<Mutex<Events>> {
Arc::new(Mutex::new(Events {
dirty: false,
events: Vec::new(),
ping,
}))
}
pub fn add_event(&mut self, ev: Event) {
self.events.push(ev);
if !self.dirty {
self.dirty = true;
self.ping.ping();
}
}
pub fn flush(&mut self) -> Vec<Event> {
self.dirty = false;
std::mem::take(&mut self.events)
}
}
+198 -20
View File
@@ -1,30 +1,56 @@
//! Utility module for fonts //! Utility module for fonts
use fontconfig::Fontconfig as FontConfig; use crate::draw;
use rusttype::Font; use rusttype::Font;
use std::{ use std::{
collections::HashMap, cell::RefCell, collections::HashMap, fs::File, hash, io::Read, mem, path::Path, rc::Rc, thread,
fs::File,
io::Read,
path::{Path, PathBuf},
}; };
/// FontMap is used to store different font configurations
pub type FontMap<'a> = HashMap<String, rusttype::Font<'a>>;
/// FontRef is used to store Fonts on widgets. /// FontRef is used to store Fonts on widgets.
pub type FontRef<'a> = &'a rusttype::Font<'a>; pub type FontRef<'a> = &'a rusttype::Font<'a>;
/// FontSeeker is a marker struct that is used to look up fonts /// Ask fontconfig for the file backing a font name ("sans", "monospace",
pub(crate) struct FontSeeker; /// "Noto Sans", ...), the way fc-match does.
#[cfg(feature = "fontconfig")]
pub(crate) fn find_font(name: &str) -> Result<String, String> {
use fontconfig::Fontconfig as FontConfig;
let fc = FontConfig::new().ok_or("could not initialise fontconfig")?;
let found = fc
.find(name, None)
.ok_or_else(|| format!("fontconfig has no match for {:?}", name))?;
found
.path
.to_str()
.map(|s| s.to_string())
.ok_or_else(|| format!("font path for {:?} is not UTF-8", name))
}
impl FontSeeker { #[cfg(not(feature = "fontconfig"))]
/// Acts like fc-match. pub(crate) fn find_font(name: &str) -> Result<String, String> {
/// Given a string, it matches it to a font file and returns its path. Err(format!(
pub(crate) fn from_string(name: &str) -> PathBuf { "no path configured for font {:?} and wldash was built without fontconfig",
let fc = FontConfig::new().unwrap(); name
fc.find(name, None).unwrap().path ))
}
/// Resolve the file to load for `name`: a path pinned in the config if it
/// exists on this machine, otherwise whatever fontconfig picks for the name.
///
/// Configs roam between machines on a shared home, and font packages do
/// not keep the same file layout everywhere (Noto under fonts/noto on one
/// distribution, fonts/google-noto on another), so a pinned path that is
/// missing is a reason to fall back, not to abort.
pub(crate) fn resolve_font_path(name: &str, pinned: Option<&str>) -> Result<String, String> {
if let Some(p) = pinned {
if Path::new(p).is_file() {
return Ok(p.to_string());
} }
eprintln!(
"wldash: configured font file for {:?} not found: {}; asking fontconfig instead",
name, p
);
}
find_font(name)
} }
/// FontLoader is a marker struct that is used to load files /// FontLoader is a marker struct that is used to load files
@@ -32,16 +58,168 @@ pub(crate) struct FontLoader;
impl FontLoader { impl FontLoader {
/// Given a path, loads it as a Font, which can be rendered to the screen. /// Given a path, loads it as a Font, which can be rendered to the screen.
pub(crate) fn from_path<'a, P>(path: P) -> Option<Font<'a>> pub(crate) fn from_path<'a, P>(path: P) -> Result<Font<'a>, String>
where where
P: AsRef<Path>, P: AsRef<Path>,
{ {
let mut file = File::open(path).expect("Font file not found"); let path = path.as_ref();
let mut file =
File::open(path).map_err(|e| format!("cannot open {}: {}", path.display(), e))?;
let mut data = match file.metadata() { let mut data = match file.metadata() {
Ok(metadata) => Vec::with_capacity(metadata.len() as usize), Ok(metadata) => Vec::with_capacity(metadata.len() as usize),
Err(_) => vec![], Err(_) => vec![],
}; };
file.read_to_end(&mut data).unwrap(); file.read_to_end(&mut data)
Font::try_from_vec(data) .map_err(|e| format!("cannot read {}: {}", path.display(), e))?;
Font::try_from_vec(data).ok_or_else(|| format!("{} is not a usable font", path.display()))
}
}
#[derive(Debug, Copy, Clone)]
struct ComparableF32(f32);
impl ComparableF32 {
fn key(&self) -> u64 {
self.0.to_bits() as u64
}
}
impl hash::Hash for ComparableF32 {
fn hash<H>(&self, state: &mut H)
where
H: hash::Hasher,
{
self.key().hash(state)
}
}
impl PartialEq for ComparableF32 {
fn eq(&self, other: &ComparableF32) -> bool {
self.key() == other.key()
}
}
impl Eq for ComparableF32 {}
/// font name -> (font name, [(size, glyphs to preload)])
type RequiredFonts = HashMap<&'static str, (&'static str, Vec<(f32, &'static str)>)>;
pub struct FontMap {
fonts: HashMap<(&'static str, ComparableF32), draw::Font<'static>>,
font_paths: HashMap<&'static str, String>,
required_fonts: RequiredFonts,
}
impl FontMap {
pub fn new() -> FontMap {
FontMap {
fonts: HashMap::new(),
font_paths: HashMap::new(),
required_fonts: HashMap::new(),
}
}
pub fn queue_font(&mut self, font_name: &'static str, size: f32, preload: &'static str) {
match self.required_fonts.get_mut(font_name) {
Some(v) => v.1.push((size, preload)),
None => {
self.required_fonts
.insert(font_name, (font_name, vec![(size, preload)]));
}
}
}
pub fn add_font_path(&mut self, font_name: &'static str, font_path: String) {
self.font_paths.insert(font_name, font_path);
}
/// Load every queued font. Runs on its own thread; a font that cannot
/// be found or read is a configuration problem the user has to see, so
/// it is reported in one line and the process exits instead of
/// aborting with a panic backtrace.
pub fn load_fonts(&mut self) {
for (font_name, v) in self.required_fonts.iter() {
let pinned = self.font_paths.get(font_name).map(|s| s.as_str());
let path = match resolve_font_path(font_name, pinned) {
Ok(p) => p,
Err(e) => {
eprintln!("wldash: cannot resolve font {:?}: {}", font_name, e);
std::process::exit(1);
}
};
let font = match FontLoader::from_path(&path) {
Ok(f) => f,
Err(e) => {
eprintln!("wldash: cannot load font {:?}: {}", font_name, e);
std::process::exit(1);
}
};
let fontref = Box::leak(Box::new(font));
for (size, preload) in &v.1 {
if let Some(font) = self.fonts.get_mut(&(font_name, ComparableF32(*size))) {
font.add_str_to_cache(preload);
} else {
let mut font = draw::Font::new(fontref, *size);
font.add_str_to_cache(preload);
self.fonts.insert((font_name, ComparableF32(*size)), font);
}
}
}
}
pub fn get_font(&mut self, font_name: &'static str, size: f32) -> &mut draw::Font<'static> {
self.fonts
.get_mut(&(font_name, ComparableF32(size)))
.expect("no font at specified size")
}
}
pub enum MaybeFontMap {
Waiting(thread::JoinHandle<FontMap>),
Ready(Rc<RefCell<FontMap>>),
Invalid,
}
impl MaybeFontMap {
pub fn unwrap(&self) -> Rc<RefCell<FontMap>> {
match self {
MaybeFontMap::Ready(f) => f.clone(),
_ => panic!("fontmap not yet ready"),
}
}
pub fn resolve(&mut self) {
if matches!(self, MaybeFontMap::Waiting(_)) {
let s = mem::replace(self, MaybeFontMap::Invalid);
match s {
MaybeFontMap::Waiting(handle) => {
*self = MaybeFontMap::Ready(Rc::new(RefCell::new(handle.join().unwrap())));
}
_ => unreachable!(),
}
}
}
}
#[cfg(test)]
mod tests {
use super::resolve_font_path;
#[test]
fn pinned_path_wins_when_present() {
let dir = std::env::temp_dir().join(format!("wldash-font-test-{}", std::process::id()));
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("x.ttf");
std::fs::write(&f, b"not really a font").unwrap();
let p = f.to_str().unwrap();
assert_eq!(resolve_font_path("sans", Some(p)).unwrap(), p);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn missing_pinned_path_falls_back_to_lookup() {
// Whatever fontconfig answers (or an error without fontconfig),
// the missing pinned path itself must never be returned.
let r = resolve_font_path("sans", Some("/nonexistent/wldash/font.ttf"));
assert_ne!(r.as_deref().ok(), Some("/nonexistent/wldash/font.ttf"));
} }
} }
-267
View File
@@ -1,267 +0,0 @@
#![allow(dead_code, non_camel_case_types, clippy::identity_op)]
#![cfg_attr(rustfmt, rustfmt_skip)]
use std::os::raw::{c_char, c_int, c_void, c_uint};
pub const XKB_MOD_NAME_SHIFT : &[u8] = b"Shift\0";
pub const XKB_MOD_NAME_CAPS : &[u8] = b"Lock\0";
pub const XKB_MOD_NAME_CTRL : &[u8] = b"Control\0";
pub const XKB_MOD_NAME_ALT : &[u8] = b"Mod1\0";
pub const XKB_MOD_NAME_NUM : &[u8] = b"Mod2\0";
pub const XKB_MOD_NAME_LOGO : &[u8] = b"Mod4\0";
pub const XKB_LED_NAME_CAPS : &[u8] = b"Caps Lock\0";
pub const XKB_LED_NAME_NUM : &[u8] = b"Num Lock\0";
pub const XKB_LED_NAME_SCROLL : &[u8] = b"Scroll Lock\0";
pub struct xkb_context;
pub struct xkb_keymap;
pub struct xkb_state;
pub struct xkb_compose_table;
pub struct xkb_compose_state;
pub type xkb_keycode_t = u32;
pub type xkb_keysym_t = u32;
pub type xkb_layout_index_t = u32;
pub type xkb_layout_mask_t = u32;
pub type xkb_level_index_t = u32;
pub type xkb_mod_index_t = u32;
pub type xkb_mod_mask_t = u32;
pub type xkb_led_index_t = u32;
pub type xkb_led_mask_t = u32;
pub const XKB_KEYCODE_INVALID :u32 = 0xffff_ffff;
pub const XKB_LAYOUT_INVALID :u32 = 0xffff_ffff;
pub const XKB_LEVEL_INVALID :u32 = 0xffff_ffff;
pub const XKB_MOD_INVALID :u32 = 0xffff_ffff;
pub const XKB_LED_INVALID :u32 = 0xffff_ffff;
pub const XKB_KEYCODE_MAX :u32 = 0xffff_fffe;
#[repr(C)]
#[derive(Copy, Clone)]
pub struct xkb_rule_names {
pub rules: *const c_char,
pub model: *const c_char ,
pub layout: *const c_char,
pub variant: *const c_char,
pub options: *const c_char,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_keysym_flags {
/** Do not apply any flags. */
XKB_KEYSYM_NO_FLAGS = 0,
/** Find keysym by case-insensitive search. */
XKB_KEYSYM_CASE_INSENSITIVE = (1 << 0)
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_context_flags {
/** Do not apply any context flags. */
XKB_CONTEXT_NO_FLAGS = 0,
/** Create this context with an empty include path. */
XKB_CONTEXT_NO_DEFAULT_INCLUDES = (1 << 0),
/**
* Don't take RMLVO names from the environment.
* @since 0.3.0
*/
XKB_CONTEXT_NO_ENVIRONMENT_NAMES = (1 << 1)
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_log_level {
/** Log critical internal errors only. */
XKB_LOG_LEVEL_CRITICAL = 10,
/** Log all errors. */
XKB_LOG_LEVEL_ERROR = 20,
/** Log warnings and errors. */
XKB_LOG_LEVEL_WARNING = 30,
/** Log information, warnings, and errors. */
XKB_LOG_LEVEL_INFO = 40,
/** Log everything. */
XKB_LOG_LEVEL_DEBUG = 50
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_keymap_compile_flags {
/** Do not apply any flags. */
XKB_KEYMAP_COMPILE_NO_FLAGS = 0,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_keymap_format {
/** Cannot be used for creation */
XKB_KEYMAP_USE_ORIGINAL_FORMAT = 0,
/** The current/classic XKB text format, as generated by xkbcomp -xkb. */
XKB_KEYMAP_FORMAT_TEXT_V1 = 1,
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_key_direction {
/** The key was released. */
XKB_KEY_UP,
/** The key was pressed. */
XKB_KEY_DOWN
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_compose_compile_flags {
XKB_COMPOSE_COMPILE_NO_FLAGS = 0
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_compose_format {
XKB_COMPOSE_FORMAT_TEXT_V1 = 1
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_compose_state_flags {
XKB_COMPOSE_STATE_NO_FLAGS = 0
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_compose_status {
XKB_COMPOSE_NOTHING,
XKB_COMPOSE_COMPOSING,
XKB_COMPOSE_COMPOSED,
XKB_COMPOSE_CANCELLED
}
#[repr(C)]
#[derive(Copy, Clone, Debug, PartialEq)]
pub enum xkb_compose_feed_result {
XKB_COMPOSE_FEED_IGNORED,
XKB_COMPOSE_FEED_ACCEPTED
}
bitflags::bitflags!(
pub struct xkb_state_component: u32 {
/** Depressed modifiers, i.e. a key is physically holding them. */
const XKB_STATE_MODS_DEPRESSED = (1 << 0);
/** Latched modifiers, i.e. will be unset after the next non-modifier
* key press. */
const XKB_STATE_MODS_LATCHED = (1 << 1);
/** Locked modifiers, i.e. will be unset after the key provoking the
* lock has been pressed again. */
const XKB_STATE_MODS_LOCKED = (1 << 2);
/** Effective modifiers, i.e. currently active and affect key
* processing (derived from the other state components).
* Use this unless you explictly care how the state came about. */
const XKB_STATE_MODS_EFFECTIVE = (1 << 3);
/** Depressed layout, i.e. a key is physically holding it. */
const XKB_STATE_LAYOUT_DEPRESSED = (1 << 4);
/** Latched layout, i.e. will be unset after the next non-modifier
* key press. */
const XKB_STATE_LAYOUT_LATCHED = (1 << 5);
/** Locked layout, i.e. will be unset after the key provoking the lock
* has been pressed again. */
const XKB_STATE_LAYOUT_LOCKED = (1 << 6);
/** Effective layout, i.e. currently active and affects key processing
* (derived from the other state components).
* Use this unless you explictly care how the state came about. */
const XKB_STATE_LAYOUT_EFFECTIVE = (1 << 7);
/** LEDs (derived from the other state components). */
const XKB_STATE_LEDS = (1 << 8);
}
);
dlopen_external_library!(XkbCommon,
functions:
fn xkb_keysym_get_name(xkb_keysym_t, *mut c_char, usize) -> c_int,
fn xkb_keysym_from_name(*const c_char, xkb_keysym_flags) -> xkb_keysym_t,
fn xkb_keysym_to_utf8(xkb_keysym_t, *mut c_char, usize) -> c_int,
fn xkb_keysym_to_utf32(xkb_keysym_t) -> u32,
fn xkb_context_new(xkb_context_flags) -> *mut xkb_context,
fn xkb_context_ref(*mut xkb_context) -> *mut xkb_context,
fn xkb_context_unref(*mut xkb_context) -> (),
fn xkb_context_set_user_data(*mut xkb_context, *mut c_void) -> (),
fn xkb_context_get_user_data(*mut xkb_context) -> *mut c_void,
fn xkb_context_include_path_append(*mut xkb_context, *const c_char) -> c_int,
fn xkb_context_include_path_append_default(*mut xkb_context) -> c_int,
fn xkb_context_include_path_reset_defaults(*mut xkb_context) -> c_int,
fn xkb_context_include_path_clear(*mut xkb_context) -> (),
fn xkb_context_num_include_paths(*mut xkb_context) -> c_uint,
fn xkb_context_include_path_get(*mut xkb_context, c_uint) -> *const c_char,
fn xkb_context_set_log_level(*mut xkb_context, xkb_log_level) -> (),
fn xkb_context_get_log_level(*mut xkb_context) -> xkb_log_level,
fn xkb_context_set_log_verbosity(*mut xkb_context, c_int) -> (),
fn xkb_context_get_log_verbosity(*mut xkb_context) -> c_int,
fn xkb_keymap_new_from_names(*mut xkb_context,
*const xkb_rule_names,
xkb_keymap_compile_flags
) -> *mut xkb_keymap,
fn xkb_keymap_new_from_string(*mut xkb_context,
*const c_char,
xkb_keymap_format,
xkb_keymap_compile_flags
) -> *mut xkb_keymap,
fn xkb_keymap_new_from_buffer(*mut xkb_context,
*const c_char,
usize,
xkb_keymap_format,
xkb_keymap_compile_flags
) -> *mut xkb_keymap,
fn xkb_keymap_ref(*mut xkb_keymap) -> *mut xkb_keymap,
fn xkb_keymap_unref(*mut xkb_keymap) -> (),
fn xkb_keymap_get_as_string(*mut xkb_keymap, xkb_keymap_format) -> *const c_char,
fn xkb_keymap_key_repeats(*mut xkb_keymap, xkb_keycode_t) -> c_int,
fn xkb_state_new(*mut xkb_keymap) -> *mut xkb_state,
fn xkb_state_ref(*mut xkb_state) -> *mut xkb_state,
fn xkb_state_unref(*mut xkb_state) -> (),
fn xkb_state_update_mask(*mut xkb_state,
xkb_mod_mask_t,
xkb_mod_mask_t,
xkb_mod_mask_t,
xkb_layout_index_t,
xkb_layout_index_t,
xkb_layout_index_t
) -> xkb_state_component,
fn xkb_state_update_key(*mut xkb_state,
xkb_keycode_t,
xkb_key_direction
) -> xkb_state_component,
fn xkb_state_key_get_syms(*mut xkb_state,
xkb_keycode_t,
*const *mut xkb_keysym_t
) -> c_int,
fn xkb_state_key_get_utf8(*mut xkb_state,
xkb_keycode_t,
*mut c_char,
usize
) -> c_int,
fn xkb_state_key_get_utf32(*mut xkb_state, xkb_keycode_t) -> u32,
fn xkb_state_key_get_one_sym(*mut xkb_state, xkb_keycode_t) -> xkb_keysym_t,
fn xkb_state_mod_name_is_active(*mut xkb_state, *const c_char, xkb_state_component) -> c_int,
fn xkb_compose_table_new_from_locale(*mut xkb_context, *const c_char, xkb_compose_compile_flags) -> *mut xkb_compose_table,
fn xkb_compose_table_unref(*mut xkb_compose_table) -> (),
fn xkb_compose_state_new(*mut xkb_compose_table, xkb_compose_state_flags) -> *mut xkb_compose_state,
fn xkb_compose_state_unref(*mut xkb_compose_state) -> (),
fn xkb_compose_state_feed(*mut xkb_compose_state, xkb_keysym_t) -> xkb_compose_feed_result,
fn xkb_compose_state_reset(*mut xkb_compose_state) -> (),
fn xkb_compose_state_get_status(*mut xkb_compose_state) -> xkb_compose_status,
fn xkb_compose_state_get_utf8(*mut xkb_compose_state, *mut c_char, usize) -> c_int,
fn xkb_compose_state_get_one_sym(*mut xkb_compose_state) -> xkb_keysym_t,
);
lazy_static::lazy_static!(
pub static ref XKBCOMMON_OPTION: Option<XkbCommon> = {
XkbCommon::open("libxkbcommon.so.0")
.or_else(|_| XkbCommon::open("libxkbcommon.so"))
.ok()
};
pub static ref XKBCOMMON_HANDLE: &'static XkbCommon = {
XKBCOMMON_OPTION.as_ref().expect("Library libxkbcommon.so could not be loaded.")
};
);
-17
View File
@@ -1,17 +0,0 @@
#![allow(missing_docs, non_upper_case_globals, unused_parens, clippy::all)]
pub const XKB_KEY_BackSpace: u32 = 0xff08; /* Back space, back char */
pub const XKB_KEY_Tab: u32 = 0xff09;
pub const XKB_KEY_Return: u32 = 0xff0d; /* Return, enter */
pub const XKB_KEY_Escape: u32 = 0xff1b;
pub const XKB_KEY_Delete: u32 = 0xffff; /* Delete, rubout */
pub const XKB_KEY_Home: u32 = 0xff50;
pub const XKB_KEY_Left: u32 = 0xff51; /* Move left, left arrow */
pub const XKB_KEY_Right: u32 = 0xff53; /* Move right, right arrow */
pub const XKB_KEY_End: u32 = 0xff57; /* EOL */
pub const XKB_KEY_ISO_Left_Tab: u32 = 0xfe20;
pub const XKB_KEY_e: u32 = 0x0065; /* U+0065 LATIN SMALL LETTER E */
pub const XKB_KEY_a: u32 = 0x0061; /* U+0061 LATIN SMALL LETTER A */
pub const XKB_KEY_c: u32 = 0x0063; /* U+0063 LATIN SMALL LETTER C */
pub const XKB_KEY_u: u32 = 0x0075; /* U+0075 LATIN SMALL LETTER U */
+80 -264
View File
@@ -12,263 +12,99 @@
//! calloop event loop. Not doing so will prevent key repetition to work //! calloop event loop. Not doing so will prevent key repetition to work
//! (but the rest of the functionnality will not be affected). //! (but the rest of the functionnality will not be affected).
use std::{cell::RefCell, os::unix::io::RawFd, rc::Rc}; use std::{default::Default, os::unix::io::OwnedFd};
use byteorder::{ByteOrder, NativeEndian}; use wayland_client::{WEnum, protocol::wl_keyboard};
use xkbcommon::xkb;
pub use wayland_client::protocol::wl_keyboard::KeyState; pub use xkbcommon::xkb::keysyms;
use wayland_client::{
protocol::{wl_keyboard, wl_seat, wl_surface},
Attached,
};
mod ffi; mod repeat;
pub mod keysyms;
mod state; mod state;
pub use self::repeat::{KeyRepeatSource, RepeatMessage};
use self::state::KbState; use self::state::KbState;
pub use self::state::{ModifiersState, RMLVO}; pub use self::state::ModifiersState;
#[derive(Debug)] #[derive(Debug, Clone)]
/// An error that occurred while trying to initialize a mapped keyboard pub struct KeyEvent {
pub enum Error { #[allow(dead_code)]
/// libxkbcommon is not available pub rawkey: u32,
XKBNotFound, pub keysym: u32,
/// Provided RMLVO specified a keymap that would not be loaded pub state: WEnum<wl_keyboard::KeyState>,
BadNames, pub modifiers: ModifiersState,
pub utf8: Option<String>,
pub repeats: bool,
} }
/// Events received from a mapped keyboard pub struct KeymapDescription {
pub enum Event<'a> { fd: OwnedFd,
/// The keyboard focus has entered a surface
Enter {
/// serial number of the event
serial: u32,
/// surface that was entered
surface: wl_surface::WlSurface,
/// raw values of the currently pressed keys
rawkeys: &'a [u32],
/// interpreted symbols of the currently pressed keys
keysyms: &'a [u32],
},
/// The keyboard focus has left a surface
Leave {
/// serial number of the event
serial: u32,
/// surface that was left
surface: wl_surface::WlSurface,
},
/// The key modifiers have changed state
Modifiers {
/// current state of the modifiers
modifiers: ModifiersState,
},
/// A key event occurred
Key {
/// serial number of the event
serial: u32,
/// time at which the keypress occurred
time: u32,
/// raw value of the key
rawkey: u32,
/// interpreted symbol of the key
keysym: u32,
/// new state of the key
state: KeyState,
/// utf8 interpretation of the entered text
///
/// will always be `None` on key release events
utf8: Option<String>,
repeats: bool,
},
RepeatInfo {
rate: i32,
delay: i32,
},
}
/// Implement a keyboard for keymap translation with key repetition
///
/// This requires you to provide a callback to receive the events after they
/// have been interpreted with the keymap.
///
/// The keymap will be loaded from the provided RMLVO rules, or from the compositor
/// provided keymap if `None`.
///
/// Returns an error if xkbcommon could not be initialized, the RMLVO specification
/// contained invalid values, or if the provided seat does not have keyboard capability.
///
/// **Note:** This adapter does not handle key repetition. See `map_keyboard_repeat` for that.
pub fn map_keyboard<F>(
seat: &Attached<wl_seat::WlSeat>,
rmlvo: Option<RMLVO>,
callback: F,
) -> Result<wl_keyboard::WlKeyboard, Error>
where
F: FnMut(Event<'_>, wl_keyboard::WlKeyboard, wayland_client::DispatchData<'_>) + 'static,
{
let keyboard = seat.get_keyboard();
let state = Rc::new(RefCell::new(
rmlvo
.map(KbState::from_rmlvo)
.unwrap_or_else(KbState::new)?,
));
let callback = Rc::new(RefCell::new(callback));
// prepare the handler
let mut kbd_handler = KbdHandler { callback, state };
keyboard.quick_assign(move |keyboard, event, data| {
kbd_handler.event(keyboard.detach(), event, data)
});
Ok(keyboard.detach())
}
/*
* Classic handling
*/
type KbdCallback = dyn FnMut(Event<'_>, wl_keyboard::WlKeyboard, wayland_client::DispatchData<'_>);
struct KbdHandler {
state: Rc<RefCell<KbState>>,
callback: Rc<RefCell<KbdCallback>>,
}
impl KbdHandler {
fn event(
&mut self,
kbd: wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
dispatch_data: wayland_client::DispatchData,
) {
use wl_keyboard::Event;
match event {
Event::Keymap { format, fd, size } => self.keymap(kbd, format, fd, size),
Event::Enter {
serial,
surface,
keys,
} => self.enter(kbd, serial, surface, keys, dispatch_data),
Event::Leave { serial, surface } => self.leave(kbd, serial, surface, dispatch_data),
Event::Key {
serial,
time,
key,
state,
} => self.key(kbd, serial, time, key, state, dispatch_data),
Event::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
..
} => self.modifiers(
kbd,
mods_depressed,
mods_latched,
mods_locked,
group,
dispatch_data,
),
Event::RepeatInfo { rate, delay } => self.repeat_info(kbd, rate, delay, dispatch_data),
_ => {}
}
}
fn keymap(
&mut self,
_: wl_keyboard::WlKeyboard,
format: wl_keyboard::KeymapFormat,
fd: RawFd,
size: u32, size: u32,
) { }
let mut state = self.state.borrow_mut();
if state.locked() { impl KeymapDescription {
// state is locked, ignore keymap updates fn new(fd: OwnedFd, size: u32) -> KeymapDescription {
return; KeymapDescription { fd, size }
} }
if state.ready() { }
// new keymap, we first deinit to free resources
unsafe { impl Clone for KeymapDescription {
state.de_init(); fn clone(&self) -> Self {
KeymapDescription::new(self.fd.try_clone().expect("unable to clone fd"), self.size)
}
}
pub struct Keyboard {
state: Option<KbState>,
desc: Option<KeymapDescription>,
}
impl Keyboard {
pub fn new() -> Keyboard {
Keyboard {
state: None,
desc: None,
} }
} }
pub fn keymap(&mut self, format: WEnum<wl_keyboard::KeymapFormat>, fd: OwnedFd, size: u32) {
match format { match format {
wl_keyboard::KeymapFormat::XkbV1 => unsafe { WEnum::Value(wl_keyboard::KeymapFormat::XkbV1) => {
state.init_with_fd(fd, size as usize); self.desc = Some(KeymapDescription::new(fd, size));
}, }
wl_keyboard::KeymapFormat::NoKeymap => { WEnum::Value(wl_keyboard::KeymapFormat::NoKeymap) => {
// TODO: how to handle this (hopefully never occuring) case? // TODO: how to handle this (hopefully never occuring) case?
} }
_ => unreachable!(), // Unknown format from a newer protocol version: ignore rather than
// abort the whole process.
_ => (),
} }
} }
fn enter( pub fn resolve(&mut self) {
&mut self, if self.state.is_some() {
object: wl_keyboard::WlKeyboard, return;
serial: u32, }
surface: wl_surface::WlSurface,
keys: Vec<u8>, if let Some(desc) = self.desc.take() {
dispatch_data: wayland_client::DispatchData, self.state = Some(
) { KbState::new_from_fd(desc.fd, desc.size as usize).expect("unable to load keymap"),
let mut state = self.state.borrow_mut();
let rawkeys = keys
.chunks_exact(4)
.map(NativeEndian::read_u32)
.collect::<Vec<_>>();
let keys: Vec<u32> = rawkeys.iter().map(|k| state.get_one_sym_raw(*k)).collect();
(&mut *self.callback.borrow_mut())(
Event::Enter {
serial,
surface,
rawkeys: &rawkeys,
keysyms: &keys,
},
object,
dispatch_data,
); );
} }
fn leave(
&mut self,
object: wl_keyboard::WlKeyboard,
serial: u32,
surface: wl_surface::WlSurface,
dispatch_data: wayland_client::DispatchData,
) {
(&mut *self.callback.borrow_mut())(Event::Leave { serial, surface }, object, dispatch_data);
} }
fn key( pub fn key(&mut self, key: u32, key_state: WEnum<wl_keyboard::KeyState>) -> KeyEvent {
&mut self, if let Some(state) = &mut self.state {
object: wl_keyboard::WlKeyboard,
serial: u32,
time: u32,
key: u32,
key_state: wl_keyboard::KeyState,
dispatch_data: wayland_client::DispatchData,
) {
let (sym, utf8, repeats) = { let (sym, utf8, repeats) = {
let mut state = self.state.borrow_mut();
// Get the values to generate a key event // Get the values to generate a key event
let sym = state.get_one_sym_raw(key); let sym = state.get_one_sym_raw(key);
let utf8 = if key_state == wl_keyboard::KeyState::Pressed { let utf8 = if key_state == WEnum::Value(wl_keyboard::KeyState::Pressed) {
match state.compose_feed(sym) { match state.compose_feed(sym) {
Some(ffi::xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED) => { Some(xkb::compose::FeedResult::Accepted) => {
if let Some(status) = state.compose_status() { if let Some(status) = state.compose_status() {
match status { match status {
ffi::xkb_compose_status::XKB_COMPOSE_COMPOSED => { xkb::compose::Status::Composed => state.compose_get_utf8(),
state.compose_get_utf8() xkb::compose::Status::Nothing => state.get_utf8_raw(key),
}
ffi::xkb_compose_status::XKB_COMPOSE_NOTHING => {
state.get_utf8_raw(key)
}
_ => None, _ => None,
} }
} else { } else {
@@ -287,58 +123,38 @@ impl KbdHandler {
} else { } else {
None None
}; };
let repeats = unsafe { state.key_repeats(key + 8) }; let repeats = state.key_repeats(xkb::Keycode::new(key + 8));
(sym, utf8, repeats) (sym, utf8, repeats)
}; };
KeyEvent {
(&mut *self.callback.borrow_mut())(
Event::Key {
serial,
time,
rawkey: key, rawkey: key,
keysym: sym, keysym: sym,
state: key_state, state: key_state,
modifiers: state.mods_state(),
utf8, utf8,
repeats, repeats,
}, }
object, } else {
dispatch_data, KeyEvent {
); rawkey: key,
keysym: 0,
state: key_state,
modifiers: Default::default(),
utf8: None,
repeats: false,
}
}
} }
fn modifiers( pub fn modifiers(
&mut self, &mut self,
object: wl_keyboard::WlKeyboard,
mods_depressed: u32, mods_depressed: u32,
mods_latched: u32, mods_latched: u32,
mods_locked: u32, mods_locked: u32,
group: u32, group: u32,
dispatch_data: wayland_client::DispatchData,
) { ) {
{ if let Some(state) = &mut self.state {
let mut state = self.state.borrow_mut();
state.update_modifiers(mods_depressed, mods_latched, mods_locked, group); state.update_modifiers(mods_depressed, mods_latched, mods_locked, group);
(&mut *self.callback.borrow_mut())(
Event::Modifiers {
modifiers: state.mods_state(),
},
object,
dispatch_data,
);
} }
} }
fn repeat_info(
&mut self,
object: wl_keyboard::WlKeyboard,
rate: i32,
delay: i32,
dispatch_data: wayland_client::DispatchData,
) {
(&mut *self.callback.borrow_mut())(
Event::RepeatInfo { rate, delay },
object,
dispatch_data,
);
}
} }
+182
View File
@@ -0,0 +1,182 @@
use std::time::Duration;
use calloop::{
EventSource, Poll, PostAction, Readiness, Token, TokenFactory,
channel::{self, Channel},
timer::{TimeoutAction, Timer},
};
use wayland_client::{WEnum, protocol::wl_keyboard};
use super::KeyEvent;
#[derive(Debug)]
pub enum RepeatMessage {
StopRepeat,
KeyEvent(KeyEvent),
RepeatInfo((u32, u32)),
}
#[derive(Debug)]
pub struct KeyRepeatSource {
channel: Channel<RepeatMessage>,
timer: Timer,
rate: Duration,
delay: Duration,
disabled: bool,
key: Option<KeyEvent>,
}
impl KeyRepeatSource {
pub fn new(ch: channel::Channel<RepeatMessage>) -> KeyRepeatSource {
KeyRepeatSource {
channel: ch,
timer: Timer::immediate(),
delay: Duration::ZERO,
rate: Duration::ZERO,
disabled: true,
key: None,
}
}
}
impl EventSource for KeyRepeatSource {
type Event = KeyEvent;
type Metadata = ();
type Ret = ();
type Error = calloop::Error;
fn process_events<F>(
&mut self,
readiness: Readiness,
token: Token,
mut callback: F,
) -> calloop::Result<PostAction>
where
F: FnMut(Self::Event, &mut Self::Metadata) -> Self::Ret,
{
let mut removed = false;
let timer = &mut self.timer;
let rate = &mut self.rate;
let delay = &mut self.delay;
let key = &mut self.key;
let disabled = &mut self.disabled;
let mut reregister = false;
let now = std::time::Instant::now();
// Check if the key repeat should stop
let channel_pa = self
.channel
.process_events(readiness, token, |event, _| match event {
channel::Event::Msg(message) => match message {
RepeatMessage::StopRepeat => {
key.take();
}
RepeatMessage::KeyEvent(event) => match event.state {
WEnum::Value(wl_keyboard::KeyState::Pressed) => {
key.replace(event);
reregister = true;
timer.set_deadline(now + *delay);
}
WEnum::Value(wl_keyboard::KeyState::Released) => match key {
Some(k) if k.keysym == event.keysym => {
key.take();
}
_ => (),
},
_ => (),
},
RepeatMessage::RepeatInfo((new_rate, new_delay)) => {
// Per wl_keyboard.repeat_info, a rate of 0 disables
// key repeat entirely. Dividing by it would panic.
if new_rate == 0 {
*disabled = true;
key.take();
} else {
*rate = Duration::from_micros(1_000_000 / new_rate as u64);
*delay = Duration::from_millis(new_delay as u64);
*disabled = false;
if key.is_some() {
timer.set_deadline(now + *delay);
}
}
}
},
channel::Event::Closed => {
removed = true;
}
})
.map_err(|err| calloop::Error::OtherError(Box::new(err)))?;
// Keyboard was destroyed
if removed {
return Ok(PostAction::Remove);
}
// Re-register the timer to start it again
if reregister {
return Ok(PostAction::Reregister);
}
let timer_pa = timer.process_events(readiness, token, |deadline, _| {
if self.disabled || key.is_none() {
return TimeoutAction::Drop;
}
if deadline - now > std::time::Duration::from_millis(5) {
// We have not yet reached the deadline within our tolerance
return TimeoutAction::ToInstant(deadline);
}
// Invoke the event
callback(key.as_ref().unwrap().clone(), &mut ());
let mut next_deadline = deadline;
if now - deadline > *rate * 2 {
// We're way behind, let's just set a new time
println!(
"KeyRepeatSource::process_events: More than two repeat cycles behind, resetting"
);
next_deadline = now + *rate;
} else {
while next_deadline < now {
next_deadline += *rate;
}
}
TimeoutAction::ToInstant(next_deadline)
})?;
// Only disable or remove if both want to, otherwise continue or re-register
Ok(match (timer_pa, channel_pa) {
(PostAction::Disable, PostAction::Disable) => PostAction::Disable,
(PostAction::Remove, PostAction::Remove) => PostAction::Remove,
(PostAction::Reregister, _) | (_, PostAction::Reregister) => PostAction::Reregister,
_ => PostAction::Continue,
})
}
fn register(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.register(poll, token_factory)?;
self.timer.register(poll, token_factory)
}
fn reregister(
&mut self,
poll: &mut Poll,
token_factory: &mut TokenFactory,
) -> calloop::Result<()> {
self.channel.reregister(poll, token_factory)?;
self.timer.reregister(poll, token_factory)
}
fn unregister(&mut self, poll: &mut Poll) -> calloop::Result<()> {
self.channel.unregister(poll)?;
self.timer.unregister(poll)
}
}
+74 -313
View File
@@ -1,48 +1,14 @@
use std::{ use std::{env, os::unix::io::OwnedFd};
env,
ffi::CString,
fs::File,
os::raw::c_char,
os::unix::{
ffi::OsStringExt,
io::{FromRawFd, RawFd},
},
ptr,
};
use memmap::MmapOptions; use xkbcommon::xkb;
use super::ffi::{self, xkb_state_component, XKBCOMMON_HANDLE as XKBH};
use super::Error;
pub struct KbState { pub struct KbState {
xkb_context: *mut ffi::xkb_context, xkb_context: xkb::Context,
xkb_keymap: *mut ffi::xkb_keymap, xkb_keymap: xkb::Keymap,
xkb_state: *mut ffi::xkb_state, xkb_state: xkb::State,
xkb_compose_table: *mut ffi::xkb_compose_table, xkb_compose_table: Option<xkb::compose::Table>,
xkb_compose_state: *mut ffi::xkb_compose_state, xkb_compose_state: Option<xkb::compose::State>,
mods_state: ModifiersState, mods_state: ModifiersState,
locked: bool,
}
/// The RMLVO description of a keymap
///
/// All fields are optional, and the system default
/// will be used if set to `None`.
pub struct RMLVO {
/// The rules file to use
pub rules: Option<String>,
/// The keyboard model by which to interpret keycodes and LEDs
pub model: Option<String>,
/// A comma separated list of layouts (languages) to include in the keymap
pub layout: Option<String>,
/// A comma separated list of variants, one per layout, which may modify or
/// augment the respective layout in various ways
pub variant: Option<String>,
/// A comma separated list of options, through which the user specifies
/// non-layout related preferences, like which key combinations are
/// used for switching layouts, or which key is the Compose key.
pub options: Option<String>,
} }
/// Represents the current state of the keyboard modifiers /// Represents the current state of the keyboard modifiers
@@ -74,52 +40,19 @@ impl ModifiersState {
ModifiersState::default() ModifiersState::default()
} }
fn update_with(&mut self, state: *mut ffi::xkb_state) { fn update_with(&mut self, state: &xkb::State) {
self.ctrl = unsafe { self.ctrl = state.mod_name_is_active("Control", xkb::STATE_MODS_EFFECTIVE);
(XKBH.xkb_state_mod_name_is_active)( self.alt = state.mod_name_is_active("Mod1", xkb::STATE_MODS_EFFECTIVE);
state, self.shift = state.mod_name_is_active("Shift", xkb::STATE_MODS_EFFECTIVE);
ffi::XKB_MOD_NAME_CTRL.as_ptr() as *const c_char, self.caps_lock = state.mod_name_is_active("Lock", xkb::STATE_MODS_EFFECTIVE);
xkb_state_component::XKB_STATE_MODS_EFFECTIVE, self.num_lock = state.mod_name_is_active("Mod2", xkb::STATE_MODS_EFFECTIVE);
) > 0 self.logo = state.mod_name_is_active("Mod4", xkb::STATE_MODS_EFFECTIVE);
};
self.alt = unsafe {
(XKBH.xkb_state_mod_name_is_active)(
state,
ffi::XKB_MOD_NAME_ALT.as_ptr() as *const c_char,
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
) > 0
};
self.shift = unsafe {
(XKBH.xkb_state_mod_name_is_active)(
state,
ffi::XKB_MOD_NAME_SHIFT.as_ptr() as *const c_char,
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
) > 0
};
self.caps_lock = unsafe {
(XKBH.xkb_state_mod_name_is_active)(
state,
ffi::XKB_MOD_NAME_CAPS.as_ptr() as *const c_char,
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
) > 0
};
self.logo = unsafe {
(XKBH.xkb_state_mod_name_is_active)(
state,
ffi::XKB_MOD_NAME_LOGO.as_ptr() as *const c_char,
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
) > 0
};
self.num_lock = unsafe {
(XKBH.xkb_state_mod_name_is_active)(
state,
ffi::XKB_MOD_NAME_NUM.as_ptr() as *const c_char,
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
) > 0
};
} }
} }
// Safety: No.
unsafe impl Send for KbState {}
impl KbState { impl KbState {
pub(crate) fn update_modifiers( pub(crate) fn update_modifiers(
&mut self, &mut self,
@@ -128,257 +61,97 @@ impl KbState {
mods_locked: u32, mods_locked: u32,
group: u32, group: u32,
) { ) {
if !self.ready() { let mask =
return; self.xkb_state
} .update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
let mask = unsafe { if (mask & xkb::STATE_MODS_EFFECTIVE) > 0 {
(XKBH.xkb_state_update_mask)(
self.xkb_state,
mods_depressed,
mods_latched,
mods_locked,
0,
0,
group,
)
};
if mask.contains(xkb_state_component::XKB_STATE_MODS_EFFECTIVE) {
// effective value of mods have changed, we need to update our state // effective value of mods have changed, we need to update our state
self.mods_state.update_with(self.xkb_state); self.mods_state.update_with(&self.xkb_state);
} }
} }
pub(crate) fn get_one_sym_raw(&mut self, keycode: u32) -> u32 { pub(crate) fn get_one_sym_raw(&mut self, keycode: u32) -> u32 {
if !self.ready() { self.xkb_state
return 0; .key_get_one_sym(xkb::Keycode::new(keycode + 8))
} .raw()
unsafe { (XKBH.xkb_state_key_get_one_sym)(self.xkb_state, keycode + 8) }
} }
pub(crate) fn get_utf8_raw(&mut self, keycode: u32) -> Option<String> { pub(crate) fn get_utf8_raw(&mut self, keycode: u32) -> Option<String> {
if !self.ready() { Some(self.xkb_state.key_get_utf8(xkb::Keycode::new(keycode + 8)))
return None;
}
let size = unsafe {
(XKBH.xkb_state_key_get_utf8)(self.xkb_state, keycode + 8, ptr::null_mut(), 0)
} + 1;
if size <= 1 {
return None;
};
let mut buffer = Vec::with_capacity(size as usize);
unsafe {
buffer.set_len(size as usize);
(XKBH.xkb_state_key_get_utf8)(
self.xkb_state,
keycode + 8,
buffer.as_mut_ptr() as *mut _,
size as usize,
);
};
// remove the final `\0`
buffer.pop();
// libxkbcommon will always provide valid UTF8
Some(unsafe { String::from_utf8_unchecked(buffer) })
} }
pub(crate) fn compose_feed(&mut self, keysym: u32) -> Option<ffi::xkb_compose_feed_result> { pub(crate) fn compose_feed(&mut self, keysym: u32) -> Option<xkb::compose::FeedResult> {
if !self.ready() || self.xkb_compose_state.is_null() { self.xkb_compose_state
return None; .as_mut()
} .map(|compose_state| compose_state.feed(xkb::Keysym::new(keysym)))
Some(unsafe { (XKBH.xkb_compose_state_feed)(self.xkb_compose_state, keysym) })
} }
pub(crate) fn compose_status(&mut self) -> Option<ffi::xkb_compose_status> { pub(crate) fn compose_status(&mut self) -> Option<xkb::compose::Status> {
if !self.ready() || self.xkb_compose_state.is_null() { self.xkb_compose_state
return None; .as_mut()
} .map(|compose_state| compose_state.status())
Some(unsafe { (XKBH.xkb_compose_state_get_status)(self.xkb_compose_state) })
} }
pub(crate) fn compose_get_utf8(&mut self) -> Option<String> { pub(crate) fn compose_get_utf8(&mut self) -> Option<String> {
if !self.ready() || self.xkb_compose_state.is_null() { match &mut self.xkb_compose_state {
return None; Some(compose_state) => compose_state.utf8(),
None => None,
} }
let size = unsafe {
(XKBH.xkb_compose_state_get_utf8)(self.xkb_compose_state, ptr::null_mut(), 0)
} + 1;
if size <= 1 {
return None;
};
let mut buffer = Vec::with_capacity(size as usize);
unsafe {
buffer.set_len(size as usize);
(XKBH.xkb_compose_state_get_utf8)(
self.xkb_compose_state,
buffer.as_mut_ptr() as *mut _,
size as usize,
);
};
// remove the final `\0`
buffer.pop();
// libxkbcommon will always provide valid UTF8
Some(unsafe { String::from_utf8_unchecked(buffer) })
} }
pub(crate) fn new() -> Result<KbState, Error> { pub(crate) fn new_from_fd(fd: OwnedFd, size: usize) -> Result<KbState, std::io::Error> {
let xkbh = match ffi::XKBCOMMON_OPTION.as_ref() { let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
Some(h) => h, let xkb_keymap = unsafe {
None => return Err(Error::XKBNotFound), xkb::Keymap::new_from_fd(
&xkb_context,
fd,
size,
xkb::KEYMAP_FORMAT_TEXT_V1,
xkb::KEYMAP_COMPILE_NO_FLAGS,
)?
.unwrap()
}; };
let xkb_context = let xkb_state = xkb::State::new(&xkb_keymap);
unsafe { (xkbh.xkb_context_new)(ffi::xkb_context_flags::XKB_CONTEXT_NO_FLAGS) };
if xkb_context.is_null() {
return Err(Error::XKBNotFound);
}
let mut me = KbState { let mut me = KbState {
xkb_context, xkb_context,
xkb_keymap: ptr::null_mut(), xkb_keymap,
xkb_state: ptr::null_mut(), xkb_state,
xkb_compose_table: ptr::null_mut(), xkb_compose_table: None,
xkb_compose_state: ptr::null_mut(), xkb_compose_state: None,
mods_state: ModifiersState::new(), mods_state: ModifiersState::new(),
locked: false,
}; };
unsafe {
me.init_compose(); me.init_compose();
}
Ok(me) Ok(me)
} }
pub(crate) fn from_rmlvo(rmlvo: RMLVO) -> Result<KbState, Error> { pub(crate) fn init_compose(&mut self) {
fn to_cstring(s: Option<String>) -> Result<Option<CString>, Error> { let locale = env::var_os("LC_ALL")
s.map_or(Ok(None), |s| CString::new(s).map(Option::Some)) .filter(|v| !v.is_empty())
.map_err(|_| Error::BadNames) .or_else(|| env::var_os("LC_CTYPE"))
} .filter(|v| !v.is_empty())
.or_else(|| env::var_os("LANG"))
.filter(|v| !v.is_empty())
.unwrap_or_else(|| "C".into());
let mut state = KbState::new()?; let compose_table = match xkb::compose::Table::new_from_locale(
&self.xkb_context,
let rules = to_cstring(rmlvo.rules)?; &locale,
let model = to_cstring(rmlvo.model)?; xkb::compose::COMPILE_NO_FLAGS,
let layout = to_cstring(rmlvo.layout)?; ) {
let variant = to_cstring(rmlvo.variant)?; Ok(t) => t,
let options = to_cstring(rmlvo.options)?; Err(_) => return,
let xkb_names = ffi::xkb_rule_names {
rules: rules.map_or(ptr::null(), |s| s.as_ptr()),
model: model.map_or(ptr::null(), |s| s.as_ptr()),
layout: layout.map_or(ptr::null(), |s| s.as_ptr()),
variant: variant.map_or(ptr::null(), |s| s.as_ptr()),
options: options.map_or(ptr::null(), |s| s.as_ptr()),
}; };
unsafe { let compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
state.init_with_rmlvo(xkb_names)?;
self.xkb_compose_table = Some(compose_table);
self.xkb_compose_state = Some(compose_state);
} }
state.locked = true; pub(crate) fn key_repeats(&mut self, keycode: xkb::Keycode) -> bool {
Ok(state) self.xkb_keymap.key_repeats(keycode)
}
pub(crate) unsafe fn init_compose(&mut self) {
let locale = env::var_os("LC_ALL")
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.or_else(|| env::var_os("LC_CTYPE"))
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.or_else(|| env::var_os("LANG"))
.and_then(|v| if v.is_empty() { None } else { Some(v) })
.unwrap_or_else(|| "C".into());
let locale = CString::new(locale.into_vec()).unwrap();
let compose_table = (XKBH.xkb_compose_table_new_from_locale)(
self.xkb_context,
locale.as_ptr(),
ffi::xkb_compose_compile_flags::XKB_COMPOSE_COMPILE_NO_FLAGS,
);
if compose_table.is_null() {
// init of compose table failed, continue without compose
return;
}
let compose_state = (XKBH.xkb_compose_state_new)(
compose_table,
ffi::xkb_compose_state_flags::XKB_COMPOSE_STATE_NO_FLAGS,
);
if compose_state.is_null() {
// init of compose state failed, continue without compose
(XKBH.xkb_compose_table_unref)(compose_table);
return;
}
self.xkb_compose_table = compose_table;
self.xkb_compose_state = compose_state;
}
pub(crate) unsafe fn post_init(&mut self, xkb_keymap: *mut ffi::xkb_keymap) {
let xkb_state = (XKBH.xkb_state_new)(xkb_keymap);
self.xkb_keymap = xkb_keymap;
self.xkb_state = xkb_state;
self.mods_state.update_with(xkb_state);
}
pub(crate) unsafe fn de_init(&mut self) {
(XKBH.xkb_state_unref)(self.xkb_state);
self.xkb_state = ptr::null_mut();
(XKBH.xkb_keymap_unref)(self.xkb_keymap);
self.xkb_keymap = ptr::null_mut();
}
pub(crate) unsafe fn init_with_fd(&mut self, fd: RawFd, size: usize) {
let map = MmapOptions::new()
.len(size)
.map(&File::from_raw_fd(fd))
.unwrap();
let xkb_keymap = (XKBH.xkb_keymap_new_from_string)(
self.xkb_context,
map.as_ptr() as *const _,
ffi::xkb_keymap_format::XKB_KEYMAP_FORMAT_TEXT_V1,
ffi::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS,
);
if xkb_keymap.is_null() {
panic!("Received invalid keymap from compositor.");
}
self.post_init(xkb_keymap);
}
pub(crate) unsafe fn init_with_rmlvo(
&mut self,
names: ffi::xkb_rule_names,
) -> Result<(), Error> {
let xkb_keymap = (XKBH.xkb_keymap_new_from_names)(
self.xkb_context,
&names,
ffi::xkb_keymap_compile_flags::XKB_KEYMAP_COMPILE_NO_FLAGS,
);
if xkb_keymap.is_null() {
return Err(Error::BadNames);
}
self.post_init(xkb_keymap);
Ok(())
}
pub(crate) unsafe fn key_repeats(&mut self, xkb_keycode_t: ffi::xkb_keycode_t) -> bool {
(XKBH.xkb_keymap_key_repeats)(self.xkb_keymap, xkb_keycode_t) == 1
}
#[inline]
pub(crate) fn ready(&self) -> bool {
!self.xkb_state.is_null()
}
#[inline]
pub(crate) fn locked(&self) -> bool {
self.locked
} }
#[inline] #[inline]
@@ -386,15 +159,3 @@ impl KbState {
self.mods_state self.mods_state
} }
} }
impl Drop for KbState {
fn drop(&mut self) {
unsafe {
(XKBH.xkb_compose_state_unref)(self.xkb_compose_state);
(XKBH.xkb_compose_table_unref)(self.xkb_compose_table);
(XKBH.xkb_state_unref)(self.xkb_state);
(XKBH.xkb_keymap_unref)(self.xkb_keymap);
(XKBH.xkb_context_unref)(self.xkb_context);
}
}
}
+348 -345
View File
@@ -1,389 +1,392 @@
use std::env;
use std::fs::File;
use std::io::{BufRead, BufReader, Read, Write};
use std::os::unix::io::AsRawFd;
use std::os::unix::net::{UnixListener, UnixStream};
use std::{collections::HashMap, sync::mpsc::channel};
use chrono::{Duration, Local};
use nix::poll::{poll, PollFd, PollFlags};
use os_pipe::pipe;
use timerfd::{SetTimeFlags, TimerFd, TimerState};
mod app;
mod buffer; mod buffer;
mod cmd;
mod color; mod color;
mod config; mod config;
mod configfmt;
mod data;
mod desktop;
mod doublemempool;
mod draw; mod draw;
mod event;
mod fonts; mod fonts;
mod keyboard; mod keyboard;
mod widget; mod state;
mod utils;
mod widgets; mod widgets;
#[macro_use] use wayland_client::{Connection, QueueHandle, WaylandSource};
extern crate dlib;
use app::{App, OutputMode}; use calloop::{
use cmd::Cmd; EventLoop,
timer::{TimeoutAction, Timer},
};
use chrono::{Duration, Local, Timelike};
use buffer::BufferView;
use config::Config; use config::Config;
use configfmt::ConfigFmt; use event::{Event, Events};
use fonts::{FontLoader, FontMap, FontSeeker}; use fonts::{FontMap, MaybeFontMap};
use widget::WaitContext; use keyboard::{KeyRepeatSource, RepeatMessage};
use state::State;
use utils::xdg;
use widgets::{Geometry, Widget};
enum Mode { use std::{
Start, env, fs,
Daemonize, fs::File,
StartOrKill, io::{BufRead, BufReader, Write},
ToggleVisible, os::unix::net::{UnixListener, UnixStream},
PrintConfig(ConfigFmt), path::Path,
rc::Rc,
thread,
};
fn print_usage() {
println!(
"
usage: wldash [OPTIONS]
OPTIONS:
--config CONFIG_FILE use the specified config file
(/etc/wldash/config.yml if present,
else ~/.config/wldash/config.yml)
--write-default-config [v1|v2|toplevel] generate and write a new config file
(v2 by default)
--desktop-refresh no-op kept for old key bindings
(desktop files are scanned at start)
"
);
}
fn die(msg: String) -> ! {
eprintln!("wldash: {}", msg);
std::process::exit(1);
}
fn ensure_dir_exists(path: &str) {
if let Err(err) = fs::create_dir_all(path) {
die(format!("error creating directory {}: {}", path, err));
}
}
fn create_required_directories() {
let dir_config_wldash = format!("{}/wldash", xdg::config_folder());
ensure_dir_exists(&dir_config_wldash);
}
/// Default config path. On a shared (e.g. NFS-roamed) home, a host-specific
/// config at /etc/wldash/config.yml takes precedence over the roamed
/// ~/.config/wldash/config.yml, so each machine can keep its own layout
/// without conflicting. An explicit --config still overrides both.
fn default_config_file() -> String {
let etc = "/etc/wldash/config.yml";
if Path::new(etc).exists() {
return etc.to_string();
}
format!("{}/wldash/config.yml", xdg::config_folder())
} }
fn main() { fn main() {
let mut args = env::args();
// Skip program name
_ = args.next();
create_required_directories();
let mut config_file = default_config_file();
loop {
match args.next() {
Some(ref s) if s == "--config" => match args.next() {
Some(ref s) => config_file = s.clone(),
None => die("missing argument to --config".to_string()),
},
Some(ref s) if s == "--write-default-config" => {
let config = match args.next() {
Some(ref s) if s == "v1" => Config::generate_v1(),
Some(ref s) if s == "v2" => Config::generate_v2(false),
Some(ref s) if s == "toplevel" => Config::generate_v2(true),
None => Config::generate_v2(false),
Some(s) => die(format!("unknown argument to --write-default-config: {}", s)),
};
match File::create(&config_file) {
Ok(f) => {
if let Err(e) = serde_yaml::to_writer(f, &config) {
die(format!("could not serialize config: {}", e));
}
}
Err(e) => die(format!("could not write {}: {}", config_file, e)),
}
std::process::exit(0);
}
Some(ref s) if s == "--desktop-refresh" => {
eprintln!("wldash: desktop files are scanned at every start; nothing to refresh");
std::process::exit(0);
}
Some(ref s) if s == "--help" => {
print_usage();
std::process::exit(0);
}
Some(_) => {
print_usage();
std::process::exit(1);
}
None => break,
}
}
let conn = match Connection::connect_to_env() {
Ok(c) => c,
Err(e) => die(format!("could not connect to the Wayland display: {}", e)),
};
let event_queue = conn.new_event_queue();
let qhandle: QueueHandle<State> = event_queue.handle();
let display = conn.display();
display.get_registry(&qhandle, ());
display.sync(&qhandle, ());
let (ping_sender, ping_source) = calloop::ping::make_ping().unwrap();
let (keyrepeat_sender, keyrepeat_channel) = calloop::channel::channel::<RepeatMessage>();
let mut event_loop: EventLoop<State> =
EventLoop::try_new().expect("Failed to initialize the event loop!");
let handle = event_loop.handle();
handle
.insert_source(
WaylandSource::new(event_queue).expect("Could not create WaylandSource!"),
|_event, queue, state| queue.dispatch_pending(state),
)
.expect("Failed to insert event source!");
handle
.insert_source(ping_source, |(), &mut (), state| {
let events = state.events.lock().unwrap().flush();
for widget in state.widgets.iter_mut() {
for event in events.iter() {
widget.event(event);
}
}
state.dirty = true;
})
.expect("Failed to insert ping source!");
let clock_source = Timer::from_duration(std::time::Duration::from_secs(1));
handle
.insert_source(clock_source, |_event, _metadata, state| {
let now = Local::now().naive_local();
let target = (now + Duration::seconds(60))
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
let d = target - now;
for widget in state.widgets.iter_mut() {
widget.event(&Event::NewMinute);
}
state.dirty = true;
// The timer event source requires us to return a TimeoutAction to
// specify if the timer should be rescheduled. In our case we just drop it.
TimeoutAction::ToDuration(d.to_std().unwrap())
})
.expect("Failed to insert event source!");
let keyrepeat_source = KeyRepeatSource::new(keyrepeat_channel);
handle
.insert_source(keyrepeat_source, |event, _metadata, state| {
let ev = Event::KeyEvent(event);
for widget in state.widgets.iter_mut() {
widget.event(&ev);
}
state.dirty = true;
})
.expect("Failed to insert keyrepeat source!");
let socket_path = match env::var("XDG_RUNTIME_DIR") { let socket_path = match env::var("XDG_RUNTIME_DIR") {
Ok(dir) => dir + "/wldash", Ok(dir) => dir + "/wldash",
Err(_) => "/tmp/wldash".to_string(), Err(_) => "/tmp/wldash".to_string(),
}; };
let config_home = match env::var("XDG_CONFIG_HOME") {
Ok(dir) => dir + "/wldash", let events = Events::new(ping_sender);
Err(_) => match env::var("HOME") {
Ok(home) => home + "/.config/wldash", let mut fm = FontMap::new();
_ => panic!("unable to find user folder"),
let config: Config = match File::open(&config_file) {
Ok(f) => match serde_yaml::from_reader(f) {
Ok(c) => c,
Err(e) => die(format!("invalid configuration {}: {}", config_file, e)),
}, },
Err(e) => die(format!(
"could not open configuration {}: {} (try 'wldash --write-default-config')",
config_file, e
)),
}; };
// From all existing files take the first readable one and write it's extension to `ext` if let Some(true) = config.server {
let mut ext = [0x0; 8];
let file = configfmt::CONFIG_NAMES
.iter()
.map(|name| std::path::Path::new(&config_home).join(name))
.filter_map(|path| {
match File::open(&path) {
Ok(file) => {
let e = path.extension().and_then(|e| e.to_str())?;
let len = e.len();
let from = len.saturating_sub(8); // the longest possible extension
ext[0..len - from].copy_from_slice(&e.as_bytes()[from..]);
Some(file)
}
Err(_) => None,
}
})
.next();
let fmt = std::str::from_utf8(&ext)
.ok()
.and_then(ConfigFmt::new)
.unwrap_or_default();
let config: Config = file
.map(|f| fmt.from_reader(BufReader::new(f)))
.unwrap_or_default();
let scale = config.scale;
let fonts: FontMap = {
let load_font = |font_name| {
let path = FontSeeker::from_string(font_name);
FontLoader::from_path(&path).expect(&format!("Loading {} failed", path.display()))
};
config
.fonts
.iter()
.map(|(key, val)| (key.clone(), load_font(val)))
.collect::<HashMap<_, _>>()
};
let mut args = env::args();
let _ = args.next();
let mode = match args.next() {
Some(arg) => match arg.as_str() {
"start" => Mode::Daemonize,
"start-or-kill" => Mode::StartOrKill,
"toggle-visible" => Mode::ToggleVisible,
"print-config" => Mode::PrintConfig(fmt),
s => {
let p = "print-config-";
let l = p.len();
let fmt = if s.starts_with(p) {
ConfigFmt::new(&s[l..])
} else {
None
};
if let Some(fmt) = fmt {
Mode::PrintConfig(fmt)
} else {
eprintln!("unsupported sub-command {}", s);
std::process::exit(1);
}
}
},
None => Mode::Start,
};
if args.next().is_some() {
// total = args.count + 2 (the two we skipped + the rest)
eprintln!("expected 0 or 1 arguments, got {}", args.count() + 2);
std::process::exit(1);
}
let mut daemon = false;
match mode {
Mode::ToggleVisible => {
if let Ok(mut socket) = UnixStream::connect(socket_path) {
socket.write_all(b"toggle_visible\n").unwrap();
return;
};
eprintln!("wldash is not running");
std::process::exit(1);
}
Mode::StartOrKill => {
if let Ok(mut socket) = UnixStream::connect(socket_path.clone()) { if let Ok(mut socket) = UnixStream::connect(socket_path.clone()) {
socket.write_all(b"kill\n").unwrap(); socket.write_all(b"kill\n").unwrap();
return; return;
}; };
}
Mode::Start => {
if UnixStream::connect(socket_path.clone()).is_ok() {
eprintln!("wldash is already running");
std::process::exit(1);
};
}
Mode::Daemonize => {
if UnixStream::connect(socket_path.clone()).is_ok() {
eprintln!("wldash is already running");
std::process::exit(1);
};
daemon = true;
}
Mode::PrintConfig(fmt) => {
println!("{}", fmt.to_string(&config));
std::process::exit(0);
}
}
let _ = std::fs::remove_file(socket_path.clone()); let _ = std::fs::remove_file(socket_path.clone());
let listener = UnixListener::bind(socket_path.clone()).unwrap(); let _ = thread::Builder::new()
.name("ipc_server".to_string())
let output_mode = match config.output_mode {
config::OutputMode::All => OutputMode::All,
config::OutputMode::Active => OutputMode::Active,
};
let background = config.background;
let (tx_draw, rx_draw) = channel();
let tx_draw_mod = tx_draw.clone();
// Print, write to a file, or send to an HTTP server.
let widget = config
.widget
.construct(Local::now().naive_local(), tx_draw_mod, &fonts)
.expect("no widget configured");
let mut app = App::new(tx_draw, output_mode, background, scale);
if daemon {
app.hide();
} else {
app.show();
}
app.set_widget(widget).unwrap();
let (mut rx_pipe, mut tx_pipe) = pipe().unwrap();
let ipc_pipe = tx_pipe.try_clone().unwrap();
let worker_queue = app.cmd_queue();
let _ = std::thread::Builder::new()
.name("cmd_proxy".to_string())
.spawn(move || loop {
let cmd = rx_draw.recv().unwrap();
worker_queue.lock().unwrap().push_back(cmd);
tx_pipe.write_all(&[0x1]).unwrap();
});
let mut timer = TimerFd::new().unwrap();
let ev_fd = PollFd::new(
app.event_queue().display().get_connection_fd(),
PollFlags::POLLIN,
);
let rx_fd = PollFd::new(rx_pipe.as_raw_fd(), PollFlags::POLLIN);
let tm_fd = PollFd::new(timer.as_raw_fd(), PollFlags::POLLIN);
let ipc_fd = PollFd::new(listener.as_raw_fd(), PollFlags::POLLIN);
app.cmd_queue().lock().unwrap().push_back(Cmd::Draw);
let mut visible = !daemon;
let mut wait_ctx = WaitContext {
fds: Vec::new(),
target_time: None,
};
let q = app.cmd_queue();
loop {
let cmd = q.lock().unwrap().pop_front();
match cmd {
Some(cmd) => match cmd {
Cmd::Draw => {
app.redraw(false).expect("Failed to draw");
app.flush_display();
}
Cmd::KeyboardTest => {
if let Some(cmd) = app.key_repeat() {
q.lock().unwrap().push_back(cmd);
}
}
Cmd::ForceDraw => {
app.redraw(true).expect("Failed to draw");
app.flush_display();
}
Cmd::MouseClick { btn, pos } => {
app.get_widget().mouse_click(btn, pos);
q.lock().unwrap().push_back(Cmd::Draw);
}
Cmd::MouseScroll { scroll, pos } => {
app.get_widget().mouse_scroll(scroll, pos);
q.lock().unwrap().push_back(Cmd::Draw);
}
Cmd::Keyboard {
key,
key_state,
modifiers_state,
interpreted,
} => {
app.get_widget()
.keyboard_input(key, modifiers_state, key_state, interpreted);
q.lock().unwrap().push_back(Cmd::Draw);
}
Cmd::ToggleVisible => {
visible = !visible;
if visible {
app.get_widget().enter();
app.show();
} else {
app.hide();
app.get_widget().leave();
}
app.flush_display();
}
Cmd::Exit => {
if daemon {
visible = false;
app.hide();
app.get_widget().leave();
app.flush_display();
} else {
let _ = std::fs::remove_file(socket_path);
return;
}
}
},
None => {
app.flush_display();
wait_ctx.fds.clear();
wait_ctx.fds.push(ev_fd);
wait_ctx.fds.push(rx_fd);
wait_ctx.fds.push(ipc_fd);
wait_ctx.target_time = None;
app.get_widget().wait(&mut wait_ctx);
app.set_keyboard_repeat(&mut wait_ctx);
if let Some(target_time) = wait_ctx.target_time {
let n = Local::now().naive_local();
let sleep = if target_time > n {
target_time - n
} else {
Duration::seconds(0)
};
timer.set_state(
TimerState::Oneshot(sleep.to_std().unwrap()),
SetTimeFlags::Default,
);
wait_ctx.fds.push(tm_fd);
}
poll(&mut wait_ctx.fds, -1).unwrap();
if wait_ctx.fds[0]
.revents()
.unwrap()
.contains(PollFlags::POLLIN)
{
if let Some(guard) = app.event_queue().prepare_read() {
if let Err(e) = guard.read_events() {
if e.kind() != ::std::io::ErrorKind::WouldBlock {
eprintln!(
"Error while trying to read from the wayland socket: {:?}",
e
);
}
}
}
app.event_queue()
.dispatch_pending(&mut (), |_, _, _| {})
.expect("Failed to dispatch all messages.");
}
if wait_ctx.fds[1]
.revents()
.unwrap()
.contains(PollFlags::POLLIN)
{
let mut v = [0x00];
rx_pipe.read_exact(&mut v).unwrap();
}
if wait_ctx.fds[2]
.revents()
.unwrap()
.contains(PollFlags::POLLIN)
{
if let Ok((stream, _)) = listener.accept() {
let client_queue = q.clone();
let mut client_pipe = ipc_pipe.try_clone().unwrap();
let _ = std::thread::Builder::new()
.name("ipc_client".to_string())
.spawn(move || { .spawn(move || {
let listener = UnixListener::bind(socket_path.clone()).unwrap();
loop {
if let Ok((stream, _)) = listener.accept() {
let _ = thread::Builder::new().name("ipc_client".to_string()).spawn(
move || {
let r = BufReader::new(stream); let r = BufReader::new(stream);
for line in r.lines() { for line in r.lines() {
match line { match line {
Ok(v) => match v.as_str() { Ok(ref cmd) if cmd == "kill" => {
"kill" => { std::process::exit(0);
client_queue.lock().unwrap().push_back(Cmd::Exit) }
_ => {}
}
} }
"toggle_visible" => client_queue
.lock()
.unwrap()
.push_back(Cmd::ToggleVisible),
v => eprintln!("unknown command: {}", v),
}, },
Err(_) => return, );
} }
client_pipe.write_all(&[0x1]).unwrap();
} }
}); });
} }
if let Some(font_paths) = config.font_paths {
for (key, value) in font_paths.into_iter() {
fm.add_font_path(Box::leak(key.into_boxed_str()), value);
}
} }
if wait_ctx.target_time.is_some() let layout = Rc::new(config.widget.construct_layout(&mut 0));
&& wait_ctx.fds[wait_ctx.fds.len() - 1] let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
.revents() config
.unwrap() .widget
.contains(PollFlags::POLLIN) .construct_widgets(&mut widgets, &mut fm, &events);
{
timer.read(); let font_thread = thread::Builder::new()
let mut qq = q.lock().unwrap(); .name("fontloader".to_string())
qq.push_back(Cmd::KeyboardTest); .spawn(move || {
qq.push_back(Cmd::Draw); fm.load_fonts();
fm
})
.unwrap();
let mut state = State::new(
config.mode,
config.background,
widgets,
layout,
MaybeFontMap::Waiting(font_thread),
events,
keyrepeat_sender,
);
// Initial setup
//
while state.running && !state.ready {
event_loop
.dispatch(None, &mut state)
.expect("Could not dispatch event loop");
}
state.check_registry(&qhandle);
while state.running {
event_loop
.dispatch(None, &mut state)
.expect("Could not dispatch event loop");
if !state.configured || !state.dirty || !state.ready {
continue;
}
let mut force = false;
if state.bufmgr.buffers.is_empty() {
state.add_buffer(&qhandle);
force = true;
}
let mut bufcnt = state.bufmgr.buffers.len();
let buf = match state.bufmgr.next_buffer() {
Some(b) => b,
None => {
if state.bufmgr.buffers.len() >= 3 {
continue;
}
state.add_buffer(&qhandle);
bufcnt = state.bufmgr.buffers.len();
match state.bufmgr.next_buffer() {
Some(b) => b,
None => {
continue;
} }
} }
} }
};
if buf.last_damage.is_empty() {
for _ in 0..state.widgets.len() {
buf.last_damage.push(Geometry::new());
}
}
state.dirty = false;
buf.acquire();
let mut bufview = BufferView::new(
&mut buf.mmap,
(state.dimensions.0 as u32, state.dimensions.1 as u32),
);
let surface = state.main_surface.wl_surface.as_ref().unwrap().clone();
if force {
for (idx, widget) in state.widgets.iter_mut().enumerate() {
if force || widget.get_dirty() {
let geo = widget.geometry();
let mut subview = bufview.subgeometry(geo);
buf.last_damage[idx] =
widget.draw(&mut state.fonts.unwrap().borrow_mut(), &mut subview);
}
}
surface.damage_buffer(0, 0, 0x7FFFFFFF, 0x7FFFFFFF);
} else {
let mut drew = false;
for (idx, widget) in state.widgets.iter_mut().enumerate() {
if bufcnt > 1 || widget.get_dirty() {
drew = true;
let old_damage = buf.last_damage[idx];
bufview.subgeometry(old_damage).clear();
let geo = widget.geometry();
let mut subview = bufview.subgeometry(geo);
let new_damage =
widget.draw(&mut state.fonts.unwrap().borrow_mut(), &mut subview);
let combined_damage = new_damage.expand(old_damage);
buf.last_damage[idx] = new_damage;
surface.damage_buffer(
combined_damage.x as i32,
combined_damage.y as i32,
combined_damage.width as i32,
combined_damage.height as i32,
);
}
}
if !drew {
buf.release();
conn.flush().unwrap();
continue;
}
}
state.ready = false;
surface.attach(Some(&buf.buffer), 0, 0);
surface.frame(&qhandle, ());
surface.commit();
conn.flush().unwrap();
// Now is a good as time as any to load the keymap
state.keyboard.resolve();
} }
} }
+859
View File
@@ -0,0 +1,859 @@
use std::{
cmp::max,
default::Default,
env,
rc::Rc,
sync::{Arc, Mutex},
};
use wayland_client::{
Connection, Dispatch, QueueHandle, WEnum,
protocol::{
wl_buffer, wl_callback, wl_compositor, wl_keyboard, wl_pointer, wl_registry, wl_seat,
wl_shm, wl_shm_pool, wl_subcompositor, wl_subsurface, wl_surface,
},
};
use wayland_protocols::{
wp::{
single_pixel_buffer::v1::client::wp_single_pixel_buffer_manager_v1,
viewporter::client::{wp_viewport, wp_viewporter},
},
xdg::{
activation::v1::client::{xdg_activation_token_v1, xdg_activation_v1},
shell::client::{xdg_surface, xdg_toplevel, xdg_wm_base},
},
};
use wayland_protocols_wlr::layer_shell::v1::client::{zwlr_layer_shell_v1, zwlr_layer_surface_v1};
use crate::{
buffer::BufferManager,
config::OperationMode,
event::{Event, Events, PointerButton, PointerEvent},
fonts::{FontMap, MaybeFontMap},
keyboard::{Keyboard, RepeatMessage},
widgets::{Geometry, Layout, Widget, WidgetUpdater},
};
use calloop::channel::Sender;
#[derive(Default)]
pub struct MainSurface {
pub wl_surface: Option<wl_surface::WlSurface>,
xdg_surface: Option<(xdg_surface::XdgSurface, xdg_toplevel::XdgToplevel)>,
layer_surface: Option<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1>,
}
#[derive(Default)]
pub struct BackgroundSurface {
wl_surface: Option<wl_surface::WlSurface>,
subsurface: Option<wl_subsurface::WlSubsurface>,
viewport: Option<wp_viewport::WpViewport>,
}
#[derive(Default)]
pub struct Protocols {
wl_compositor: Option<wl_compositor::WlCompositor>,
wl_subcompositor: Option<wl_subcompositor::WlSubcompositor>,
wl_shm: Option<wl_shm::WlShm>,
xdg_wm_base: Option<xdg_wm_base::XdgWmBase>,
xdg_activation: Option<xdg_activation_v1::XdgActivationV1>,
zwlr_layer_shell: Option<zwlr_layer_shell_v1::ZwlrLayerShellV1>,
viewporter: Option<wp_viewporter::WpViewporter>,
wp_single_pixel_buffer_manager:
Option<wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1>,
}
pub struct State {
mode: OperationMode,
background: Option<u32>,
pub main_surface: MainSurface,
pub bg_surface: BackgroundSurface,
pub protocols: Protocols,
pub running: bool,
pub ready: bool,
pub dirty: bool,
activated: bool,
pub configured: bool,
pub dimensions: (i32, i32),
pub bufmgr: BufferManager,
pub widgets: Vec<Box<dyn Widget>>,
pub keyboard: Keyboard,
pub keyrepeat_sender: Sender<RepeatMessage>,
pointer: Option<(f64, f64)>,
pub fonts: MaybeFontMap,
pub events: Arc<Mutex<Events>>,
layout: Rc<Box<dyn Layout>>,
}
impl State {
pub fn new(
mode: OperationMode,
background: Option<u32>,
widgets: Vec<Box<dyn Widget>>,
layout: Rc<Box<dyn Layout>>,
fonts: MaybeFontMap,
events: Arc<Mutex<Events>>,
keyrepeat_sender: Sender<RepeatMessage>,
) -> State {
State {
protocols: Default::default(),
bg_surface: Default::default(),
main_surface: Default::default(),
running: true,
dirty: true,
activated: false,
configured: false,
dimensions: (320, 240),
bufmgr: BufferManager::new(),
keyboard: Keyboard::new(),
pointer: None,
keyrepeat_sender,
mode,
background,
widgets,
fonts,
events,
layout,
ready: false,
}
}
pub fn add_buffer(&mut self, qh: &QueueHandle<Self>) {
self.bufmgr.add_buffer(
self.protocols.wl_shm.as_ref().expect("missing wl_shm"),
self.dimensions,
qh,
);
}
pub fn activate(&mut self, qh: &QueueHandle<Self>) {
let key = match env::var("XDG_ACTIVATION_TOKEN") {
Ok(token) => token,
Err(_) => return,
};
if let (Some(surface), Some(activation)) = (
&self.main_surface.wl_surface,
&self.protocols.xdg_activation,
) {
activation.activate(key, surface);
let activation_token = activation.get_activation_token(qh, ());
activation_token.commit();
unsafe {
env::remove_var("XDG_ACTIVATION_TOKEN");
}
}
}
}
impl WidgetUpdater for State {
fn geometry_update(
&mut self,
idx: usize,
fonts: &mut FontMap,
geometry: &Geometry,
) -> Geometry {
self.widgets[idx].geometry_update(fonts, geometry)
}
fn minimum_size(&mut self, idx: usize, fonts: &mut FontMap) -> Geometry {
self.widgets[idx].minimum_size(fonts)
}
}
impl State {
pub fn check_registry(&mut self, qh: &QueueHandle<Self>) {
let wl_compositor = self
.protocols
.wl_compositor
.as_ref()
.expect("wl_compositor missing");
let main_surface = wl_compositor.create_surface(qh, ());
let bg_surface = wl_compositor.create_surface(qh, ());
self.main_surface.wl_surface = Some(main_surface.clone());
self.bg_surface.wl_surface = Some(bg_surface.clone());
self.activate(qh);
let wl_subcompositor = self
.protocols
.wl_subcompositor
.as_ref()
.expect("wl_subcompositor missing");
let subsurface = wl_subcompositor.get_subsurface(&bg_surface, &main_surface, qh, ());
subsurface.place_below(&main_surface);
self.bg_surface.subsurface = Some(subsurface.clone());
match self.mode {
OperationMode::XdgToplevel => {
self.fonts.resolve();
let xdg_wm_base = self
.protocols
.xdg_wm_base
.as_ref()
.expect("xdg_wm_base missing");
let xdg_surface = xdg_wm_base.get_xdg_surface(&main_surface, qh, ());
let toplevel = xdg_surface.get_toplevel(qh, ());
toplevel.set_title("wldash".into());
main_surface.commit();
self.main_surface.xdg_surface = Some((xdg_surface, toplevel));
}
OperationMode::LayerSurface(size) => {
self.fonts.resolve();
let layer_shell = self
.protocols
.zwlr_layer_shell
.as_ref()
.expect("zwlr_layer_shell_v1 missing");
let layer_surface = layer_shell.get_layer_surface(
&main_surface,
None,
zwlr_layer_shell_v1::Layer::Overlay,
"launcher".to_string(),
qh,
(),
);
let fonts = self.fonts.unwrap();
let layout = self.layout.clone();
let min_size = layout.minimum_size(&mut fonts.borrow_mut(), self);
layer_surface.set_keyboard_interactivity(
zwlr_layer_surface_v1::KeyboardInteractivity::Exclusive,
);
layer_surface.set_anchor(
zwlr_layer_surface_v1::Anchor::Left
.union(zwlr_layer_surface_v1::Anchor::Bottom)
.union(zwlr_layer_surface_v1::Anchor::Right)
.union(zwlr_layer_surface_v1::Anchor::Top),
);
layer_surface.set_size(max(size.0, min_size.width), max(size.1, min_size.height));
main_surface.commit();
self.main_surface.layer_surface = Some(layer_surface);
}
}
let viewporter = self
.protocols
.viewporter
.as_ref()
.expect("wp_viewporter missing");
let viewport = viewporter.get_viewport(&bg_surface, qh, ());
self.bg_surface.viewport = Some(viewport);
if let Some(background) = self.background {
let wp_single_pixel_buffer_manager = self
.protocols
.wp_single_pixel_buffer_manager
.as_ref()
.expect("wp_single_pixel_buffer_manager_v1 missing");
let buffer =
wp_single_pixel_buffer_manager.create_u32_rgba_buffer(0, 0, 0, background, qh, ());
bg_surface.attach(Some(&buffer), 0, 0);
bg_surface.damage_buffer(0, 0, 1, 1);
}
}
}
impl Dispatch<wl_registry::WlRegistry, ()> for State {
fn event(
state: &mut Self,
registry: &wl_registry::WlRegistry,
event: wl_registry::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_registry::Event::Global {
name, interface, ..
} = event
{
match &interface[..] {
"wl_compositor" => {
state.protocols.wl_compositor =
Some(registry.bind::<wl_compositor::WlCompositor, _, _>(name, 5, qh, ()));
}
"wl_subcompositor" => {
state.protocols.wl_subcompositor = Some(
registry.bind::<wl_subcompositor::WlSubcompositor, _, _>(name, 1, qh, ()),
);
}
"wl_shm" => {
state.protocols.wl_shm =
Some(registry.bind::<wl_shm::WlShm, _, _>(name, 1, qh, ()));
}
"wl_seat" => {
registry.bind::<wl_seat::WlSeat, _, _>(name, 8, qh, ());
}
"xdg_wm_base" => {
if let OperationMode::XdgToplevel = state.mode {
state.protocols.xdg_wm_base =
Some(registry.bind::<xdg_wm_base::XdgWmBase, _, _>(name, 2, qh, ()));
}
}
"xdg_activation_v1" => {
state.protocols.xdg_activation = Some(
registry.bind::<xdg_activation_v1::XdgActivationV1, _, _>(name, 1, qh, ()),
);
}
"zwlr_layer_shell_v1" => {
if let OperationMode::LayerSurface(_) = state.mode {
state.protocols.zwlr_layer_shell = Some(
registry.bind::<zwlr_layer_shell_v1::ZwlrLayerShellV1, _, _>(
name,
4,
qh,
(),
),
);
}
}
"wp_viewporter" => {
state.protocols.viewporter =
Some(registry.bind::<wp_viewporter::WpViewporter, _, _>(name, 1, qh, ()));
}
"wp_single_pixel_buffer_manager_v1" => {
state.protocols.wp_single_pixel_buffer_manager = Some(registry.bind::<wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1, _, _>(name, 1, qh, ()));
}
_ => {}
}
}
}
}
impl Dispatch<wl_callback::WlCallback, ()> for State {
fn event(
state: &mut Self,
_: &wl_callback::WlCallback,
event: wl_callback::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let wl_callback::Event::Done { callback_data: _ } = event {
state.ready = true
}
}
}
impl Dispatch<wl_compositor::WlCompositor, ()> for State {
fn event(
_: &mut Self,
_: &wl_compositor::WlCompositor,
_: wl_compositor::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_subcompositor::WlSubcompositor, ()> for State {
fn event(
_: &mut Self,
_: &wl_subcompositor::WlSubcompositor,
_: wl_subcompositor::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_surface::WlSurface, ()> for State {
fn event(
_: &mut Self,
_: &wl_surface::WlSurface,
_: wl_surface::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_subsurface::WlSubsurface, ()> for State {
fn event(
_: &mut Self,
_: &wl_subsurface::WlSubsurface,
_: wl_subsurface::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_shm::WlShm, ()> for State {
fn event(
_: &mut Self,
_: &wl_shm::WlShm,
_: wl_shm::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_shm_pool::WlShmPool, ()> for State {
fn event(
_: &mut Self,
_: &wl_shm_pool::WlShmPool,
_: wl_shm_pool::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wl_buffer::WlBuffer, ()> for State {
fn event(
state: &mut Self,
buffer: &wl_buffer::WlBuffer,
_: wl_buffer::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
for buf in state.bufmgr.buffers.iter_mut() {
if &buf.buffer == buffer {
buf.release();
}
}
}
}
impl Dispatch<zwlr_layer_shell_v1::ZwlrLayerShellV1, ()> for State {
fn event(
_: &mut Self,
_: &zwlr_layer_shell_v1::ZwlrLayerShellV1,
_: zwlr_layer_shell_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<zwlr_layer_surface_v1::ZwlrLayerSurfaceV1, ()> for State {
fn event(
state: &mut Self,
layer_surface: &zwlr_layer_surface_v1::ZwlrLayerSurfaceV1,
event: zwlr_layer_surface_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
zwlr_layer_surface_v1::Event::Configure {
serial,
mut width,
mut height,
} => {
if (width, height) == (0, 0) {
if state.configured {
return;
}
(width, height) = (640, 480);
}
layer_surface.ack_configure(serial);
let first_configure = !state.configured;
state.configured = true;
state.dirty = true;
let dim = (width as i32, height as i32);
// Always lay out on the first configure, even if the
// compositor happens to hand us our placeholder dimensions.
if !first_configure && state.dimensions == dim {
return;
}
state.bufmgr.clear_buffers();
state.dimensions = dim;
let fonts = state.fonts.unwrap();
let layout = state.layout.clone();
layout.geometry_update(
&mut fonts.borrow_mut(),
&Geometry {
x: 0,
y: 0,
width,
height,
},
state,
);
let surface = state
.bg_surface
.wl_surface
.as_ref()
.expect("bg surface was not ready");
let viewport = state
.bg_surface
.viewport
.as_ref()
.expect("bg viewport was not ready");
viewport.set_destination(dim.0, dim.1);
surface.commit();
}
zwlr_layer_surface_v1::Event::Closed => {
std::process::exit(0);
}
_ => (),
}
}
}
impl Dispatch<xdg_wm_base::XdgWmBase, ()> for State {
fn event(
_: &mut Self,
wm_base: &xdg_wm_base::XdgWmBase,
event: xdg_wm_base::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_wm_base::Event::Ping { serial } = event {
wm_base.pong(serial);
}
}
}
impl Dispatch<xdg_surface::XdgSurface, ()> for State {
fn event(
state: &mut Self,
xdg_surface: &xdg_surface::XdgSurface,
event: xdg_surface::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_surface::Event::Configure { serial, .. } = event {
xdg_surface.ack_configure(serial);
state.configured = true;
state.dirty = true;
}
}
}
impl Dispatch<xdg_toplevel::XdgToplevel, ()> for State {
fn event(
state: &mut Self,
_: &xdg_toplevel::XdgToplevel,
event: xdg_toplevel::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
xdg_toplevel::Event::Configure {
mut width,
mut height,
states,
..
} => {
if (width, height) == (0, 0) {
if state.configured {
return;
}
(width, height) = (1280, 720);
}
let activated = states
.iter()
.find(|&st| *st as u32 == xdg_toplevel::State::Activated as u32)
.is_some();
if activated {
state.activated = true;
} else if state.activated && !activated {
let ev = Event::FocusLost;
for widget in state.widgets.iter_mut() {
widget.event(&ev);
}
}
if !state.configured || state.dimensions != (width, height) {
let surface = state
.bg_surface
.wl_surface
.as_ref()
.expect("bg surface was not ready");
let viewport = state
.bg_surface
.viewport
.as_ref()
.expect("bg viewportw as not ready");
viewport.set_destination(width, height);
surface.commit();
state.bufmgr.clear_buffers();
state.dimensions = (width, height);
state.fonts.resolve();
let fonts = state.fonts.unwrap();
let layout = state.layout.clone();
layout.geometry_update(
&mut fonts.borrow_mut(),
&Geometry {
x: 0,
y: 0,
width: width as u32,
height: height as u32,
},
state,
);
}
}
xdg_toplevel::Event::Close => {
state.running = false;
}
_ => (),
}
}
}
impl Dispatch<wl_seat::WlSeat, ()> for State {
fn event(
_: &mut Self,
seat: &wl_seat::WlSeat,
event: wl_seat::Event,
_: &(),
_: &Connection,
qh: &QueueHandle<Self>,
) {
if let wl_seat::Event::Capabilities {
capabilities: WEnum::Value(capabilities),
} = event
{
if capabilities.contains(wl_seat::Capability::Keyboard) {
seat.get_keyboard(qh, ());
}
if capabilities.contains(wl_seat::Capability::Pointer) {
seat.get_pointer(qh, ());
}
}
}
}
impl Dispatch<wl_keyboard::WlKeyboard, ()> for State {
fn event(
state: &mut Self,
_: &wl_keyboard::WlKeyboard,
event: wl_keyboard::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_keyboard::Event::Leave { .. } => {
state
.keyrepeat_sender
.send(RepeatMessage::StopRepeat)
.unwrap();
}
wl_keyboard::Event::Key {
key,
state: kbstate,
..
} => {
state.keyboard.resolve();
let k = state.keyboard.key(key, kbstate);
let repeats = k.repeats;
let ev = Event::KeyEvent(k);
// Widgets see the key first so the launcher gets to save its
// prompt on Escape (the early-return used to swallow it).
for widget in state.widgets.iter_mut() {
widget.event(&ev);
}
// Raw keycode 1 is KEY_ESC: fall back to quitting even when no
// keymap could be loaded and no widget handled it.
if key == 1 && kbstate == WEnum::Value(wl_keyboard::KeyState::Pressed) {
state.running = false;
return;
}
if repeats && let Event::KeyEvent(k) = ev {
state
.keyrepeat_sender
.send(RepeatMessage::KeyEvent(k))
.unwrap();
}
state.dirty = true;
}
wl_keyboard::Event::Modifiers {
mods_depressed,
mods_latched,
mods_locked,
group,
..
} => {
state
.keyboard
.modifiers(mods_depressed, mods_latched, mods_locked, group);
}
wl_keyboard::Event::Keymap { format, fd, size } => {
// TODO: Keymap loading blocks xdg configure, consider delaying
state.keyboard.keymap(format, fd, size);
}
wl_keyboard::Event::RepeatInfo { rate, delay } => {
state
.keyrepeat_sender
.send(RepeatMessage::RepeatInfo((rate as u32, delay as u32)))
.unwrap();
}
_ => (),
}
}
}
// TODO: Move to dedicated pointer module
const BTN_LEFT: u32 = 0x110;
const BTN_RIGHT: u32 = 0x111;
const BTN_MIDDLE: u32 = 0x112;
impl Dispatch<wl_pointer::WlPointer, ()> for State {
fn event(
state: &mut Self,
_: &wl_pointer::WlPointer,
event: wl_pointer::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
match event {
wl_pointer::Event::Enter {
surface_x,
surface_y,
..
} => {
state.pointer = Some((surface_x, surface_y));
}
wl_pointer::Event::Motion {
surface_x,
surface_y,
..
} => {
state.pointer = Some((surface_x, surface_y));
}
wl_pointer::Event::Leave { .. } => state.pointer = None,
wl_pointer::Event::Axis { axis, value, .. } => {
if let Some((x, y)) = state.pointer {
let axis = match axis {
WEnum::Value(wl_pointer::Axis::VerticalScroll) => {
PointerButton::ScrollVertical(value)
}
WEnum::Value(wl_pointer::Axis::HorizontalScroll) => {
PointerButton::ScrollHorizontal(value)
}
_ => return,
};
let pos = (x as u32, y as u32);
for widget in state.widgets.iter_mut() {
let geo = widget.geometry();
if geo.contains(pos) {
let pos = (pos.0 - geo.x, pos.1 - geo.y);
let ev = Event::PointerEvent(PointerEvent { button: axis, pos });
widget.event(&ev);
state.dirty = true;
break;
}
}
}
}
wl_pointer::Event::Button {
button,
state: button_state,
..
} => {
if let (button, WEnum::Value(wl_pointer::ButtonState::Pressed), Some((x, y))) =
(button, button_state, state.pointer)
{
let button = match button {
BTN_LEFT => PointerButton::Left,
BTN_RIGHT => PointerButton::Right,
BTN_MIDDLE => PointerButton::Middle,
_ => return,
};
let pos = (x as u32, y as u32);
for widget in state.widgets.iter_mut() {
let geo = widget.geometry();
if geo.contains(pos) {
let pos = (pos.0 - geo.x, pos.1 - geo.y);
let ev = Event::PointerEvent(PointerEvent { button, pos });
widget.event(&ev);
state.dirty = true;
break;
}
}
}
}
_ => (),
}
}
}
impl Dispatch<wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1, ()> for State {
fn event(
_: &mut Self,
_: &wp_single_pixel_buffer_manager_v1::WpSinglePixelBufferManagerV1,
_: wp_single_pixel_buffer_manager_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wp_viewporter::WpViewporter, ()> for State {
fn event(
_: &mut Self,
_: &wp_viewporter::WpViewporter,
_: wp_viewporter::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<wp_viewport::WpViewport, ()> for State {
fn event(
_: &mut Self,
_: &wp_viewport::WpViewport,
_: wp_viewport::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<xdg_activation_v1::XdgActivationV1, ()> for State {
fn event(
_: &mut Self,
_: &xdg_activation_v1::XdgActivationV1,
_: xdg_activation_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
}
}
impl Dispatch<xdg_activation_token_v1::XdgActivationTokenV1, ()> for State {
fn event(
state: &mut Self,
_: &xdg_activation_token_v1::XdgActivationTokenV1,
event: xdg_activation_token_v1::Event,
_: &(),
_: &Connection,
_: &QueueHandle<Self>,
) {
if let xdg_activation_token_v1::Event::Done { token } = event {
let ev = Event::TokenUpdate(token);
for widget in state.widgets.iter_mut() {
widget.event(&ev);
}
}
}
}
+125
View File
@@ -0,0 +1,125 @@
use std::{
cmp::Ordering,
collections::HashSet,
error::Error,
fs::read_to_string,
hash::{Hash, Hasher},
io::{Error as io_error, ErrorKind},
path::Path,
};
use walkdir::WalkDir;
use crate::utils::{inish, xdg};
#[derive(Clone, Debug, Eq)]
pub struct Desktop {
pub entry_type: String,
pub name: String,
pub no_display: bool,
pub hidden: bool,
pub exec: Option<String>,
pub url: Option<String>,
pub term: bool,
pub keywords: Vec<String>,
}
impl Desktop {
fn parse(f: &Path) -> Result<Desktop, Box<dyn Error>> {
let s = read_to_string(f)?;
let config = inish::parse(&s)?;
match config.get("Desktop Entry") {
Some(section) => Ok(Desktop {
entry_type: section.get("Type").unwrap_or(&"").to_string(),
name: section.get("Name").unwrap_or(&"").to_string(),
term: section.get("Terminal").unwrap_or(&"") == &"true",
no_display: section.get("NoDisplay").unwrap_or(&"") == &"true",
hidden: section.get("Hidden").unwrap_or(&"") == &"true",
exec: section.get("Exec").map(|x| x.to_string()),
url: section.get("URL").map(|x| x.to_string()),
keywords: section
.get("Keywords")
.map(|x| {
x.split(';')
.map(|y| y.trim().to_string())
.filter(|z| !z.is_empty())
.collect()
})
.unwrap_or_else(std::vec::Vec::new),
}),
None => Err(Box::new(io_error::new(
ErrorKind::NotFound,
"no desktop entry in file",
))),
}
}
}
impl Ord for Desktop {
fn cmp(&self, other: &Self) -> Ordering {
self.name.cmp(&other.name)
}
}
impl PartialOrd for Desktop {
fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
Some(self.cmp(other))
}
}
impl PartialEq for Desktop {
fn eq(&self, other: &Self) -> bool {
self.name == other.name
}
}
// Must agree with PartialEq: equal entries (same name) must hash equally.
impl Hash for Desktop {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state)
}
}
/// Scan every XDG applications directory for launchable entries.
///
/// This used to be cached as JSON under XDG_CACHE_HOME and refreshed by
/// hand with `--desktop-refresh`. The scan is a few milliseconds over
/// local /usr/share and runs on a background thread, while the cache sat
/// on the (possibly NFS-mounted) home directory and went stale whenever a
/// package was installed. So: no cache.
pub fn load_desktop_files() -> Vec<Desktop> {
let dirs = xdg::data_folders();
let mut desktop: Vec<Desktop> = Vec::new();
let mut seen: HashSet<String> = HashSet::new();
for dir in dirs {
for entry in WalkDir::new(format!("{}/applications", dir))
.into_iter()
.flatten()
{
// Entries are often symlinks, so only rule out directories.
if entry.file_type().is_dir()
|| entry.path().extension().and_then(|e| e.to_str()) != Some("desktop")
{
continue;
}
{
if let Ok(d) = Desktop::parse(entry.path()) {
if d.hidden
|| d.no_display
|| (d.entry_type != "Application" && d.entry_type != "Link")
{
continue;
}
if !seen.contains(&d.name) {
seen.insert(d.name.to_string());
desktop.push(d);
}
}
}
}
}
desktop
}
+134
View File
@@ -0,0 +1,134 @@
use std::{collections::HashMap, error::Error};
fn parse_field(line: &str) -> Result<(&str, &str), Box<dyn Error>> {
let split = match line.find('=') {
Some(v) => v,
None => return Err("expected equals sign on line, but found none".into()),
};
let (key, value) = line.split_at(split);
let (key, value) = (key.trim(), value.trim_matches('=').trim());
Ok((key, value))
}
type InishSection<'a> = HashMap<&'a str, &'a str>;
type Inish<'a> = HashMap<&'a str, InishSection<'a>>;
pub fn parse<'a>(s: &'a str) -> Result<Inish<'a>, Box<dyn Error>> {
let mut sections: Inish<'a> = HashMap::new();
let mut current_section = HashMap::new();
let mut current_section_name = "";
for line in s.lines() {
let line = line.trim();
let mut chars = line.chars();
let start = chars.next();
let end = chars.last();
match (start, end) {
(Some('#'), _) => continue,
(Some('['), Some(']')) => {
sections.insert(current_section_name, current_section);
current_section = HashMap::new();
let len = line.len();
current_section_name = line[1..len - 1].trim();
}
(Some('['), v) => {
return Err(format!(
"expected Some(']') to terminate section name, but got {:?}",
v
)
.into());
}
_ if line.is_empty() => continue,
_ => {
let (key, value) = parse_field(line)?;
current_section.insert(key, value);
}
}
}
sections.insert(current_section_name, current_section);
Ok(sections)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sections() {
let config = parse(
"
[section_a]
[section_b]
",
)
.expect("config didn't parse");
if !config.contains_key("section_a") {
panic!("named section did not exist");
}
if !config.contains_key("section_b") {
panic!("named section did not exist");
}
if !config.contains_key("") {
panic!("unnamed section did not exist");
}
}
#[test]
fn fields() {
let config = parse(
"
field_outside = 'hello'
[section_a]
field_a = 1234
",
)
.expect("config didn't parse");
let section_a = match config.get("section_a") {
Some(v) => v,
None => panic!("named section did not exist"),
};
assert_eq!(section_a.get("field_a"), Some(&"1234"));
let section_unnamed = match config.get("") {
Some(v) => v,
None => panic!("unnamed section did not exist"),
};
assert_eq!(section_unnamed.get("field_outside"), Some(&"'hello'"));
}
#[test]
fn whitespaces() {
let config = parse(
"
field outside = 'hello'
[ section a ]
field a = 1234 ",
)
.expect("config didn't parse");
let section_a = match config.get("section a") {
Some(v) => v,
None => panic!("named section did not exist"),
};
assert_eq!(section_a.get("field a"), Some(&"1234"));
let section_unnamed = match config.get("") {
Some(v) => v,
None => panic!("unnamed section did not exist"),
};
assert_eq!(section_unnamed.get("field outside"), Some(&"'hello'"));
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod desktop;
mod inish;
pub mod xdg;
+52
View File
@@ -0,0 +1,52 @@
use std::env;
pub fn cache_folder() -> String {
match env::var_os("XDG_CACHE_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.cache", home)
}
}
}
/// XDG_STATE_HOME: small persistent state (calculator history, saved
/// prompt). Unlike a cache this is worth roaming with the home directory.
pub fn state_folder() -> String {
match env::var_os("XDG_STATE_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.local/state", home)
}
}
}
pub fn config_folder() -> String {
match env::var_os("XDG_CONFIG_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.config", home)
}
}
}
pub fn data_folders() -> Vec<String> {
let xdg_data_home = match env::var_os("XDG_DATA_HOME") {
Some(s) => s.into_string().unwrap(),
None => {
let home = env::var_os("HOME").unwrap().into_string().unwrap();
format!("{}/.local/share", home)
}
};
let xdg_data_dirs = match env::var_os("XDG_DATA_DIRS") {
Some(s) => s.into_string().unwrap(),
None => "/usr/local/share:/usr/share".to_string(),
};
// Eww.
std::iter::once(xdg_data_home.to_string())
.chain(xdg_data_dirs.split(':').map(|x| x.to_string()))
.collect()
}
-458
View File
@@ -1,458 +0,0 @@
use crate::buffer::Buffer;
use crate::color::Color;
pub use crate::keyboard::{KeyState, ModifiersState};
use chrono::NaiveDateTime;
use nix::poll::PollFd;
pub struct DrawContext<'a> {
pub buf: &'a mut Buffer<'a>,
pub bg: &'a Color,
pub time: NaiveDateTime,
pub force: bool,
}
#[derive(Debug)]
pub struct DrawReport {
pub width: u32,
pub height: u32,
pub damage: Vec<(i32, i32, i32, i32)>,
pub full_damage: bool,
}
impl DrawReport {
pub fn empty(width: u32, height: u32) -> DrawReport {
DrawReport {
width,
height,
damage: Vec::new(),
full_damage: false,
}
}
}
pub struct WaitContext {
pub fds: Vec<PollFd>,
pub target_time: Option<NaiveDateTime>,
}
impl WaitContext {
pub fn set_time(&mut self, new_time: NaiveDateTime) {
if let Some(ot) = self.target_time {
if new_time < ot {
self.target_time = Some(new_time);
}
} else {
self.target_time = Some(new_time);
}
}
}
pub trait Widget {
fn wait(&mut self, ctx: &mut WaitContext);
fn enter(&mut self);
fn leave(&mut self);
fn size(&self) -> (u32, u32);
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error>;
fn keyboard_input(
&mut self,
keysym: u32,
modifier_state: ModifiersState,
key_state: KeyState,
interpreted: Option<String>,
);
fn mouse_click(&mut self, button: u32, pos: (u32, u32));
fn mouse_scroll(&mut self, scroll: (f64, f64), pos: (u32, u32));
}
pub struct VerticalLayout<'a> {
pub children: Vec<Box<dyn Widget + Send + 'a>>,
}
#[allow(dead_code)]
impl<'a> VerticalLayout<'a> {
pub fn new(children: Vec<Box<dyn Widget + Send + 'a>>) -> Box<VerticalLayout> {
Box::new(VerticalLayout { children })
}
}
impl<'a> Widget for VerticalLayout<'a> {
fn wait(&mut self, ctx: &mut WaitContext) {
for child in &mut self.children {
child.wait(ctx);
}
}
fn enter(&mut self) {
for child in &mut self.children {
child.enter();
}
}
fn leave(&mut self) {
for child in &mut self.children {
child.leave();
}
}
fn size(&self) -> (u32, u32) {
let mut width = 0;
let mut height = 0;
for child in &self.children {
let size = child.size();
if size.0 > width {
width = size.0;
}
height += size.1;
}
(width, height)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let mut offset = 0;
let mut width = 0;
let mut damage = Vec::new();
let mut full_damage = false;
for child in &mut self.children {
let mut report = child.draw(
ctx,
(pos.0, offset + pos.1),
(expansion.0, expansion.1 - offset),
)?;
if report.width > width {
width = report.width
}
offset += report.height;
full_damage |= report.full_damage;
damage.append(&mut report.damage);
}
Ok(DrawReport {
width,
height: offset,
damage,
full_damage,
})
}
fn keyboard_input(
&mut self,
keysym: u32,
modifier_state: ModifiersState,
key_state: KeyState,
interpreted: Option<String>,
) {
for child in &mut self.children {
child.keyboard_input(keysym, modifier_state, key_state, interpreted.clone());
}
}
fn mouse_click(&mut self, button: u32, pos: (u32, u32)) {
let mut height = 0;
for child in &mut self.children {
let size = child.size();
if pos.1 >= height && pos.1 < height + size.1 {
let pos = (pos.0, pos.1 - height);
child.mouse_click(button, pos);
return;
}
height += size.1;
}
}
fn mouse_scroll(&mut self, scroll: (f64, f64), pos: (u32, u32)) {
let mut height = 0;
for child in &mut self.children {
let size = child.size();
if pos.1 >= height && pos.1 < height + size.1 {
let pos = (pos.0, pos.1 - height);
child.mouse_scroll(scroll, pos);
return;
}
height += size.1;
}
}
}
pub struct HorizontalLayout<'a> {
pub children: Vec<Box<dyn Widget + Send + 'a>>,
}
#[allow(dead_code)]
impl<'a> HorizontalLayout<'a> {
pub fn new(children: Vec<Box<dyn Widget + Send + 'a>>) -> Box<HorizontalLayout> {
Box::new(HorizontalLayout { children })
}
fn height(&self) -> u32 {
let mut height = 0;
for child in &self.children {
let size = child.size();
if size.1 > height {
height = size.1;
}
}
height
}
}
impl<'a> Widget for HorizontalLayout<'a> {
fn wait(&mut self, ctx: &mut WaitContext) {
for child in &mut self.children {
child.wait(ctx);
}
}
fn enter(&mut self) {
for child in &mut self.children {
child.enter();
}
}
fn leave(&mut self) {
for child in &mut self.children {
child.leave();
}
}
fn size(&self) -> (u32, u32) {
let mut width = 0;
let mut height = 0;
for child in &self.children {
let size = child.size();
if size.1 > height {
height = size.1;
}
width += size.0;
}
(width, height)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let mut offset = 0;
let mut height = 0;
let mut damage = Vec::new();
let mut full_damage = false;
for child in &mut self.children {
let mut report = child.draw(
ctx,
(offset + pos.0, pos.1),
(expansion.0 - offset, expansion.1),
)?;
if report.height > height {
height = report.height
}
offset += report.width;
full_damage |= report.full_damage;
damage.append(&mut report.damage);
}
Ok(DrawReport {
width: offset,
height,
damage,
full_damage,
})
}
fn keyboard_input(
&mut self,
keysym: u32,
modifier_state: ModifiersState,
key_state: KeyState,
interpreted: Option<String>,
) {
for child in &mut self.children {
child.keyboard_input(keysym, modifier_state, key_state, interpreted.clone());
}
}
fn mouse_click(&mut self, button: u32, pos: (u32, u32)) {
let mut width = 0;
for child in &mut self.children {
let size = child.size();
if pos.0 >= width && pos.0 < width + size.0 {
let pos = (pos.0 - width, pos.1);
child.mouse_click(button, pos);
return;
}
width += size.0;
}
}
fn mouse_scroll(&mut self, scroll: (f64, f64), pos: (u32, u32)) {
let mut width = 0;
for child in &mut self.children {
let size = child.size();
if pos.0 >= width && pos.0 < width + size.0 {
let pos = (pos.0 - width, pos.1);
child.mouse_scroll(scroll, pos);
return;
}
width += size.0;
}
}
}
pub struct Margin<'a> {
pub child: Box<dyn Widget + Send + 'a>,
pub margins: (u32, u32, u32, u32), // left, right, top, bottom
}
#[allow(dead_code)]
impl<'a> Margin<'a> {
pub fn new(margins: (u32, u32, u32, u32), child: Box<dyn Widget + Send + 'a>) -> Box<Margin> {
Box::new(Margin { child, margins })
}
}
impl<'a> Widget for Margin<'a> {
fn wait(&mut self, ctx: &mut WaitContext) {
self.child.wait(ctx)
}
fn enter(&mut self) {
self.child.enter()
}
fn leave(&mut self) {
self.child.leave()
}
fn size(&self) -> (u32, u32) {
let size = self.child.size();
(
size.0 + self.margins.0 + self.margins.1,
size.1 + self.margins.2 + self.margins.3,
)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let expansion = (
expansion.0 - self.margins.0 - self.margins.1,
expansion.1 - self.margins.2 - self.margins.3,
);
let report = self.child.draw(
ctx,
(pos.0 + self.margins.0, pos.1 + self.margins.2),
expansion,
)?;
Ok(DrawReport {
width: report.width + self.margins.0 + self.margins.1,
height: report.height + self.margins.2 + self.margins.3,
damage: report.damage,
full_damage: report.full_damage,
})
}
fn keyboard_input(
&mut self,
keysym: u32,
modifier_state: ModifiersState,
key_state: KeyState,
interpreted: Option<String>,
) {
self.child
.keyboard_input(keysym, modifier_state, key_state, interpreted);
}
fn mouse_click(&mut self, button: u32, pos: (u32, u32)) {
let pos = (
pos.0.saturating_sub(self.margins.0),
pos.1.saturating_sub(self.margins.2),
);
self.child.mouse_click(button, pos);
}
fn mouse_scroll(&mut self, scroll: (f64, f64), pos: (u32, u32)) {
let pos = (
pos.0.saturating_sub(self.margins.0),
pos.1.saturating_sub(self.margins.2),
);
self.child.mouse_scroll(scroll, pos);
}
}
pub struct Fixed<'a> {
pub child: Box<dyn Widget + Send + 'a>,
pub size: (u32, u32),
}
#[allow(dead_code)]
impl<'a> Fixed<'a> {
pub fn new(size: (u32, u32), child: Box<dyn Widget + Send + 'a>) -> Box<Fixed> {
Box::new(Fixed { child, size })
}
}
impl<'a> Widget for Fixed<'a> {
fn wait(&mut self, ctx: &mut WaitContext) {
self.child.wait(ctx)
}
fn enter(&mut self) {
self.child.enter()
}
fn leave(&mut self) {
self.child.leave()
}
fn size(&self) -> (u32, u32) {
self.size
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let report = self.child.draw(ctx, pos, expansion)?;
Ok(DrawReport {
width: self.size.0,
height: self.size.1,
damage: report.damage,
full_damage: report.full_damage,
})
}
fn keyboard_input(
&mut self,
keysym: u32,
modifier_state: ModifiersState,
key_state: KeyState,
interpreted: Option<String>,
) {
self.child
.keyboard_input(keysym, modifier_state, key_state, interpreted);
}
fn mouse_click(&mut self, button: u32, pos: (u32, u32)) {
self.child.mouse_click(button, pos);
}
fn mouse_scroll(&mut self, scroll: (f64, f64), pos: (u32, u32)) {
self.child.mouse_scroll(scroll, pos);
}
}
+112
View File
@@ -0,0 +1,112 @@
use std::{
thread,
sync::{Arc, Mutex},
};
use crate::{
color::Color,
fonts::FontMap,
event::{Event, Events, PointerButton},
widgets::bar_widget::{BarWidget, BarWidgetImpl},
};
use alsa::mixer::{
Mixer, Selem, SelemChannelId, SelemId,
};
#[inline]
fn alsa_volume_to_f32(volume: i64, min: i64, max: i64) -> f32 {
(volume - min) as f32 / (max - min) as f32
}
#[inline]
fn f32_to_alsa_volume(volume: f32, min: i64, max: i64) -> i64 {
min + ((max - min) as f32 * volume) as i64
}
fn get_master_volume(selem: &mut Selem) -> f32 {
let (min, max) = selem.get_playback_volume_range();
selem
.get_playback_volume(SelemChannelId::mono())
.map(|volume| alsa_volume_to_f32(volume, min, max))
.unwrap()
}
fn get_mute(selem: &mut Selem) -> bool {
let res = selem.get_playback_switch(SelemChannelId::FrontLeft);
false
}
fn set_master_volume(selem: &mut Selem, volume: f32) {
let (min, max) = selem.get_playback_volume_range();
let volume = f32_to_alsa_volume(volume, min, max);
selem.set_playback_volume_all(volume).unwrap();
}
fn start_monitor(inner: Arc<Mutex<Inner>>) {
thread::Builder::new()
.name("audiomon".to_string())
.spawn(move || {
let mixer = Mixer::new("default", true).unwrap();
let id = SelemId::new("Master", 0);
let mut selem = mixer.find_selem(&id).unwrap();
let mut inner = inner.lock().unwrap();
inner.volume = get_master_volume(&mut selem);
inner.mute = get_mute(&mut selem);
})
.unwrap();
}
struct Inner {
volume: f32,
mute: bool,
}
pub struct AlsaAudio {
inner: Arc<Mutex<Inner>>,
}
impl AlsaAudio {
pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
let inner = Arc::new(Mutex::new(Inner{
mute: false,
volume: 0.,
}));
start_monitor(inner.clone());
let dev = AlsaAudio{ inner };
BarWidget::new(Box::new(dev), fm, font, size)
}
}
impl BarWidgetImpl for AlsaAudio {
fn name(&self) -> &'static str {
"audio"
}
fn get_dirty(&self) -> bool {
true
}
fn value(&mut self) -> f32 {
let inner = self.inner.lock().unwrap();
inner.volume
}
fn color(&self) -> Color {
let inner = self.inner.lock().unwrap();
if inner.mute {
Color::DARKORANGE
} else {
Color::WHITE
}
}
fn click(&mut self, pos: f32, btn: PointerButton) {
let inner = self.inner.lock().unwrap();
match btn {
// PointerButton::Left => inner.set_master_volume(pos),
//PointerButton::Right => Some(Change::ToggleMute),
//PointerButton::ScrollVertical(val) => Some(Change::VolumeInc((val / 512.) as f32)),
//PointerButton::ScrollHorizontal(val) => Some(Change::VolumeInc((val / 100.) as f32)),
_ => (),
};
}
}
-119
View File
@@ -1,119 +0,0 @@
use crate::color::Color;
use crate::widget::WaitContext;
use crate::{
fonts::FontRef,
widgets::bar_widget::{BarWidget, BarWidgetImpl},
};
use alsa::mixer::{Mixer, Selem, SelemChannelId, SelemId};
const CARD_NAME: &str = "default";
const SELEM_NAME: &str = "Master";
const SELEM_ID: u32 = 0u32;
#[inline]
fn alsa_error_to_io_error(fail: &'static str, err: &alsa::Error) -> ::std::io::Error {
let kind = ::std::io::ErrorKind::Other;
let func = err.func();
let errno = err
.errno()
.map(|errno| format!("; errno = {}", errno))
.unwrap_or_default();
::std::io::Error::new(kind, format!("{}: {}{}", fail, func, errno))
}
#[inline]
fn alsa_volume_to_f32(volume: i64, min: i64, max: i64) -> f32 {
(volume - min) as f32 / (max - min) as f32
}
#[inline]
fn f32_to_alsa_volume(volume: f32, min: i64, max: i64) -> i64 {
min + ((max - min) as f32 * volume) as i64
}
pub struct Alsa {
pub mixer: Mixer,
}
impl Alsa {
pub fn new(font: FontRef, font_size: f32, length: u32) -> ::std::io::Result<Box<BarWidget>> {
let mixer = Mixer::new(CARD_NAME, true)
.map_err(|err| alsa_error_to_io_error("Failed to create ALSA mixer", &err))?;
let mixer = Self { mixer };
mixer.get_master_volume()?; // probing
Ok(BarWidget::new_simple(
font,
font_size,
length,
Box::new(mixer),
))
}
#[inline]
pub fn get_master(&self) -> ::std::io::Result<Selem> {
let master_id = SelemId::new(SELEM_NAME, SELEM_ID);
match self.mixer.find_selem(&master_id) {
Some(master) => Ok(master),
None => {
let kind = ::std::io::ErrorKind::NotFound;
let desc = "`Master` not found";
Err(::std::io::Error::new(kind, desc))
}
}
}
pub fn get_master_volume(&self) -> ::std::io::Result<f32> {
let master = self.get_master()?;
let (min, max) = master.get_playback_volume_range();
master
.get_playback_volume(SelemChannelId::mono())
.map(|volume| alsa_volume_to_f32(volume, min, max))
.map_err(|e| alsa_error_to_io_error("Failed to get `Master` volume", &e))
}
pub fn set_master_volume(&self, volume: f32) -> ::std::io::Result<()> {
let master = self.get_master()?;
let (min, max) = master.get_playback_volume_range();
let volume = f32_to_alsa_volume(volume, min, max);
master
.set_playback_volume_all(volume)
.map_err(|e| alsa_error_to_io_error("Failed to set `Master` volume", &e))
}
pub fn inc_master_volume(&self, diff: f32) -> ::std::io::Result<()> {
let master = self.get_master()?;
let (min, max) = master.get_playback_volume_range();
master
.get_playback_volume(SelemChannelId::mono())
.and_then(|volume| {
let volume = alsa_volume_to_f32(volume, min, max);
let volume = f32_to_alsa_volume(volume + diff, min, max);
master.set_playback_volume_all(volume)
})
.map_err(|e| alsa_error_to_io_error("Failed to inc/dec `Master` volume", &e))
}
}
impl BarWidgetImpl for Alsa {
fn wait(&mut self, _: &mut WaitContext) {}
fn name(&self) -> &str {
"volume"
}
fn value(&self) -> f32 {
match self.get_master_volume() {
Ok(v) => v,
Err(e) => panic!("{}", e),
}
}
fn color(&self) -> Color {
Color::new(1.0, 1.0, 1.0, 1.0)
}
fn inc(&mut self, diff: f32) {
if let Err(e) = self.inc_master_volume(diff) {
panic!("{}", e)
}
}
fn set(&mut self, abs: f32) {
if let Err(e) = self.set_master_volume(abs) {
panic!("{}", e)
}
}
fn toggle(&mut self) {}
}
-9
View File
@@ -1,9 +0,0 @@
#[cfg(feature = "alsa-widget")]
mod alsa_sound;
#[cfg(feature = "alsa-widget")]
pub use alsa_sound::Alsa;
#[cfg(feature = "pulseaudio-widget")]
mod pulse_sound;
#[cfg(feature = "pulseaudio-widget")]
pub use pulse_sound::PulseAudio;
-544
View File
@@ -1,544 +0,0 @@
use crate::cmd::Cmd;
use crate::color::Color;
use crate::widget::WaitContext;
use crate::{
fonts::FontRef,
widgets::bar_widget::{BarWidget, BarWidgetImpl},
};
use std::cell::RefCell;
use std::collections::HashMap;
use std::ops::Deref;
use std::rc::Rc;
use std::sync::mpsc::{channel, Sender};
use std::sync::{Arc, Mutex};
use std::thread;
use libpulse_binding::callbacks::ListResult;
use libpulse_binding::context::{
introspect::ServerInfo, introspect::SinkInfo, subscribe::Facility, subscribe::InterestMaskSet,
subscribe::Operation as SubscribeOperation, Context, FlagSet, State as PulseState,
};
use libpulse_binding::mainloop::standard::IterateResult;
use libpulse_binding::mainloop::standard::Mainloop;
use libpulse_binding::proplist::{properties, Proplist};
use libpulse_binding::volume::{ChannelVolumes, Volume};
struct PulseAudioConnection {
mainloop: Rc<RefCell<Mainloop>>,
context: Rc<RefCell<Context>>,
}
#[derive(Clone)]
struct PulseAudioSinkInfo {
volume: ChannelVolumes,
mute: bool,
}
struct PulseAudioClient {
sender: Sender<PulseAudioClientRequest>,
default_sink: String,
sinks: HashMap<String, PulseAudioSinkInfo>,
}
enum PulseAudioClientRequest {
GetDefaultDevice(Option<Sender<bool>>),
GetSinkInfoByIndex(Option<Sender<bool>>, u32),
GetSinkInfoByName(Option<Sender<bool>>, String),
SetSinkVolumeByName(Option<Sender<bool>>, String, ChannelVolumes),
SetSinkMuteByName(Option<Sender<bool>>, String, bool),
}
#[derive(Debug)]
struct PulseAudioSoundDeviceInner {
name: Option<String>,
volume: Option<ChannelVolumes>,
volume_avg: f32,
muted: bool,
default_sink: String,
}
struct PulseAudioSoundDevice {
client: Arc<Mutex<PulseAudioClient>>,
inner: Arc<Mutex<PulseAudioSoundDeviceInner>>,
}
impl PulseAudioConnection {
fn new() -> Result<Self, ::std::io::Error> {
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(properties::APPLICATION_NAME, "wldash")
.unwrap();
let mainloop = Rc::new(RefCell::new(Mainloop::new().unwrap()));
let context = Rc::new(RefCell::new(
Context::new_with_proplist(mainloop.borrow().deref(), "wldash_context", &proplist)
.unwrap(),
));
context
.borrow_mut()
.connect(None, FlagSet::NOFLAGS, None)
.map_err(|_e| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to connect to pulseaudio context",
)
})?;
let mut connection = PulseAudioConnection { mainloop, context };
// Wait for context to be ready
loop {
connection.iterate(false)?;
match connection.context.borrow().get_state() {
PulseState::Ready => {
break;
}
PulseState::Failed | PulseState::Terminated => {}
_ => {}
}
}
Ok(connection)
}
fn iterate(&mut self, blocking: bool) -> Result<(), ::std::io::Error> {
match self.mainloop.borrow_mut().iterate(blocking) {
IterateResult::Quit(_) | IterateResult::Err(_) => Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to iterate pulseaudio state",
)),
IterateResult::Success(_) => Ok(()),
}
}
}
impl PulseAudioClient {
fn new<F>(listener: F) -> Result<Arc<Mutex<Self>>, ::std::io::Error>
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static + Clone,
{
let (tx, rx) = channel();
let client = Arc::new(Mutex::new(PulseAudioClient {
sender: tx,
default_sink: "@DEFAULT_SINK@".to_string(),
sinks: HashMap::new(),
}));
let loop_client = client.clone();
let (tx1, rx1) = channel();
let _ = thread::Builder::new()
.name("pa_writer".to_string())
.spawn(move || {
let mut conn = match PulseAudioConnection::new() {
Ok(v) => {
tx1.send(true).unwrap();
v
}
Err(_) => {
tx1.send(false).unwrap();
return;
}
};
// make sure mainloop dispatched everything
for _ in 0..10 {
conn.iterate(false).unwrap();
}
loop {
let cl = loop_client.clone();
let l = listener.clone();
match rx.recv() {
Err(_) => return,
Ok(req) => {
let mut introspector = conn.context.borrow_mut().introspect();
let done = Arc::new(Mutex::new(false));
let done_reader = done.clone();
let s = match req {
PulseAudioClientRequest::GetDefaultDevice(s) => {
introspector.get_server_info(move |info| {
*done.lock().unwrap() = true;
PulseAudioClient::server_info_callback(
cl.clone(),
l.clone(),
info,
);
});
s
}
PulseAudioClientRequest::GetSinkInfoByIndex(s, index) => {
introspector.get_sink_info_by_index(index, move |res| {
*done.lock().unwrap() = true;
PulseAudioClient::sink_info_callback(
cl.clone(),
l.clone(),
res,
);
});
s
}
PulseAudioClientRequest::GetSinkInfoByName(s, name) => {
introspector.get_sink_info_by_name(&name, move |res| {
*done.lock().unwrap() = true;
PulseAudioClient::sink_info_callback(
cl.clone(),
l.clone(),
res,
);
});
s
}
PulseAudioClientRequest::SetSinkVolumeByName(s, name, volumes) => {
introspector.set_sink_volume_by_name(
&name,
&volumes,
Some(Box::new(move |_| {
*done.lock().unwrap() = true;
})),
);
s
}
PulseAudioClientRequest::SetSinkMuteByName(s, name, mute) => {
introspector.set_sink_mute_by_name(
&name,
mute,
Some(Box::new(move |_| {
*done.lock().unwrap() = true;
})),
);
s
}
};
// send request and receive response
loop {
if *done_reader.lock().unwrap() {
break;
}
conn.iterate(true).unwrap();
}
if let Some(s) = s {
let _ = s.send(true);
}
}
}
}
});
// subscribe
let cl2 = client.clone();
let (tx2, rx2) = channel();
let _ = thread::Builder::new()
.name("pa_reader".to_string())
.spawn(move || {
let conn = match PulseAudioConnection::new() {
Ok(v) => {
tx2.send(true).unwrap();
v
}
Err(_) => {
tx2.send(false).unwrap();
return;
}
};
// subcribe for events
conn.context
.borrow_mut()
.set_subscribe_callback(Some(Box::new(move |facility, operation, index| {
cl2.lock()
.unwrap()
.subscribe_callback(facility, operation, index)
})));
conn.context
.borrow_mut()
.subscribe(InterestMaskSet::SERVER | InterestMaskSet::SINK, |_| {});
conn.mainloop.borrow_mut().run().unwrap();
});
let pa_writer_res = rx1.recv().unwrap();
let pa_reader_res = rx2.recv().unwrap();
if !pa_writer_res || !pa_reader_res {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to start pulseaudio thread",
));
}
Ok(client)
}
fn send(&self, request: PulseAudioClientRequest) -> Result<(), ::std::io::Error> {
self.sender.send(request).map_err(|_e| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to send pulseaudio request",
)
})
}
fn server_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, server_info: &ServerInfo)
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static,
{
match server_info.default_sink_name.clone() {
None => {}
Some(default_sink) => {
(*s.lock().unwrap()).default_sink = default_sink.into();
listener(s);
}
}
}
fn sink_info_callback<F>(s: Arc<Mutex<Self>>, listener: F, result: ListResult<&SinkInfo>)
where
F: Fn(Arc<Mutex<Self>>) -> (),
F: Send + 'static,
{
match result {
ListResult::End | ListResult::Error => {}
ListResult::Item(sink_info) => match sink_info.name.clone() {
None => {}
Some(name) => {
let info = PulseAudioSinkInfo {
volume: sink_info.volume,
mute: sink_info.mute,
};
s.lock().unwrap().sinks.insert(name.into(), info);
listener(s);
}
},
}
}
fn subscribe_callback(
&self,
facility: Option<Facility>,
_operation: Option<SubscribeOperation>,
index: u32,
) {
match facility {
None => {}
Some(facility) => match facility {
Facility::Server => {
let _ = self.send(PulseAudioClientRequest::GetDefaultDevice(None));
}
Facility::Sink => {
let _ = self.send(PulseAudioClientRequest::GetSinkInfoByIndex(None, index));
}
_ => {}
},
}
}
}
impl PulseAudioSoundDevice {
fn new<F>(listener: F) -> Result<Self, ::std::io::Error>
where
F: Fn() -> (),
F: Send + 'static + Clone,
{
let inner = Arc::new(Mutex::new(PulseAudioSoundDeviceInner {
name: None,
volume: None,
volume_avg: 0.0,
muted: false,
default_sink: "@DEFAULT_SINK@".to_string(),
}));
let cb_inner = inner.clone();
let client = PulseAudioClient::new(move |client| {
let mut inner = cb_inner.lock().unwrap();
inner.default_sink = client.lock().unwrap().default_sink.clone();
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
let sink_info = match client.lock().unwrap().sinks.get(&name) {
None => return,
Some(sink_info) => (*sink_info).clone(),
};
inner.volume = Some(sink_info.volume);
inner.volume_avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
inner.muted = sink_info.mute;
listener();
})?;
let cl = client.clone();
let (tx, rx) = channel();
{
let cl = client.lock().unwrap();
cl.send(PulseAudioClientRequest::GetDefaultDevice(Some(tx)))?;
}
rx.recv().unwrap();
let name = {
let cl = client.lock().unwrap();
cl.default_sink.to_string()
};
(*inner.lock().unwrap()).name = Some(name.clone());
let device = PulseAudioSoundDevice { client: cl, inner };
let (tx, rx) = channel();
{
let cl = client.lock().unwrap();
cl.send(PulseAudioClientRequest::GetSinkInfoByName(Some(tx), name))?;
}
rx.recv().unwrap();
Ok(device)
}
fn muted(&self) -> bool {
let inner = self.inner.lock().unwrap();
inner.muted
}
fn volume(&self) -> f32 {
let inner = self.inner.lock().unwrap();
inner.volume_avg
}
fn inc_volume(&mut self, step: f32) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
let mut volume = match inner.volume {
Some(volume) => volume,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to set volume",
))
}
};
// apply step to volumes
let step = (step * Volume::NORMAL.0 as f32).round() as i32;
if step > 0 {
volume.inc_clamp(Volume(step as u32), Volume::MAX);
} else {
volume.decrease(Volume(-step as u32));
}
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
// update volumes
inner.volume = Some(volume);
inner.volume_avg = volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkVolumeByName(
None, name, volume,
))?;
Ok(())
}
fn set_volume(&mut self, val: f32) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
let mut volume = match inner.volume {
Some(volume) => volume,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"unable to set volume",
))
}
};
// apply step to volumes
volume.scale(Volume((val * Volume::NORMAL.0 as f32).round() as u32));
let name = inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone());
// update volumes
inner.volume = Some(volume);
inner.volume_avg = volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkVolumeByName(
None, name, volume,
))?;
Ok(())
}
fn toggle(&mut self) -> Result<(), ::std::io::Error> {
let mut inner = self.inner.lock().unwrap();
inner.muted = !inner.muted;
self.client
.lock()
.unwrap()
.send(PulseAudioClientRequest::SetSinkMuteByName(
None,
inner
.name
.clone()
.unwrap_or_else(|| inner.default_sink.clone()),
inner.muted,
))?;
Ok(())
}
}
pub struct PulseAudio {
device: PulseAudioSoundDevice,
}
impl PulseAudio {
pub fn new(
font: FontRef,
font_size: f32,
length: u32,
sender: Sender<Cmd>,
) -> Result<Box<BarWidget>, ::std::io::Error> {
BarWidget::new(font, font_size, length, move |dirty| {
let device = PulseAudioSoundDevice::new(move || {
*dirty.lock().unwrap() = true;
sender.send(Cmd::Draw).unwrap();
})?;
Ok(Box::new(PulseAudio { device }))
})
}
}
impl BarWidgetImpl for PulseAudio {
fn wait(&mut self, _: &mut WaitContext) {}
fn name(&self) -> &str {
"volume"
}
fn value(&self) -> f32 {
self.device.volume()
}
fn color(&self) -> Color {
let muted = self.device.muted();
if muted {
Color::new(1.0, 1.0, 0.0, 1.0)
} else {
Color::new(1.0, 1.0, 1.0, 1.0)
}
}
fn inc(&mut self, inc: f32) {
self.device.inc_volume(inc).unwrap();
}
fn set(&mut self, val: f32) {
self.device.set_volume(val).unwrap();
}
fn toggle(&mut self) {
self.device.toggle().unwrap();
}
}
+109 -105
View File
@@ -1,27 +1,23 @@
use crate::color::Color; use std::{
use crate::widget::WaitContext; fs::{self, OpenOptions},
use crate::{ io::{Error, Write},
fonts::FontRef, path::{Path, PathBuf},
widgets::bar_widget::{BarWidget, BarWidgetImpl},
}; };
use std::fs; use walkdir::WalkDir;
use std::fs::OpenOptions;
use std::io::Write;
use std::io::{Error, ErrorKind};
use std::path::{Path, PathBuf};
pub struct Backlight { use crate::{
device_path: PathBuf, color::Color,
cur_brightness: u64, event::PointerButton,
max_brightness: u64, fonts::FontMap,
} widgets::bar_widget::{BarWidget, BarWidgetImpl},
};
fn read_file_as_u64(path: &Path) -> Result<u64, Error> { fn read_file_as_u64(path: &Path) -> Result<u64, Error> {
let mut s = fs::read_to_string(path)?; let mut s = fs::read_to_string(path)?;
s.pop(); s.pop();
s.parse::<u64>() s.parse::<u64>()
.map_err(|_e| Error::new(ErrorKind::Other, "unable to parse value")) .map_err(|_e| Error::other("unable to parse value"))
} }
fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> { fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
@@ -29,113 +25,121 @@ fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
file.write_fmt(format_args!("{}", value)) file.write_fmt(format_args!("{}", value))
} }
pub struct Backlight {
/// `None` when no backlight exists on this machine: the widget then
/// renders empty and ignores clicks instead of taking wldash down.
/// Configs are shared between hosts, so a laptop config running on a
/// desktop must not panic here.
device_path: Option<PathBuf>,
cur: u64,
max: u64,
dirty: bool,
}
/// First device under /sys/class/backlight, if any. `min_depth(1)` skips the
/// class directory itself, which otherwise got picked up as a "device".
fn find_device() -> Option<PathBuf> {
WalkDir::new("/sys/class/backlight")
.min_depth(1)
.max_depth(1)
.into_iter()
.flatten()
.map(|e| e.path().to_path_buf())
.next()
}
impl Backlight { impl Backlight {
pub fn update(&mut self) -> Result<(), Error> { fn update(&mut self) {
self.cur_brightness = read_file_as_u64(self.device_path.join("brightness").as_path())?; let Some(dev) = &self.device_path else {
self.max_brightness = read_file_as_u64(self.device_path.join("max_brightness").as_path())?; return;
Ok(()) };
if let Ok(cur) = read_file_as_u64(dev.join("brightness").as_path()) {
self.cur = cur;
}
if let Ok(max) = read_file_as_u64(dev.join("max_brightness").as_path()) {
self.max = max;
}
} }
pub fn sync(&mut self) -> Result<(), Error> { // Wraps the impl in the generic BarWidget on purpose.
write_file_as_u64( #[allow(clippy::new_ret_no_self)]
self.device_path.join("brightness").as_path(), pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
self.cur_brightness, let device_path = match path {
)?; Some(path) => {
self.update()?; let p = Path::new("/sys/class/backlight").join(path);
Ok(()) if p.is_dir() {
} Some(p)
pub fn brightness(&self) -> f32 {
if self.cur_brightness > self.max_brightness {
// what.
return 1.0;
}
self.cur_brightness as f32 / self.max_brightness as f32
}
pub fn set(&mut self, val: f32) -> Result<(), Error> {
self.cur_brightness = (self.max_brightness as f32 * val) as u64;
Ok(())
}
pub fn add(&mut self, diff: f32) -> Result<(), Error> {
let inc = (self.max_brightness as f32 * diff) as i64;
self.cur_brightness = if self.cur_brightness as i64 + inc < 1 {
1
} else if self.cur_brightness as i64 + inc > self.max_brightness as i64 {
self.max_brightness
} else { } else {
(self.cur_brightness as i64 + inc) as u64 eprintln!(
"wldash: backlight device {:?} not found, widget disabled",
p
);
None
}
}
None => {
let p = find_device();
if p.is_none() {
eprintln!(
"wldash: no backlight device under /sys/class/backlight, widget disabled"
);
}
p
}
};
let mut dev = Backlight {
device_path,
cur: 0,
max: 0,
dirty: true,
}; };
Ok(()) dev.update();
BarWidget::new(Box::new(dev), fm, font, size)
} }
pub fn new<'a>( pub fn detect() -> bool {
path: &str, find_device().is_some()
font: FontRef<'a>, }
font_size: f32,
length: u32, pub fn set(&mut self, brightness: f32) {
) -> Result<Box<BarWidget<'a>>, Error> { let Some(dev) = &self.device_path else {
let mut dev = Backlight { return;
device_path: Path::new("/sys/class/backlight").to_path_buf().join(path),
cur_brightness: 0,
max_brightness: 0,
}; };
let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64;
dev.update()?; let brightness_path = dev.join("brightness");
let _ = write_file_as_u64(&brightness_path, val);
Ok(BarWidget::new_simple( if let Ok(cur) = read_file_as_u64(&brightness_path) {
font, self.cur = cur;
font_size, }
length,
Box::new(dev),
))
} }
} }
impl BarWidgetImpl for Backlight { impl BarWidgetImpl for Backlight {
fn wait(&mut self, _: &mut WaitContext) {} fn name(&self) -> &'static str {
fn name(&self) -> &str {
"backlight" "backlight"
} }
fn value(&self) -> f32 { fn get_dirty(&self) -> bool {
self.brightness() self.dirty
}
fn value(&mut self) -> f32 {
self.dirty = false;
if self.device_path.is_none() {
return 0.0;
}
if self.max == 0 || self.cur > self.max {
// Sysfs handed us nonsense; show full rather than a bogus ratio.
return 1.0;
}
self.cur as f32 / self.max as f32
} }
fn color(&self) -> Color { fn color(&self) -> Color {
Color::new(1.0, 1.0, 1.0, 1.0) Color::WHITE
} }
fn inc(&mut self, inc: f32) { fn click(&mut self, pos: f32, btn: PointerButton) {
self.add(inc).unwrap(); self.dirty = true;
match self.sync() { if let PointerButton::Left = btn {
Ok(val) => val, self.set(pos)
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
}
}
}
fn set(&mut self, val: f32) {
self.set(val).unwrap();
match self.sync() {
Ok(val) => val,
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
}
}
}
fn toggle(&mut self) {
self.cur_brightness = if self.cur_brightness == 1 {
self.max_brightness
} else {
1
}; };
match self.sync() {
Ok(val) => val,
Err(err) => {
eprintln!("Error while trying to change brightness: {}", err);
}
}
} }
} }
+84 -139
View File
@@ -1,163 +1,108 @@
use crate::color::Color; use std::{cmp::min, default::Default};
use crate::draw::{draw_bar, draw_box, Font};
use crate::{ use crate::{
fonts::FontRef, buffer::BufferView,
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget}, color::Color,
draw::{draw_bar, draw_box},
event::{Event, PointerButton},
fonts::FontMap,
widgets::{Geometry, Widget},
}; };
use std::sync::{Arc, Mutex};
pub trait BarWidgetImpl { pub trait BarWidgetImpl {
fn wait(&mut self, ctx: &mut WaitContext); fn get_dirty(&self) -> bool {
fn name(&self) -> &str; false
fn value(&self) -> f32; }
fn name(&self) -> &'static str;
fn value(&mut self) -> f32;
fn color(&self) -> Color; fn color(&self) -> Color;
fn inc(&mut self, inc: f32); fn click(&mut self, _pos: f32, _btn: PointerButton) {}
fn set(&mut self, val: f32);
fn toggle(&mut self);
} }
pub struct BarWidget<'a> { pub struct BarWidget {
bar_impl: Box<dyn BarWidgetImpl + Send>, font: &'static str,
font: Font<'a>, size: f32,
font_size: u32, geometry: Geometry,
length: u32, inner_widget: Box<dyn BarWidgetImpl>,
dirty: Arc<Mutex<bool>>,
} }
impl<'a> BarWidget<'a> { impl BarWidget {
pub fn new_simple( pub fn new(
font: FontRef, inner_widget: Box<dyn BarWidgetImpl>,
font_size: f32, fm: &mut FontMap,
length: u32, font: &'static str,
w: Box<dyn BarWidgetImpl + Send>, size: f32,
) -> Box<BarWidget> { ) -> BarWidget {
let mut font = Font::new(font, font_size); fm.queue_font(font, size, inner_widget.name());
font.add_str_to_cache(w.name()); BarWidget {
geometry: Default::default(),
Box::new(BarWidget { inner_widget,
bar_impl: w,
dirty: Arc::new(Mutex::new(true)),
font, font,
font_size: font_size as u32, size,
length,
})
} }
pub fn new<F>(
font: FontRef<'a>,
font_size: f32,
length: u32,
f: F,
) -> Result<Box<BarWidget>, ::std::io::Error>
where
F: FnOnce(Arc<Mutex<bool>>) -> Result<Box<dyn BarWidgetImpl + Send>, ::std::io::Error>,
F: Send + 'a + Clone,
{
let dirty = Arc::new(Mutex::new(true));
let im = f(dirty.clone())?;
let mut font = Font::new(font, font_size);
font.add_str_to_cache(im.name());
Ok(Box::new(BarWidget {
bar_impl: im,
dirty,
font,
font_size: font_size as u32,
length,
}))
} }
} }
impl<'a> Widget for BarWidget<'a> { impl Widget for BarWidget {
fn wait(&mut self, ctx: &mut WaitContext) { fn geometry(&self) -> Geometry {
self.bar_impl.wait(ctx); self.geometry
}
fn enter(&mut self) {}
fn leave(&mut self) {}
fn size(&self) -> (u32, u32) {
(self.length, self.font_size)
}
fn draw(
&mut self,
ctx: &mut DrawContext,
pos: (u32, u32),
expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
if self.length == 0 {
self.length = expansion.0;
}
let (width, height) = (self.length, self.font_size);
{
let mut d = self.dirty.lock().unwrap();
if !*d && !ctx.force {
return Ok(DrawReport::empty(width, height));
}
*d = false;
} }
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?; fn get_dirty(&self) -> bool {
buf.memset(ctx.bg); self.inner_widget.get_dirty()
let c = Color::new(1.0, 1.0, 1.0, 1.0);
self.font.draw_text(buf, ctx.bg, &c, self.bar_impl.name())?;
let c = self.bar_impl.color();
let bar_off = 5 * self.font_size;
let mut val = self.bar_impl.value();
draw_bar(
&mut buf.offset((bar_off, 0))?,
&c,
width - bar_off,
self.font_size,
val,
)?;
let mut iter = 1.0;
while val > 1.0 {
let c = &Color::new(0.75 / iter, 0.25 / iter, 0.25 / iter, 1.0);
val -= 1.0;
iter += 1.0;
draw_bar(
&mut buf.offset((bar_off, 0))?,
&c,
width - bar_off,
self.font_size,
val,
)?;
}
draw_box(
&mut buf.offset((bar_off, 0))?,
&c,
(width - bar_off, self.font_size),
)?;
Ok(DrawReport {
width,
height,
damage: vec![buf.get_signed_bounds()],
full_damage: false,
})
} }
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {} fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
fn mouse_click(&mut self, button: u32, (x, _): (u32, u32)) { let font = fonts.get_font(self.font, self.size);
*self.dirty.lock().unwrap() = true; let fg = Color::WHITE;
match button {
272 => { font.draw_text(view, fg, self.inner_widget.name()).unwrap();
let offset = 5 * self.font_size; let size = self.size.ceil() as u32;
if x > offset { let bar_offset = 4 * size;
self.bar_impl let val = self.inner_widget.value();
.set(((x - offset) + 1) as f32 / (self.length - offset) as f32);
let c = self.inner_widget.color();
let bar_width = self.geometry.width.saturating_sub(bar_offset);
if bar_width < 2 {
return self.geometry;
}
draw_bar(&mut view.offset((bar_offset, 0)), c, bar_width, size, val).unwrap();
draw_box(&mut view.offset((bar_offset, 0)), c, (bar_width, size)).unwrap();
self.geometry
}
fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
let w = min(geometry.width, 768);
let mut x = geometry.x;
if geometry.width > w {
x += geometry.width - w;
}
self.geometry = Geometry {
x,
y: geometry.y,
width: w,
height: self.size.ceil() as u32,
};
self.geometry
}
fn minimum_size(&mut self, _fonts: &mut FontMap) -> Geometry {
Geometry {
x: 0,
y: 0,
width: self.size.ceil() as u32 * 6,
height: self.size.ceil() as u32,
} }
} }
273 => self.bar_impl.toggle(),
x => { fn event(&mut self, event: &Event) {
dbg!(x); if let Event::PointerEvent(ev) = event {
let offset = 4 * self.size.ceil() as u32;
if ev.pos.0 >= offset && self.geometry.width > offset {
let val = (ev.pos.0 - offset) as f32 / (self.geometry.width - offset) as f32;
self.inner_widget.click(val, ev.button);
} }
} }
} }
fn mouse_scroll(&mut self, (_, y): (f64, f64), _: (u32, u32)) {
*self.dirty.lock().unwrap() = true;
self.bar_impl.inc(y as f32 / -800.0);
}
} }
+154 -208
View File
@@ -1,240 +1,186 @@
use crate::cmd::Cmd; use std::{
use crate::color::Color; error::Error,
use crate::widget::WaitContext; sync::{Arc, Mutex},
thread,
time::Duration,
};
use dbus::blocking::{
LocalConnection,
stdintf::org_freedesktop_dbus::{Properties, PropertiesPropertiesChanged},
};
use crate::{ use crate::{
fonts::FontRef, color::Color,
event::{Event, Events},
fonts::FontMap,
widgets::bar_widget::{BarWidget, BarWidgetImpl}, widgets::bar_widget::{BarWidget, BarWidgetImpl},
}; };
use std::sync::mpsc::Sender;
use std::sync::{Arc, Mutex};
use nix::poll::{PollFd, PollFlags};
fn get_upower_property(
con: &dbus::Connection,
device_path: &str,
property: &str,
) -> Result<dbus::Message, ::std::io::Error> {
let msg = dbus::Message::new_method_call(
"org.freedesktop.UPower",
device_path,
"org.freedesktop.DBus.Properties",
"Get",
)
.map_err(|_| {
::std::io::Error::new(
::std::io::ErrorKind::Other,
"could not send make dbus method call",
)
})?
.append2(
dbus::MessageItem::Str("org.freedesktop.UPower.Device".to_string()),
dbus::MessageItem::Str(property.to_string()),
);
con.send_with_reply_and_block(msg, 1000).map_err(|_| {
::std::io::Error::new(::std::io::ErrorKind::Other, "could not send dbus message")
})
}
// The widget is created on a different thread than where it will be used, so
// we need it to be Send. However, dbus::Connection has a void*, so it's not
// auto-derived. So, we make a small wrapper where we make it Send.
struct DbusConnection(dbus::Connection);
impl std::convert::AsRef<dbus::Connection> for DbusConnection {
fn as_ref(&self) -> &dbus::Connection {
&self.0
}
}
unsafe impl Send for DbusConnection {}
pub struct UpowerBattery {
device_path: String,
sender: Sender<Cmd>,
dirty: Arc<Mutex<bool>>,
state: UpowerBatteryState,
capacity: f64,
con: DbusConnection,
watch: dbus::Watch,
}
enum UpowerBatteryState { enum UpowerBatteryState {
Unknown,
Charging, Charging,
Discharging, Discharging,
Empty, Empty,
Full, FullyCharged,
NotCharging, PendingCharge,
Unknown, PendingDischarge,
} }
impl UpowerBattery { impl UpowerBatteryState {
pub fn from_device( fn from_dbus(val: u64) -> UpowerBatteryState {
dirty: Arc<Mutex<bool>>, match val {
sender: Sender<Cmd>, 1 => UpowerBatteryState::Charging,
device: &str, 2 => UpowerBatteryState::Discharging,
) -> Result<Self, ::std::io::Error> { 3 => UpowerBatteryState::Empty,
let con = dbus::Connection::get_private(dbus::BusType::System).map_err(|_| { 4 => UpowerBatteryState::FullyCharged,
::std::io::Error::new(::std::io::ErrorKind::Other, "unable to open dbus") 5 => UpowerBatteryState::PendingCharge,
})?; 6 => UpowerBatteryState::PendingDischarge,
let device_path = format!("/org/freedesktop/UPower/devices/{}", device); _ => UpowerBatteryState::Unknown,
}
}
}
let rule = format!( struct InnerBattery {
"type='signal',\ value: f32,
path='{}',\ state: UpowerBatteryState,
interface='org.freedesktop.DBus.Properties',\ dirty: bool,
member='PropertiesChanged'", }
&device_path
const DEVICE_PATH: &str = "/org/freedesktop/UPower/devices/DisplayDevice";
const DEVICE_IFACE: &str = "org.freedesktop.UPower.Device";
/// True if UPower reports a present battery. The DisplayDevice object exists
/// even on machines without a battery, so checking for mere reachability
/// (as before) made every desktop grow a battery widget.
fn battery_present() -> Result<bool, Box<dyn Error>> {
let conn = LocalConnection::new_system()?;
let proxy = conn.with_proxy(
"org.freedesktop.UPower",
DEVICE_PATH,
Duration::from_millis(500),
); );
con.add_match(&rule).map_err(|_| { let present: bool = proxy.get(DEVICE_IFACE, "IsPresent")?;
::std::io::Error::new( Ok(present)
::std::io::ErrorKind::Other, }
"unable to add match rule to dbus connection",
)
})?;
let upower_type: dbus::arg::Variant<u32> =
match get_upower_property(&con, &device_path, "Type")?.get1() {
Some(v) => v,
None => {
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"no such upower device",
));
}
};
// https://upower.freedesktop.org/docs/Device.html#Device:Type fn monitor(
if upower_type.0 != 2 { inner: Arc<Mutex<InnerBattery>>,
return Err(::std::io::Error::new( events: Arc<Mutex<Events>>,
::std::io::ErrorKind::Other, ) -> Result<(), Box<dyn Error>> {
"UPower device is not a battery.", let conn = LocalConnection::new_system()?;
)); let proxy = conn.with_proxy(
} "org.freedesktop.UPower",
DEVICE_PATH,
Duration::from_millis(500),
);
let capacity: f64 = proxy.get(DEVICE_IFACE, "Percentage")?;
let state: u32 = proxy.get(DEVICE_IFACE, "State")?;
let capacity: f64 = match get_upower_property(&con, &device_path, "Percentage")?
.get1::<dbus::arg::Variant<f64>>()
{ {
Some(v) => v.0, let mut inner = inner.lock().unwrap();
None => { inner.value = capacity as f32 / 100.;
return Err(::std::io::Error::new( inner.state = UpowerBatteryState::from_dbus(state as u64);
::std::io::ErrorKind::Other, inner.dirty = true;
"no such upower device",
));
} }
}; events.lock().unwrap().add_event(Event::PowerUpdate);
let state: UpowerBatteryState = match get_upower_property(&con, &device_path, "State")?
.get1::<dbus::arg::Variant<u32>>() proxy.match_signal(
{ move |c: PropertiesPropertiesChanged, _: &LocalConnection, _: &dbus::Message| {
Some(v) => match v.0 { if c.interface_name != DEVICE_IFACE {
1 => UpowerBatteryState::Charging, return true;
2 => UpowerBatteryState::Discharging, }
3 => UpowerBatteryState::Empty, let mut inner = inner.lock().unwrap();
4 => UpowerBatteryState::Full, for (key, value) in c.changed_properties {
5 => UpowerBatteryState::NotCharging, match key.as_str() {
6 => UpowerBatteryState::Discharging, "State" => {
_ => UpowerBatteryState::Unknown, if let Some(v) = value.0.as_u64() {
inner.state = UpowerBatteryState::from_dbus(v);
inner.dirty = true;
events.lock().unwrap().add_event(Event::PowerUpdate);
}
}
"Percentage" => {
if let Some(v) = value.0.as_f64() {
inner.value = v as f32 / 100.;
inner.dirty = true;
events.lock().unwrap().add_event(Event::PowerUpdate);
}
}
_ => (),
}
}
true
}, },
None => { )?;
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"no such upower device",
));
}
};
let fds = con.watch_fds(); loop {
if fds.len() != 1 { conn.process(Duration::from_millis(60000))?;
return Err(::std::io::Error::new(
::std::io::ErrorKind::Other,
"expected 1 watch fd from dbus",
));
}
Ok(UpowerBattery {
device_path,
con: DbusConnection(con),
dirty,
sender,
capacity,
state,
watch: fds[0],
})
}
pub fn new(
font: FontRef,
font_size: f32,
length: u32,
sender: Sender<Cmd>,
) -> Result<Box<BarWidget>, ::std::io::Error> {
BarWidget::new(font, font_size, length, move |dirty| {
let d = UpowerBattery::from_device(dirty, sender, "DisplayDevice")?;
Ok(Box::new(d))
})
} }
} }
impl BarWidgetImpl for UpowerBattery { fn start_monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) {
fn wait(&mut self, ctx: &mut WaitContext) { thread::Builder::new()
for _ in self .name("battmon".to_string())
.con .spawn(move || {
.as_ref() // The release profile uses panic = "abort": a panic here would take
.watch_handle(self.watch.fd(), dbus::WatchEvent::Readable as u32) // the whole dashboard down, so report and leave the bar at 0.
{ if let Err(e) = monitor(inner, events) {
let capacity = get_upower_property(self.con.as_ref(), &self.device_path, "Percentage") eprintln!("wldash: battery monitor stopped: {}", e);
.unwrap() }
.get1::<dbus::arg::Variant<f64>>() })
.unwrap() .unwrap();
.0; }
let state = match get_upower_property(self.con.as_ref(), &self.device_path, "State")
.unwrap() pub struct Battery {
.get1::<dbus::arg::Variant<u32>>() inner: Arc<Mutex<InnerBattery>>,
.unwrap() }
.0
{ impl Battery {
1 => UpowerBatteryState::Charging, // Wraps the impl in the generic BarWidget on purpose.
2 => UpowerBatteryState::Discharging, #[allow(clippy::new_ret_no_self)]
3 => UpowerBatteryState::Empty, pub fn new(
4 => UpowerBatteryState::Full, events: Arc<Mutex<Events>>,
5 => UpowerBatteryState::NotCharging, fm: &mut FontMap,
6 => UpowerBatteryState::Discharging, font: &'static str,
_ => UpowerBatteryState::Unknown, size: f32,
) -> BarWidget {
let battery = Battery {
inner: Arc::new(Mutex::new(InnerBattery {
value: 0.,
state: UpowerBatteryState::Unknown,
dirty: false,
})),
}; };
self.state = state; start_monitor(battery.inner.clone(), events);
self.capacity = capacity; BarWidget::new(Box::new(battery), fm, font, size)
*self.dirty.lock().unwrap() = true;
self.sender.send(Cmd::Draw).unwrap();
} }
ctx.fds pub fn detect() -> bool {
.push(PollFd::new(self.watch.fd(), PollFlags::POLLIN)); battery_present().unwrap_or(false)
} }
fn name(&self) -> &str { }
impl BarWidgetImpl for Battery {
fn get_dirty(&self) -> bool {
self.inner.lock().unwrap().dirty
}
fn name(&self) -> &'static str {
"battery" "battery"
} }
fn value(&self) -> f32 { fn value(&mut self) -> f32 {
(self.capacity as f32) / 100.0 let mut inner = self.inner.lock().unwrap();
inner.dirty = false;
inner.value
} }
fn color(&self) -> Color { fn color(&self) -> Color {
match self.state { let inner = self.inner.lock().unwrap();
UpowerBatteryState::Discharging | UpowerBatteryState::Unknown => { match inner.state {
if self.capacity > 10.0 { UpowerBatteryState::Charging | UpowerBatteryState::FullyCharged => Color::LIGHTGREEN,
Color::new(1.0, 1.0, 1.0, 1.0) _ if inner.value > 0.25 => Color::WHITE,
} else { _ if inner.value > 0.1 => Color::DARKORANGE,
Color::new(1.0, 0.5, 0.0, 1.0) _ => Color::RED,
} }
} }
UpowerBatteryState::Charging | UpowerBatteryState::Full => {
Color::new(0.5, 1.0, 0.5, 1.0)
}
UpowerBatteryState::NotCharging | UpowerBatteryState::Empty => {
Color::new(1.0, 0.5, 0.5, 1.0)
}
}
}
fn inc(&mut self, _: f32) {}
fn set(&mut self, _: f32) {}
fn toggle(&mut self) {}
} }
+271 -206
View File
@@ -1,37 +1,114 @@
use crate::buffer::Buffer; use std::default::Default;
use crate::color::Color;
use crate::draw::Font;
use crate::{ use crate::{
fonts::FontRef, buffer::BufferView,
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget}, color::Color,
event::Event,
fonts::FontMap,
widgets::{Geometry, Widget},
}; };
use chrono::{Datelike, NaiveDate, NaiveDateTime}; use chrono::{Datelike, Local, NaiveDate};
pub struct Calendar<'a> { pub struct Calendar {
cur_date: NaiveDate, font: &'static str,
month_font: &'static str,
sections_x: i32,
sections_y: i32,
shown_sections_x: u32,
shown_sections_y: u32,
size: f32,
dirty: bool, dirty: bool,
offset: f64, geometry: Geometry,
sections: u32, year_width: u32,
font_size: u32, date_width: u32,
calendar_cache: Font<'a>, day_width: u32,
month_cache: Font<'a>,
year_cache: Font<'a>,
day_cache: Font<'a>,
} }
impl<'a> Calendar<'a> { const DAY_FACTOR: f32 = 0.6;
fn draw_month( const YEAR_FACTOR: f32 = 0.75;
&self, const MONTH_FACTOR: f32 = 1.25;
buf: &mut Buffer, const DATE_FACTOR: f32 = 1.;
background_color: &Color,
orig: NaiveDate,
time: NaiveDate,
) -> Result<(i32, i32, i32, i32), ::std::io::Error> {
let mut time = time;
let mut y_off = 1;
let mut done = false;
const LINE_HEIGHT: f32 = 0.85;
impl Calendar {
pub fn new(
fm: &mut FontMap,
font: &'static str,
month_font: &'static str,
size: f32,
sections_x: i32,
sections_y: i32,
) -> Calendar {
fm.queue_font(font, size * DAY_FACTOR, "ADEFHIMNORSTUW");
fm.queue_font(font, size * YEAR_FACTOR, "0123456789");
fm.queue_font(font, size * DATE_FACTOR, "0123456789");
fm.queue_font(
month_font,
size * MONTH_FACTOR,
"ADFJMNOSabcehgilmnoprstuvy",
);
Calendar {
font,
month_font,
size,
sections_x,
sections_y,
shown_sections_x: 0,
shown_sections_y: 0,
dirty: true,
geometry: Default::default(),
date_width: 0,
day_width: 0,
year_width: 0,
}
}
fn draw_month(
&mut self,
fonts: &mut FontMap,
view: &mut BufferView,
orig: NaiveDate,
mut time: NaiveDate,
) {
let cal_width = 8 * self.date_width + 7 * (self.date_width / 2);
//
// Draw the week day
//
let day_offset = (self.date_width - self.day_width) / 2;
let date_offset = self.date_width + self.date_width / 2;
for idx in 1..8 {
let day_font = fonts.get_font(self.font, self.size * DAY_FACTOR);
let (wk_chr, c) = match idx {
1 => ("MON", Color::WHITE),
2 => ("TUE", Color::WHITE),
3 => ("WED", Color::WHITE),
4 => ("THU", Color::WHITE),
5 => ("FRI", Color::WHITE),
6 => ("SAT", Color::GREY80),
7 => ("SUN", Color::GREY80),
_ => panic!("impossible value"),
};
day_font
.draw_text(
&mut view.offset((
idx * date_offset + day_offset,
(self.size * MONTH_FACTOR).ceil() as u32
+ (self.size * DAY_FACTOR * 0.2).ceil() as u32,
)),
c,
wk_chr,
)
.unwrap();
}
//
// Draw the month
//
let month_font = fonts.get_font(self.month_font, self.size * MONTH_FACTOR);
let month_str = match time.month() { let month_str = match time.month() {
1 => "January", 1 => "January",
2 => "February", 2 => "February",
@@ -47,234 +124,222 @@ impl<'a> Calendar<'a> {
12 => "December", 12 => "December",
_ => panic!("impossible value"), _ => panic!("impossible value"),
}; };
month_font.draw_text(view, Color::WHITE, month_str).unwrap();
// //
// Draw the of the month // Draw the year
// //
self.month_cache.draw_text(
buf,
background_color,
&Color::new(1.0, 1.0, 1.0, 1.0),
month_str,
)?;
if time.year() != orig.year() { if time.year() != orig.year() {
self.year_cache.draw_text( let year_font = fonts.get_font(self.font, self.size * YEAR_FACTOR);
&mut buf.offset((self.font_size * 20, 0))?, year_font
background_color, .draw_text(
&Color::new(0.8, 0.8, 0.8, 1.0), &mut view.offset((cal_width - self.year_width, 0)),
Color::GREY80,
&format!("{:}", time.year()), &format!("{:}", time.year()),
)?; )
} .unwrap();
//
// Draw the week day
//
for idx in 1..8 {
let wk_chr = match idx {
1 => "MON",
2 => "TUE",
3 => "WED",
4 => "THU",
5 => "FRI",
6 => "SAT",
7 => "SUN",
_ => panic!("impossible value"),
};
self.day_cache.draw_text(
&mut buf.offset((
idx * self.font_size * 3 + self.font_size / 5,
y_off * self.font_size * 2 + self.font_size * 4,
))?,
background_color,
&Color::new(1.0, 1.0, 1.0, 1.0),
&wk_chr,
)?;
}
y_off += 1;
while !done {
let mut x_pos = 0;
//
// Find the start of this week
//
let mut wkday = time.weekday();
while wkday != chrono::Weekday::Mon {
x_pos += 1;
wkday = wkday.pred();
} }
let cal_font = fonts.get_font(self.font, self.size * DATE_FACTOR);
'row: for y_off in 1.. {
// //
// Draw the week number // Draw the week number
// //
let wk = time.iso_week(); let wk = time.iso_week();
self.calendar_cache.draw_text( cal_font
&mut buf.offset((0, y_off * self.font_size * 2 + self.font_size * 4))?, .draw_text(
background_color, &mut view.offset((
&Color::new(0.75, 0.75, 0.75, 1.0), 0,
y_off * (self.size * LINE_HEIGHT).ceil() as u32
+ (self.size * MONTH_FACTOR).ceil() as u32,
)),
if wk == orig.iso_week() {
Color::WHITE
} else {
Color::GREY75
},
&format!("{:02}", wk.week()), &format!("{:02}", wk.week()),
)?; )
x_pos += 1; .unwrap();
// //
// Draw the dates // Draw the dates
// //
while x_pos < 8 { let x_off = time.weekday().number_from_monday();
for x_pos in x_off..8 {
let c = if time.day() == orig.day() && time.month() == orig.month() { let c = if time.day() == orig.day() && time.month() == orig.month() {
Color::new(1.0, 1.0, 1.0, 1.0) Color::WHITE
} else if x_pos < 6 {
Color::GREY50
} else { } else {
Color::new(0.5, 0.5, 0.5, 1.0) Color::GREY35
}; };
self.calendar_cache.draw_text( cal_font
&mut buf.offset(( .draw_text(
x_pos * self.font_size * 3, &mut view.offset((
y_off * self.font_size * 2 + self.font_size * 4, x_pos * date_offset,
))?, y_off * (self.size * LINE_HEIGHT).ceil() as u32
background_color, + (self.size * MONTH_FACTOR).ceil() as u32,
&c, )),
c,
&format!("{:02}", time.day()), &format!("{:02}", time.day()),
)?; )
.unwrap();
let t = time.with_day(time.day() + 1); let t = time.with_day(time.day() + 1);
if t.is_none() { if t.is_none() {
done = true; break 'row;
break;
} }
time = t.unwrap(); time = t.unwrap();
x_pos += 1;
} }
y_off += 1;
} }
Ok(buf.get_signed_bounds())
} }
} }
impl<'a> Calendar<'a> { impl Widget for Calendar {
pub fn new( fn get_dirty(&self) -> bool {
time: NaiveDateTime, self.dirty
font_primary: FontRef<'a>,
font_secondary: FontRef<'a>,
font_size: f32,
sections: u32,
) -> Box<Calendar<'a>> {
let mut calendar_cache = Font::new(font_secondary, font_size * 2.0);
calendar_cache.add_str_to_cache("0123456789");
let mut month_cache = Font::new(font_primary, font_size * 4.0);
month_cache.add_str_to_cache("JanuryFebMchApilJgstSmOoNvD");
let mut year_cache = Font::new(font_primary, font_size * 1.5);
year_cache.add_str_to_cache("-0123456789");
let mut day_cache = Font::new(font_primary, font_size);
day_cache.add_str_to_cache("MONTUEWDHFRISA");
Box::new(Calendar {
cur_date: time.date(),
dirty: true,
offset: 0.0,
sections,
font_size: font_size as u32,
calendar_cache,
month_cache,
year_cache,
day_cache,
})
}
}
impl<'a> Widget for Calendar<'a> {
fn wait(&mut self, _: &mut WaitContext) {}
fn enter(&mut self) {}
fn leave(&mut self) {}
fn size(&self) -> (u32, u32) {
let cal_width = 7 * self.font_size * 3 + self.font_size * 2;
let cal_pad = self.font_size * 3;
(
cal_width * self.sections + cal_pad * (self.sections - 1),
(self.font_size as f32 * 21.5) as u32,
)
} }
fn draw( fn geometry(&self) -> Geometry {
&mut self, self.geometry
ctx: &mut DrawContext,
pos: (u32, u32),
_expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if ctx.time.date() == self.cur_date && !ctx.force && !self.dirty {
return Ok(DrawReport::empty(width, height));
} }
self.dirty = false;
self.cur_date = ctx.time.date();
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?; fn event(&mut self, event: &Event) {
buf.memset(ctx.bg); if let Event::NewMinute = event {
let time = ctx.time.date(); self.dirty = true;
}
}
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
if self.shown_sections_y == 0 || self.shown_sections_x == 0 {
return Geometry {
x: self.geometry.x,
y: self.geometry.y,
width: 0,
height: 0,
};
}
let time = Local::now().naive_local().date();
let cal_height =
(self.size * MONTH_FACTOR).ceil() as u32 + (self.size * LINE_HEIGHT * 8.).ceil() as u32;
let cal_width = 8 * self.date_width + 7 * (self.date_width / 2);
let cal_pad = self.date_width * 2;
let mut t = time.with_day(1).unwrap(); let mut t = time.with_day(1).unwrap();
let o = (self.offset / 100.0) as i32; let cals = self.shown_sections_y * self.shown_sections_x;
if cals >= 3 {
let cals = self.sections - 1; t = t.pred_opt().unwrap().with_day(1).unwrap();
let pre_cals = cals / 2;
for _ in 0..pre_cals {
t = t.pred().with_day(1).unwrap();
} }
if o != 0 { for ydx in 0..self.shown_sections_y {
let mut month = (t.month() - 1) as i32 + o; for idx in 0..self.shown_sections_x {
let mut year = t.year(); self.draw_month(
while month > 11 { fonts,
year += 1; &mut view.offset(((cal_width + cal_pad) * idx, cal_height * ydx)),
month -= 12; time,
} t,
while month < 0 { );
year -= 1;
month += 12;
}
t = t
.with_year(year)
.unwrap()
.with_month((month + 1) as u32)
.unwrap();
}
let cal_pad = 7 * self.font_size * 3 + self.font_size * 5;
for idx in 0..self.sections {
self.draw_month(&mut buf.offset((cal_pad * idx, 0))?, ctx.bg, time, t)?;
t = if t.month() == 12 { t = if t.month() == 12 {
t.with_year(t.year() + 1).unwrap().with_month(1).unwrap() t.with_year(t.year() + 1).unwrap().with_month(1).unwrap()
} else { } else {
t.with_month(t.month() + 1).unwrap() t.with_month(t.month() + 1).unwrap()
}; };
} }
Ok(DrawReport { }
width, self.dirty = false;
height, self.geometry
damage: vec![buf.get_signed_bounds()],
full_damage: false,
})
} }
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {} fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
fn mouse_click(&mut self, _: u32, (x, _): (u32, u32)) { let year_font = fonts.get_font(self.font, self.size * YEAR_FACTOR);
let cal_pad = 7 * self.font_size * 3 + self.font_size * 5; self.year_width = year_font.auto_widest("0123456789").unwrap() * 4;
let cals = self.sections - 1; let cal_font = fonts.get_font(self.font, self.size * DATE_FACTOR);
let pre_cals = cals / 2; self.date_width = cal_font.auto_widest("0123456789").unwrap() * 2;
if x < pre_cals * cal_pad { let day_font = fonts.get_font(self.font, self.size * DAY_FACTOR);
self.offset -= 100.0; self.day_width = day_font.auto_widest("ADEFHIMNORSTUW").unwrap() * 3;
} else if x >= (pre_cals + 1) * cal_pad {
self.offset += 100.0; let cal_width = 8 * self.date_width + 7 * (self.date_width / 2);
let cal_height =
(self.size * MONTH_FACTOR).ceil() as u32 + (self.size * LINE_HEIGHT * 8.).ceil() as u32;
let cal_pad = self.date_width * 2;
if geometry.width < cal_width || geometry.height < cal_height {
self.shown_sections_x = 0;
self.shown_sections_y = 0;
self.geometry = Default::default();
return self.geometry;
}
let possible_sections_x = 1 + (geometry.width - cal_width) / (cal_width + cal_pad);
let possible_sections_y = 1 + (geometry.height - cal_height) / cal_height;
self.shown_sections_x =
if self.sections_x > 0 && self.sections_x < possible_sections_x as i32 {
self.sections_x as u32
} else { } else {
self.offset = 0.0; possible_sections_x
};
self.shown_sections_y =
if self.sections_y > 0 && self.sections_y < possible_sections_y as i32 {
self.sections_y as u32
} else {
possible_sections_y
};
self.geometry = Geometry {
x: geometry.x,
y: geometry.y,
width: cal_width * self.shown_sections_x + cal_pad * (self.shown_sections_x - 1),
height: cal_height * self.shown_sections_y,
};
self.geometry
} }
self.dirty = true;
fn minimum_size(&mut self, fonts: &mut FontMap) -> Geometry {
let cal_font = fonts.get_font(self.font, self.size * DATE_FACTOR);
let date_width = cal_font.auto_widest("0123456789").unwrap() * 2;
let cal_width = 8 * date_width + 7 * (date_width / 2);
let cal_height =
(self.size * MONTH_FACTOR).ceil() as u32 + (self.size * LINE_HEIGHT * 8.).ceil() as u32;
let cal_pad = date_width * 2;
let sections_x = match self.sections_x {
-1 => 1,
0 => {
return Geometry {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
x => x,
};
let sections_y = match self.sections_y {
-1 => 1,
0 => {
return Geometry {
x: 0,
y: 0,
width: 0,
height: 0,
};
}
y => y,
};
let width = cal_width * sections_x as u32 + cal_pad * (sections_x as u32 - 1);
let height = cal_height * sections_y as u32;
Geometry {
x: 0,
y: 0,
width,
height,
} }
fn mouse_scroll(&mut self, (_, y): (f64, f64), _: (u32, u32)) {
self.offset += y;
self.dirty = true;
} }
} }
+72 -71
View File
@@ -1,92 +1,93 @@
use crate::color::Color; use std::default::Default;
use crate::draw::Font;
use crate::{ use crate::{
fonts::FontRef, buffer::BufferView,
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget}, color::Color,
event::Event,
fonts::FontMap,
widgets::{Geometry, Widget},
}; };
use chrono::{Duration, NaiveDateTime, Timelike}; use chrono::{Local, Timelike};
pub struct Clock<'a> { pub struct Clock {
cur_time: NaiveDateTime, font: &'static str,
clock_cache: Font<'a>,
size: f32, size: f32,
digit: u32, dirty: bool,
colon: u32, geometry: Geometry,
digit_width: u32,
colon_width: u32,
} }
impl<'a> Clock<'a> { impl Clock {
pub fn new(time: NaiveDateTime, font: FontRef, size: f32) -> ::std::io::Result<Box<Clock>> { pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> Clock {
let mut clock_cache = Font::new(font, size); fm.queue_font(font, size, "0123456789: ");
clock_cache.add_str_to_cache("0123456789:"); Clock {
font,
let digit = clock_cache.auto_widest("123456789")?;
let colon = clock_cache.auto_widest(":")?;
Ok(Box::new(Clock {
cur_time: time,
clock_cache,
size, size,
digit, dirty: true,
colon, geometry: Default::default(),
})) digit_width: 0,
colon_width: 0,
}
} }
} }
impl<'a> Widget for Clock<'a> { impl Widget for Clock {
fn wait(&mut self, ctx: &mut WaitContext) { fn get_dirty(&self) -> bool {
let target = (self.cur_time + Duration::seconds(60)) self.dirty
.with_second(0)
.unwrap()
.with_nanosecond(0)
.unwrap();
ctx.set_time(target);
}
fn enter(&mut self) {}
fn leave(&mut self) {}
fn size(&self) -> (u32, u32) {
(self.digit * 4 + self.colon, self.size.ceil() as u32)
} }
fn draw( fn geometry(&self) -> Geometry {
&mut self, self.geometry
ctx: &mut DrawContext,
pos: (u32, u32),
_expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if !(ctx.time.date() != self.cur_time.date()
|| ctx.time.hour() != self.cur_time.hour()
|| ctx.time.minute() != self.cur_time.minute()
|| ctx.force)
{
return Ok(DrawReport::empty(width, height));
} }
self.cur_time = ctx.time; fn event(&mut self, event: &Event) {
if let Event::NewMinute = event {
self.dirty = true;
}
}
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?; fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
buf.memset(ctx.bg); let time = Local::now().naive_local();
let fg = Color::WHITE;
let digit = self.digit; let digit = self.digit_width;
let colon = self.colon; let colon = self.colon_width;
self.clock_cache.draw_text_fixed_width( let font = fonts.get_font(self.font, self.size);
buf, font.draw_text_fixed_width(
ctx.bg, view,
&Color::new(1.0, 1.0, 1.0, 1.0), fg,
&[digit, digit, colon, digit, digit], &[digit, digit, colon, digit, digit],
&format!("{:02}:{:02}", ctx.time.hour(), ctx.time.minute()), &format!("{:02}:{:02}", time.hour(), time.minute()),
)?; )
.unwrap();
Ok(DrawReport { self.dirty = false;
width, self.geometry
height,
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
full_damage: false,
})
} }
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {} fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {} let font = fonts.get_font(self.font, self.size);
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {} self.digit_width = font.auto_widest("0123456789").unwrap();
self.colon_width = font.auto_widest(":").unwrap();
self.geometry = Geometry {
x: geometry.x,
y: geometry.y,
width: self.digit_width * 4 + self.colon_width,
height: self.size.ceil() as u32,
};
self.geometry
}
fn minimum_size(&mut self, fonts: &mut FontMap) -> Geometry {
let font = fonts.get_font(self.font, self.size);
let digit_width = font.auto_widest("0123456789").unwrap();
let colon_width = font.auto_widest(":").unwrap();
Geometry {
x: 0,
y: 0,
width: digit_width * 4 + colon_width,
height: self.size.ceil() as u32,
}
}
} }
+68 -74
View File
@@ -1,95 +1,89 @@
use crate::color::Color; use std::default::Default;
use crate::draw::Font;
use crate::{ use crate::{
fonts::FontRef, buffer::BufferView,
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget}, color::Color,
event::Event,
fonts::FontMap,
widgets::{Geometry, Widget},
}; };
use chrono::{Datelike, NaiveDateTime}; use chrono::{Datelike, Local};
pub struct Date<'a> { pub struct Date {
cur_time: NaiveDateTime, font: &'static str,
date_cache: Font<'a>,
size: f32, size: f32,
ch_width: u32, dirty: bool,
digit_width: u32, geometry: Geometry,
spacing_width: u32,
} }
impl<'a> Date<'a> { impl Date {
pub fn new(time: NaiveDateTime, font: FontRef, size: f32) -> ::std::io::Result<Box<Date>> { pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> Date {
let mut date_cache = Font::new(font, size); fm.queue_font(font, size, "0123456789/, adehinortuFMSTW");
let chs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; Date {
let digits = "0123456789,/ "; font,
let spacings = ",/ ";
date_cache.add_str_to_cache(chs);
date_cache.add_str_to_cache(digits);
date_cache.add_str_to_cache(spacings);
let ch_width = date_cache.auto_widest(chs)?;
let digit_width = date_cache.auto_widest(digits)?;
let spacing_width = date_cache.auto_widest(spacings)?;
Ok(Box::new(Date {
cur_time: time,
date_cache,
size, size,
ch_width, dirty: true,
digit_width, geometry: Default::default(),
spacing_width, }
}))
} }
} }
impl<'a> Widget for Date<'a> { impl Widget for Date {
fn wait(&mut self, _: &mut WaitContext) {} fn get_dirty(&self) -> bool {
fn enter(&mut self) {} self.dirty
fn leave(&mut self) {}
fn size(&self) -> (u32, u32) {
(
(3 * self.ch_width + 8 * self.digit_width + 4 * self.spacing_width) as u32,
self.size.ceil() as u32,
)
} }
fn draw( fn geometry(&self) -> Geometry {
&mut self, self.geometry
ctx: &mut DrawContext,
pos: (u32, u32),
_expansion: (u32, u32),
) -> Result<DrawReport, ::std::io::Error> {
let (width, height) = self.size();
if !(ctx.time.date() != self.cur_time.date() || ctx.force) {
return Ok(DrawReport::empty(width, height));
} }
self.cur_time = ctx.time; fn event(&mut self, event: &Event) {
if let Event::NewMinute = event {
self.dirty = true;
}
}
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?; fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
buf.memset(ctx.bg); let time = Local::now().naive_local();
self.date_cache.draw_text( let fg = Color::WHITE;
buf, let font = fonts.get_font(self.font, self.size);
ctx.bg, font.draw_text(
&Color::new(1.0, 1.0, 1.0, 1.0), view,
fg,
&format!( &format!(
"{:?}, {:02}/{:02}/{:4}", "{}, {:02}/{:02}/{:4}",
ctx.time.weekday(), time.weekday(),
ctx.time.day(), time.day(),
ctx.time.month(), time.month(),
ctx.time.year() time.year()
), ),
)?; )
.unwrap();
Ok(DrawReport { self.dirty = false;
width, self.geometry
height,
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
full_damage: false,
})
} }
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {} fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {} let width = (3. + 2. + 2. + 2. + 4.) * self.size / 2.;
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {} let font = fonts.get_font(self.font, self.size);
self.geometry = Geometry {
x: geometry.x,
y: geometry.y,
width: width.ceil() as u32,
height: font.height().ceil() as u32,
};
self.geometry
}
fn minimum_size(&mut self, fonts: &mut FontMap) -> Geometry {
let width = (3. + 2. + 2. + 2. + 4.) * self.size / 2.;
let font = fonts.get_font(self.font, self.size);
Geometry {
x: 0,
y: 0,
width: width.ceil() as u32,
height: font.height().ceil() as u32,
}
}
} }
+829 -421
View File
File diff suppressed because it is too large Load Diff
+286
View File
@@ -0,0 +1,286 @@
// Constructors here deliberately return Box<dyn Layout> so configs compose.
#![allow(clippy::new_ret_no_self)]
use std::cmp::max;
use crate::{fonts::FontMap, state::State, widgets::Geometry};
pub trait Layout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry;
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry;
}
pub struct HorizontalLayout {
pub widgets: Vec<Box<dyn Layout>>,
}
impl HorizontalLayout {
pub fn new(widgets: Vec<Box<dyn Layout>>) -> Box<dyn Layout> {
Box::new(HorizontalLayout { widgets })
}
}
impl Layout for HorizontalLayout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
let mut geo = *geometry;
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.geometry_update(fonts, &geo, state);
geo.x = result.x + result.width;
geo.width -= result.width;
max_width += result.width;
max_height = max(result.height, max_height);
}
geo.width = max_width;
geo.height = max_height;
geo
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.minimum_size(fonts, state);
max_width += result.x + result.width;
max_height = max(result.y + result.height, max_height);
}
Geometry {
width: max_width,
height: max_height,
x: 0,
y: 0,
}
}
}
pub struct VerticalLayout {
pub widgets: Vec<Box<dyn Layout>>,
}
impl VerticalLayout {
pub fn new(widgets: Vec<Box<dyn Layout>>) -> Box<dyn Layout> {
Box::new(VerticalLayout { widgets })
}
}
impl Layout for VerticalLayout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
let mut geo = *geometry;
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.geometry_update(fonts, &geo, state);
geo.y = result.y + result.height;
geo.height -= result.height;
max_width = max(result.width, max_width);
max_height += result.height;
}
geo.width = max_width;
geo.height = max_height;
geo
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.minimum_size(fonts, state);
max_width = max(result.x + result.width, max_width);
max_height += result.y + result.height;
}
Geometry {
width: max_width,
height: max_height,
x: 0,
y: 0,
}
}
}
pub trait WidgetUpdater {
fn geometry_update(&mut self, idx: usize, fonts: &mut FontMap, geometry: &Geometry)
-> Geometry;
fn minimum_size(&mut self, idx: usize, fonts: &mut FontMap) -> Geometry;
}
pub struct IndexedLayout {
pub widget_idx: usize,
}
impl IndexedLayout {
pub fn new(widget_idx: usize) -> Box<dyn Layout> {
Box::new(IndexedLayout { widget_idx })
}
}
impl Layout for IndexedLayout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
state.geometry_update(self.widget_idx, fonts, geometry)
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
state.minimum_size(self.widget_idx, fonts)
}
}
pub struct InvertedHorizontalLayout {
pub widgets: Vec<Box<dyn Layout>>,
}
impl InvertedHorizontalLayout {
pub fn new(widgets: Vec<Box<dyn Layout>>) -> Box<dyn Layout> {
Box::new(InvertedHorizontalLayout { widgets })
}
}
impl Layout for InvertedHorizontalLayout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
let mut geo = *geometry;
let mut max_height = 0;
for w in self.widgets.iter() {
let mut temp_geo = geo;
let temp_result = w.geometry_update(fonts, &temp_geo, state);
temp_geo.x = geo.x + (geo.width - temp_result.width);
let result = w.geometry_update(fonts, &temp_geo, state);
geo.width -= result.width;
max_height = max(result.height, max_height);
}
geo.height = max_height;
geo
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.minimum_size(fonts, state);
max_width += result.x + result.width;
max_height = max(result.y + result.height, max_height);
}
Geometry {
width: max_width,
height: max_height,
x: 0,
y: 0,
}
}
}
pub struct InvertedVerticalLayout {
pub widgets: Vec<Box<dyn Layout>>,
}
impl InvertedVerticalLayout {
pub fn new(widgets: Vec<Box<dyn Layout>>) -> Box<dyn Layout> {
Box::new(InvertedVerticalLayout { widgets })
}
}
impl Layout for InvertedVerticalLayout {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
let mut geo = *geometry;
let mut max_width = 0;
for w in self.widgets.iter() {
let mut temp_geo = geo;
let temp_result = w.geometry_update(fonts, &temp_geo, state);
temp_geo.y = geo.y + (geo.height - temp_result.height);
let result = w.geometry_update(fonts, &temp_geo, state);
geo.height -= result.height;
max_width = max(result.width, max_width);
}
geo.width = max_width;
geo
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
let mut max_width = 0;
let mut max_height = 0;
for w in self.widgets.iter() {
let result = w.minimum_size(fonts, state);
max_width = max(result.x + result.width, max_width);
max_height += result.y + result.height;
}
Geometry {
width: max_width,
height: max_height,
x: 0,
y: 0,
}
}
}
pub struct Margin {
pub widget: Box<dyn Layout>,
pub margin: (u32, u32, u32, u32),
}
impl Margin {
pub fn new(widget: Box<dyn Layout>, margin: (u32, u32, u32, u32)) -> Box<dyn Layout> {
Box::new(Margin { widget, margin })
}
}
impl Layout for Margin {
fn geometry_update(
&self,
fonts: &mut FontMap,
geometry: &Geometry,
state: &mut State,
) -> Geometry {
let geo = Geometry {
x: geometry.x + self.margin.0,
y: geometry.y + self.margin.1,
width: geometry.width - self.margin.0 - self.margin.2,
height: geometry.height - self.margin.1 - self.margin.3,
};
let out = self.widget.geometry_update(fonts, &geo, state);
Geometry {
x: out.x - self.margin.0,
y: out.y - self.margin.1,
width: out.width + self.margin.0 + self.margin.2,
height: out.height + self.margin.1 + self.margin.3,
}
}
fn minimum_size(&self, fonts: &mut FontMap, state: &mut State) -> Geometry {
let size = self.widget.minimum_size(fonts, state);
Geometry {
width: size.x + size.width + self.margin.0 + self.margin.2,
height: size.y + size.height + self.margin.1 + self.margin.3,
x: 0,
y: 0,
}
}
}
+62
View File
@@ -0,0 +1,62 @@
use std::default::Default;
use crate::{
buffer::BufferView,
color::Color,
fonts::FontMap,
widgets::{Geometry, Widget},
};
pub struct Line {
geometry: Geometry,
thickness: u32,
vertical: bool,
}
impl Line {
pub fn new(thickness: u32, vertical: bool) -> Line {
Line {
geometry: Default::default(),
thickness,
vertical,
}
}
}
impl Widget for Line {
fn geometry(&self) -> Geometry {
self.geometry
}
fn draw(&mut self, _fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
view.memset(Color::WHITE);
self.geometry
}
fn geometry_update(&mut self, _fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
self.geometry = Geometry {
x: geometry.x,
y: geometry.y,
width: if self.vertical {
self.thickness
} else {
geometry.width
},
height: if self.vertical {
geometry.height
} else {
self.thickness
},
};
self.geometry
}
fn minimum_size(&mut self, _fonts: &mut FontMap) -> Geometry {
Geometry {
x: 0,
y: 0,
width: if self.vertical { self.thickness } else { 1 },
height: if self.vertical { 1 } else { self.thickness },
}
}
}
+26 -9
View File
@@ -1,10 +1,27 @@
pub mod backlight; //mod alsaaudio;
pub mod bar_widget; mod backlight;
pub mod battery; mod bar_widget;
pub mod calendar; mod battery;
pub mod clock; mod calendar;
pub mod date; mod clock;
pub mod launcher; mod date;
mod launcher;
mod layout;
mod line;
mod pulseaudio;
mod widget;
#[cfg(any(feature = "alsa-widget", feature = "pulseaudio-widget"))] //pub use alsaaudio::AlsaAudio;
pub mod audio; pub use backlight::Backlight;
pub use battery::Battery;
pub use calendar::Calendar;
pub use clock::Clock;
pub use date::Date;
pub use launcher::Interface;
pub use layout::{
HorizontalLayout, IndexedLayout, InvertedHorizontalLayout, InvertedVerticalLayout, Layout,
Margin, VerticalLayout, WidgetUpdater,
};
pub use line::Line;
pub use pulseaudio::PulseAudio;
pub use widget::{Geometry, Widget};
+256
View File
@@ -0,0 +1,256 @@
use std::{
cell::RefCell,
error::Error,
os::fd::RawFd,
rc::Rc,
sync::{Arc, Mutex},
thread,
};
use nix::unistd::{pipe, read, write};
use crate::{
color::Color,
event::{Event, Events, PointerButton},
fonts::FontMap,
widgets::bar_widget::{BarWidget, BarWidgetImpl},
};
use libpulse_binding::volume::ChannelVolumes;
fn monitor(
events: Arc<Mutex<Events>>,
inner: Arc<Mutex<InnerAudio>>,
pipe: RawFd,
) -> Result<(), Box<dyn Error>> {
use libpulse_binding::{
callbacks::ListResult,
context::{
Context, FlagSet, State,
subscribe::{Facility, InterestMaskSet},
},
mainloop::{
self, api::Mainloop, events::io::FlagSet as IoFlagSet, standard::IterateResult,
},
proplist::{Proplist, properties},
volume::Volume,
};
let mut proplist = Proplist::new().unwrap();
proplist
.set_str(properties::APPLICATION_NAME, "wldash")
.unwrap();
let mut mainloop = mainloop::standard::Mainloop::new().unwrap();
let context = Rc::new(RefCell::new(
Context::new_with_proplist(&mainloop, "wldash_context", &proplist).unwrap(),
));
context.borrow_mut().connect(None, FlagSet::NOFLAGS, None)?;
loop {
match mainloop.iterate(true) {
IterateResult::Quit(_) | IterateResult::Err(_) => {
return Err("pulseaudio mainloop quit while connecting".into());
}
_ => (),
}
match context.borrow().get_state() {
State::Ready => break,
State::Failed | State::Terminated => {
return Err("could not connect to pulseaudio".into());
}
_ => (),
}
}
let local_context = context.clone();
let local_inner = inner.clone();
let local_events = events.clone();
context.borrow_mut().set_subscribe_callback(Some(Box::new(
move |facility, _operation, index| {
if let Some(Facility::Sink) = facility {
let introspector = local_context.borrow_mut().introspect();
let inner = local_inner.clone();
let events = local_events.clone();
introspector.get_sink_info_by_index(index, move |res| match res {
ListResult::End | ListResult::Error => (),
ListResult::Item(sink_info) => {
if sink_info.name.is_some() {
let avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
let mut inner = inner.lock().unwrap();
inner.volume = avg;
inner.mute = sink_info.mute;
inner.dirty = true;
drop(inner);
let mut events = events.lock().unwrap();
events.add_event(Event::AudioUpdate);
}
}
});
}
},
)));
context
.borrow_mut()
.subscribe(InterestMaskSet::SINK, |_| {});
let introspector = context.borrow_mut().introspect();
let local_inner = inner.clone();
introspector.get_sink_info_by_name("@DEFAULT_SINK@", move |res| match res {
ListResult::End | ListResult::Error => (),
ListResult::Item(sink_info) => {
if sink_info.name.is_some() {
let avg = sink_info.volume.avg().0 as f32 / Volume::NORMAL.0 as f32;
let mut inner = local_inner.lock().unwrap();
inner.volume = avg;
inner.channel_volume = Some(sink_info.volume);
inner.mute = sink_info.mute;
inner.dirty = true;
drop(inner);
let mut events = events.lock().unwrap();
events.add_event(Event::AudioUpdate);
}
}
});
drop(introspector);
let local_context = context.clone();
let local_inner = inner.clone();
let event_source = mainloop.new_io_event(
pipe,
IoFlagSet::INPUT,
Box::new(move |_, _, _| {
let mut buf = [0; 8];
_ = read(pipe, &mut buf);
let mut inner = local_inner.lock().unwrap();
let mut introspector = local_context.borrow_mut().introspect();
// Clicks arriving before the first sink query finished have no
// channel map to work with; drop them instead of panicking.
match (inner.last_click.take(), inner.channel_volume) {
(Some(Change::Volume(volume)), Some(mut vol)) => {
vol.scale(Volume((volume * Volume::NORMAL.0 as f32).round() as u32));
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
}
(Some(Change::VolumeInc(value)), Some(mut vol)) => {
// apply step to volumes
let step = (value * Volume::NORMAL.0 as f32).round() as i32;
if step > 0 {
vol.inc_clamp(Volume(step as u32), Volume::NORMAL);
} else {
vol.decrease(Volume(-step as u32));
}
// HACK: It would be better to only update the volume from feedback, but axis
// events fire fast and require the last set volume immediately for reference.
inner.channel_volume = Some(vol);
introspector.set_sink_volume_by_name("@DEFAULT_SINK@", &vol, None);
}
(Some(Change::ToggleMute), _) => {
introspector.set_sink_mute_by_name("@DEFAULT_SINK@", !inner.mute, None);
}
_ => (),
}
}),
);
loop {
match mainloop.iterate(true) {
IterateResult::Quit(_) | IterateResult::Err(_) => break,
_ => (),
}
}
// Need to keep the event source alive
_ = event_source;
Ok(())
}
fn start_monitor(events: Arc<Mutex<Events>>, inner: Arc<Mutex<InnerAudio>>, pipe: RawFd) {
thread::Builder::new()
.name("audiomon".to_string())
.spawn(move || {
// panic = "abort" in release: a failed audio connection must not
// kill the dashboard. The widget simply stays at 0.
if let Err(e) = monitor(events, inner, pipe) {
eprintln!("wldash: audio monitor stopped: {}", e);
}
})
.unwrap();
}
enum Change {
Volume(f32),
VolumeInc(f32),
ToggleMute,
}
struct InnerAudio {
volume: f32,
channel_volume: Option<ChannelVolumes>,
mute: bool,
dirty: bool,
last_click: Option<Change>,
pipe: RawFd,
}
pub struct PulseAudio {
inner: Arc<Mutex<InnerAudio>>,
}
impl PulseAudio {
// Wraps the impl in the generic BarWidget on purpose.
#[allow(clippy::new_ret_no_self)]
pub fn new(
events: Arc<Mutex<Events>>,
fm: &mut FontMap,
font: &'static str,
size: f32,
) -> BarWidget {
let (a, b) = pipe().unwrap();
let audio = PulseAudio {
inner: Arc::new(Mutex::new(InnerAudio {
volume: 0.0,
channel_volume: None,
mute: false,
dirty: false,
last_click: None,
pipe: b,
})),
};
start_monitor(events, audio.inner.clone(), a);
BarWidget::new(Box::new(audio), fm, font, size)
}
}
impl BarWidgetImpl for PulseAudio {
fn get_dirty(&self) -> bool {
self.inner.lock().unwrap().dirty
}
fn name(&self) -> &'static str {
"volume"
}
fn value(&mut self) -> f32 {
let mut inner = self.inner.lock().unwrap();
inner.dirty = false;
inner.volume
}
fn color(&self) -> Color {
match self.inner.lock().unwrap().mute {
true => Color::YELLOW,
false => Color::WHITE,
}
}
fn click(&mut self, pos: f32, btn: PointerButton) {
let mut inner = self.inner.lock().unwrap();
inner.last_click = match btn {
PointerButton::Left => Some(Change::Volume(pos)),
PointerButton::Right => Some(Change::ToggleMute),
PointerButton::ScrollVertical(val) => Some(Change::VolumeInc((val / 512.) as f32)),
PointerButton::ScrollHorizontal(val) => Some(Change::VolumeInc((val / 100.) as f32)),
_ => None,
};
_ = write(inner.pipe, &[0]);
}
}
+96
View File
@@ -0,0 +1,96 @@
use std::{
cmp::{max, min},
fmt,
};
use crate::{buffer::BufferView, event::Event, fonts::FontMap};
#[derive(Debug, Default, Clone, Copy)]
pub struct Geometry {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl fmt::Display for Geometry {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"(x: {}, y: {}, width: {}, height: {})",
self.x, self.y, self.width, self.height
)
}
}
impl Geometry {
pub fn new() -> Geometry {
Geometry {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
pub fn expand(&self, other: Geometry) -> Geometry {
let x = min(self.x, other.x);
let y = min(self.y, other.y);
Geometry {
x,
y,
width: max(self.width + self.x, other.width + other.x) - x,
height: max(self.height + self.y, other.height + other.y) - y,
}
}
pub fn contains(&self, position: (u32, u32)) -> bool {
position.0 >= self.x
&& position.0 < self.x + self.width
&& position.1 >= self.y
&& position.1 < self.y + self.height
}
}
pub trait Widget {
fn get_dirty(&self) -> bool {
false
}
fn geometry(&self) -> Geometry;
fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry;
fn minimum_size(&mut self, _fonts: &mut FontMap) -> Geometry {
Default::default()
}
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry;
fn event(&mut self, _event: &Event) {}
}
#[cfg(test)]
mod tests {
use super::Geometry;
#[test]
fn expand_is_bounding_box() {
let a = Geometry {
x: 10,
y: 20,
width: 5,
height: 5,
};
let b = Geometry {
x: 0,
y: 100,
width: 2,
height: 2,
};
let e = a.expand(b);
assert_eq!((e.x, e.y), (0, 20));
assert_eq!(e.x + e.width, 15);
assert_eq!(e.y + e.height, 102);
let e2 = b.expand(a);
assert_eq!(
(e2.x, e2.y, e2.width, e2.height),
(e.x, e.y, e.width, e.height)
);
}
}