Compare commits
102
Commits
wayland_bump
...
v0.4.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
63179b9c47 | ||
|
|
d1df30d6d0 | ||
|
|
9953865128 | ||
|
|
03a3c681b0 | ||
|
|
1c6a536026 | ||
|
|
42ffe9c492 | ||
|
|
856f8d8bf4 | ||
|
|
eea3a78140 | ||
|
|
8c319cffc6 | ||
|
|
450a20bf63 | ||
|
|
6af81b587c | ||
|
|
c033335912 | ||
|
|
01af00b5ea | ||
|
|
2bed8b7fac | ||
|
|
1367bd8591 | ||
|
|
1156b3503a | ||
|
|
d80a64444f | ||
|
|
456d61de01 | ||
|
|
ab2655556f | ||
|
|
f0331e65b1 | ||
|
|
25c7322c4b | ||
|
|
7af5c34856 | ||
|
|
e8def40fae | ||
|
|
34591f2677 | ||
|
|
91c26b9d54 | ||
|
|
a8cd777ca9 | ||
|
|
83179d2c7e | ||
|
|
7e112ddd1f | ||
|
|
9fd054fc2c | ||
|
|
40aec97f79 | ||
|
|
9031361dd1 | ||
|
|
f337ce6e95 | ||
|
|
bc9ba0045c | ||
|
|
2b3048faca | ||
|
|
f09abf1d53 | ||
|
|
5776886884 | ||
|
|
7ee58d8d68 | ||
|
|
c169843bcb | ||
|
|
1d1f7d3a8e | ||
|
|
a882e26e2b | ||
|
|
84c241a90e | ||
|
|
921938cc0d | ||
|
|
8835708b31 | ||
|
|
c01c810c10 | ||
|
|
b451a2de31 | ||
|
|
ebeb21f3c2 | ||
|
|
558d528f70 | ||
|
|
6351cc242d | ||
|
|
66e66e19ad | ||
|
|
3a77696569 | ||
|
|
a975da5074 | ||
|
|
9d1f310f12 | ||
|
|
54e4ba16dd | ||
|
|
764a1e3444 | ||
|
|
6fac465c3a | ||
|
|
5097faab66 | ||
|
|
8202963a6f | ||
|
|
b09f642fd6 | ||
|
|
59e2f658db | ||
|
|
022f6a719a | ||
|
|
8826cab89d | ||
|
|
9e48eb053d | ||
|
|
db085168dc | ||
|
|
353ed1f189 | ||
|
|
45e5a750f9 | ||
|
|
6e90b1f0e8 | ||
|
|
3c69a31030 | ||
|
|
b9f4e74062 | ||
|
|
6f0ca36826 | ||
|
|
69959e86dc | ||
|
|
74591902e9 | ||
|
|
3a2badbfd9 | ||
|
|
b49b9e9764 | ||
|
|
7593cd48a4 | ||
|
|
3a40bb18c5 | ||
|
|
b2a9475e4c | ||
|
|
be6ee9899c | ||
|
|
d3002d2180 | ||
|
|
25644c2b15 | ||
|
|
dd790468ad | ||
|
|
0a4544a608 | ||
|
|
8e80c0a69c | ||
|
|
70e53c1246 | ||
|
|
b2ae30bab4 | ||
|
|
2538e41f00 | ||
|
|
3eb7cddb6c | ||
|
|
2813ae0eff | ||
|
|
1b1ffc4435 | ||
|
|
cf9fabb052 | ||
|
|
43fe73bf09 | ||
|
|
d291e36b3f | ||
|
|
fe37c15f95 | ||
|
|
d4f62da6f5 | ||
|
|
1edefd9cad | ||
|
|
90ffa7df27 | ||
|
|
d69e941ec1 | ||
|
|
5e5e8876f6 | ||
|
|
886d595f91 | ||
|
|
e04237b928 | ||
|
|
ef84c519a2 | ||
|
|
1e647c7036 | ||
|
|
56f6b1668d |
@@ -4,6 +4,7 @@ packages:
|
||||
- libpulse
|
||||
- alsa-lib
|
||||
- fontconfig
|
||||
- libxkbcommon
|
||||
sources:
|
||||
- https://git.sr.ht/~kennylevinsen/wldash
|
||||
tasks:
|
||||
|
||||
@@ -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,4 +1,3 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
liberapay: kennylevinsen
|
||||
custom: https://donate.kl.wtf/
|
||||
|
||||
@@ -1,2 +1 @@
|
||||
/target
|
||||
**/*.rs.bk
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
# Changelog
|
||||
|
||||
## 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
+634
-318
File diff suppressed because it is too large
Load Diff
+26
-33
@@ -1,48 +1,41 @@
|
||||
[package]
|
||||
name = "wldash"
|
||||
version = "0.2.0"
|
||||
authors = ["Kenny Levinsen <kl@kl.wtf>"]
|
||||
edition = "2018"
|
||||
version = "0.4.2"
|
||||
edition = "2024"
|
||||
|
||||
[profile.release]
|
||||
#debug = true
|
||||
lto = "fat"
|
||||
panic = "abort"
|
||||
opt-level = "z"
|
||||
|
||||
[features]
|
||||
default = ["alsa-widget", "pulseaudio-widget", "yaml-cfg", "json-cfg"]
|
||||
damage_debug = []
|
||||
|
||||
yaml-cfg = ["serde_yaml"]
|
||||
json-cfg = ["serde_json"]
|
||||
|
||||
alsa-widget = ["alsa"]
|
||||
pulseaudio-widget = ["libpulse-binding"]
|
||||
default = ["fontconfig"]
|
||||
|
||||
[dependencies]
|
||||
byteorder = "1.3"
|
||||
bitflags = "1.0"
|
||||
dlib = "0.4"
|
||||
rusttype = "^0.9"
|
||||
wayland-client = { version = "0.30", features = ["calloop", "log"] }
|
||||
wayland-protocols = { version = "0.30", features = ["client", "unstable", "staging"] }
|
||||
wayland-protocols-wlr = { version = "0.1", features = ["client"] }
|
||||
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"
|
||||
nix = "0.16"
|
||||
memmap = "0.7"
|
||||
os_pipe = "0.9"
|
||||
wayland-client = { version = "0.28"}
|
||||
wayland-protocols = { version = "0.28", features = ["client", "unstable_protocols"] }
|
||||
dbus = "0.6"
|
||||
|
||||
walkdir = "2.4"
|
||||
|
||||
unicode-segmentation = "1.10"
|
||||
fuzzy-matcher = "0.3"
|
||||
lazy_static = "1.4"
|
||||
rcalc_lib = "0.9"
|
||||
rust-ini = "0.14"
|
||||
shlex = "0.1"
|
||||
timerfd = "1.0.0"
|
||||
|
||||
xkbcommon = { version = "0.8", features = ["wayland"] }
|
||||
|
||||
rcalc_lib = "1.0"
|
||||
|
||||
#alsa = { version = "0.7", optional = true }
|
||||
dbus = "0.9"
|
||||
libpulse-binding = "2.28"
|
||||
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0", optional = true }
|
||||
serde_yaml = { version = "0.8", optional = true }
|
||||
serde_yaml = "0.9"
|
||||
|
||||
libpulse-binding = { version = "2.22", optional = true }
|
||||
alsa = { version = "0.4.0", optional = true }
|
||||
fontconfig = "0.2.0"
|
||||
unicode-segmentation = "1.6.0"
|
||||
|
||||
@@ -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.2
|
||||
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"
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
## 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
|
||||
|
||||
1. Checkout: `git clone https://github.com/kennylevinsen/wldash`
|
||||
1. Checkout: `git clone https://project.uhhm.no/bl/wldash`
|
||||
2. Build: `cargo build --release`
|
||||
3. Put somewhere: `cp target/release/wldash /usr/local/bin/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`.
|
||||
|
||||
@@ -63,6 +78,7 @@ The launcher also accepts prefix operators to change its mode:
|
||||
## Notable missing features:
|
||||
|
||||
- Scaling of any kind - all sizes are currently hardcoded in pixels
|
||||
- Configurability
|
||||
- Cleanup and reorganization
|
||||
- Proper line editor for the launcher
|
||||
|
||||
## How to discuss
|
||||
|
||||
Go to #kennylevinsen @ irc.libera.chat to discuss, or use [~kennylevinsen/public-inbox@lists.sr.ht](https://lists.sr.ht/~kennylevinsen/public-inbox).
|
||||
|
||||
-55
@@ -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": "alacritty -e ",
|
||||
"url_opener": "xdg-open "
|
||||
}}
|
||||
]}
|
||||
}
|
||||
},
|
||||
"outputMode": "active",
|
||||
"scale": 1,
|
||||
"background": {
|
||||
"red": 0,
|
||||
"green": 0,
|
||||
"blue": 0,
|
||||
"opacity": 0.9
|
||||
}
|
||||
}
|
||||
-50
@@ -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: "alacritty -e "
|
||||
url_opener: "xdg-open "
|
||||
-600
@@ -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_shm, wl_surface, wl_seat};
|
||||
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::{WaitContext, DrawContext, 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_and_swap(false, true, Ordering::SeqCst) {
|
||||
*(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(version, 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
+232
-127
@@ -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::widgets::Geometry;
|
||||
|
||||
pub struct Buffer<'a> {
|
||||
buf: &'a mut MmapMut,
|
||||
fn create_shm_fd() -> io::Result<RawFd> {
|
||||
// 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),
|
||||
subdimensions: Option<(u32, u32, u32, u32)>,
|
||||
}
|
||||
|
||||
impl<'a> Buffer<'a> {
|
||||
pub fn new(buf: &'a mut MmapMut, dimensions: (u32, u32)) -> Buffer {
|
||||
Buffer {
|
||||
buf,
|
||||
impl<'a> BufferView<'a> {
|
||||
pub fn new(buf: &'a mut MmapMut, dimensions: (u32, u32)) -> BufferView<'a> {
|
||||
let view = unsafe {
|
||||
std::slice::from_raw_parts_mut(
|
||||
buf.as_mut_ptr() as *mut u32,
|
||||
(dimensions.0 * dimensions.1) as usize,
|
||||
)
|
||||
};
|
||||
BufferView {
|
||||
view,
|
||||
dimensions,
|
||||
subdimensions: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn get_bounds(&self) -> (u32, u32, u32, u32) {
|
||||
if let Some(subdim) = self.subdimensions {
|
||||
subdim
|
||||
@@ -25,68 +198,20 @@ impl<'a> Buffer<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_signed_bounds(&self) -> (i32, i32, i32, i32) {
|
||||
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> {
|
||||
pub fn subdimensions(&mut self, subdimensions: (u32, u32, u32, u32)) -> BufferView<'_> {
|
||||
let bounds = self.get_bounds();
|
||||
if subdimensions.0 + subdimensions.2 >= bounds.2
|
||||
|| subdimensions.1 + subdimensions.3 >= bounds.3
|
||||
if cfg!(debug_assertions)
|
||||
&& (subdimensions.0 + subdimensions.2 > bounds.2
|
||||
|| subdimensions.1 + subdimensions.3 > bounds.3)
|
||||
{
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!(
|
||||
"cannot create subdimensions larger than buffer: {:?} > {:?}",
|
||||
subdimensions, bounds
|
||||
),
|
||||
));
|
||||
panic!(
|
||||
"cannot create subdimensions larger than buffer: {:?} > {:?}",
|
||||
subdimensions, bounds
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Buffer {
|
||||
buf: self.buf,
|
||||
BufferView {
|
||||
view: self.view,
|
||||
dimensions: self.dimensions,
|
||||
subdimensions: Some((
|
||||
subdimensions.0 + bounds.0,
|
||||
@@ -94,23 +219,24 @@ impl<'a> Buffer<'a> {
|
||||
subdimensions.2,
|
||||
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();
|
||||
if offset.0 > bounds.2 || offset.1 > bounds.3 {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!(
|
||||
"cannot create offset outside buffer: {:?} > {:?}",
|
||||
offset, bounds
|
||||
),
|
||||
));
|
||||
if cfg!(debug_assertions) && (offset.0 > bounds.2 || offset.1 > bounds.3) {
|
||||
panic!(
|
||||
"cannot create offset outside buffer: {:?} > {:?}",
|
||||
offset, bounds
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Buffer {
|
||||
buf: self.buf,
|
||||
BufferView {
|
||||
view: self.view,
|
||||
dimensions: self.dimensions,
|
||||
subdimensions: Some((
|
||||
offset.0 + bounds.0,
|
||||
@@ -118,63 +244,42 @@ impl<'a> Buffer<'a> {
|
||||
bounds.2 - offset.0,
|
||||
bounds.3 - offset.1,
|
||||
)),
|
||||
})
|
||||
}
|
||||
|
||||
pub fn memset(&mut self, c: &Color) {
|
||||
if let Some(subdim) = self.subdimensions {
|
||||
unsafe {
|
||||
let ptr = self.buf.as_mut_ptr();
|
||||
for y in subdim.1..(subdim.1 + subdim.3) {
|
||||
for x in subdim.0..(subdim.0 + subdim.2) {
|
||||
*((ptr as *mut u32).offset((x + y * self.dimensions.0) as isize)) =
|
||||
c.as_argb8888();
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
unsafe {
|
||||
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> {
|
||||
let true_pos = if let Some(subdim) = self.subdimensions {
|
||||
if pos.0 >= subdim.2 || pos.1 >= subdim.3 {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!(
|
||||
"put({:?}) is not within subdimensions of buffer ({:?})",
|
||||
pos, subdim
|
||||
),
|
||||
));
|
||||
pub fn memset(&mut self, c: Color) {
|
||||
if let Some(subdim) = self.subdimensions {
|
||||
for y in subdim.1..(subdim.1 + subdim.3) {
|
||||
let start = (subdim.0 + y * self.dimensions.0) as usize;
|
||||
let end = start + subdim.2 as usize;
|
||||
self.view[start..end].fill(c.0);
|
||||
}
|
||||
(pos.0 + subdim.0, pos.1 + subdim.1)
|
||||
} else {
|
||||
if pos.0 >= self.dimensions.0 || pos.1 >= self.dimensions.1 {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!(
|
||||
"put({:?}) is not within dimensions of buffer ({:?})",
|
||||
pos, self.dimensions
|
||||
),
|
||||
));
|
||||
self.view.fill(c.0);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear(&mut self) {
|
||||
if let Some(subdim) = self.subdimensions {
|
||||
for y in subdim.1..(subdim.1 + subdim.3) {
|
||||
let start = (subdim.0 + y * self.dimensions.0) as usize;
|
||||
let end = start + subdim.2 as usize;
|
||||
self.view[start..end].fill(0x0);
|
||||
}
|
||||
pos
|
||||
};
|
||||
} else {
|
||||
self.view.fill(0x0);
|
||||
}
|
||||
}
|
||||
|
||||
unsafe {
|
||||
let ptr = self
|
||||
.buf
|
||||
.as_mut_ptr()
|
||||
.offset(4 * (true_pos.0 + (true_pos.1 * self.dimensions.0)) as isize);
|
||||
*(ptr as *mut u32) = c.as_argb8888();
|
||||
};
|
||||
#[inline]
|
||||
pub fn put_raw(&mut self, pos: (u32, u32), c: Color) {
|
||||
self.view[(pos.0 + pos.1 * self.dimensions.0) as usize] = c.0
|
||||
}
|
||||
|
||||
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
@@ -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>,
|
||||
},
|
||||
}
|
||||
+37
-66
@@ -1,71 +1,42 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, Default)]
|
||||
pub struct Color {
|
||||
red: f32,
|
||||
green: f32,
|
||||
blue: f32,
|
||||
opacity: f32,
|
||||
}
|
||||
#[derive(Copy, Clone)]
|
||||
pub struct Color(pub u32);
|
||||
|
||||
impl Color {
|
||||
pub fn new(red: f32, green: f32, blue: f32, opacity: f32) -> Color {
|
||||
let (red, green, blue, opacity) = clamp(red, green, blue, opacity);
|
||||
Color {
|
||||
red,
|
||||
green,
|
||||
blue,
|
||||
opacity,
|
||||
pub const WHITE: Color = Color(0xFFFFFFFF);
|
||||
pub const GREY35: Color = Color(0xFF595959);
|
||||
pub const GREY50: Color = Color(0xFF7F7F7F);
|
||||
pub const GREY75: Color = Color(0xFFBFBFBF);
|
||||
pub const GREY80: Color = Color(0xFFCCCCCC);
|
||||
pub const RED: Color = Color(0xFFFF0000);
|
||||
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))
|
||||
}
|
||||
|
||||
fn red(self) -> u8 {
|
||||
((self.0 >> 16) & 0xFF) as u8
|
||||
}
|
||||
fn green(self) -> u8 {
|
||||
((self.0 >> 8) & 0xFF) as u8
|
||||
}
|
||||
fn blue(self) -> u8 {
|
||||
(self.0 & 0xFF) as u8
|
||||
}
|
||||
|
||||
pub fn alpha(self, alpha: f32) -> Color {
|
||||
unsafe {
|
||||
Color(
|
||||
(255. * alpha).to_int_unchecked::<u32>() << 24
|
||||
| (self.red() as f32 * alpha).to_int_unchecked::<u32>() << 16
|
||||
| (self.green() as f32 * alpha).to_int_unchecked::<u32>() << 8
|
||||
| (self.blue() as f32 * alpha).to_int_unchecked::<u32>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn blend(&self, other: &Color, ratio: f32) -> Color {
|
||||
let ratio = clamp_f32(ratio, 0.0, 1.0);
|
||||
|
||||
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),
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_argb8888(&self) -> u32 {
|
||||
((255.0 * self.opacity) as u32 & 0xFF) << 24
|
||||
| ((255.0 * self.red) as u32 & 0xFF) << 16
|
||||
| ((255.0 * self.green) as u32 & 0xFF) << 8
|
||||
| ((255.0 * self.blue) as u32 & 0xFF)
|
||||
}
|
||||
}
|
||||
|
||||
#[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)
|
||||
}
|
||||
|
||||
+315
-284
@@ -1,29 +1,39 @@
|
||||
use crate::cmd::Cmd;
|
||||
use crate::color::Color;
|
||||
use crate::widget;
|
||||
use crate::{
|
||||
fonts::{FontMap, FontRef},
|
||||
widgets,
|
||||
};
|
||||
use chrono::NaiveDateTime;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde::{self, Deserialize, Serialize};
|
||||
|
||||
use std::collections::HashMap;
|
||||
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)]
|
||||
#[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 {
|
||||
Margin {
|
||||
margins: (u32, u32, u32, u32),
|
||||
widget: Box<Widget>,
|
||||
},
|
||||
Fixed {
|
||||
width: u32,
|
||||
height: u32,
|
||||
widget: Box<Widget>,
|
||||
},
|
||||
HorizontalLayout(Vec<Widget>),
|
||||
InvertedHorizontalLayout(Vec<Widget>),
|
||||
VerticalLayout(Vec<Widget>),
|
||||
InvertedVerticalLayout(Vec<Widget>),
|
||||
HorizontalLine(u32),
|
||||
VerticalLine(u32),
|
||||
Clock {
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
@@ -36,315 +46,336 @@ pub enum Widget {
|
||||
font_primary: Option<String>,
|
||||
font_secondary: Option<String>,
|
||||
font_size: f32,
|
||||
sections: u32,
|
||||
sections_x: i32,
|
||||
sections_y: i32,
|
||||
},
|
||||
Launcher {
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
#[serde(default)]
|
||||
app_opener: String,
|
||||
#[serde(default)]
|
||||
term_opener: String,
|
||||
#[serde(default)]
|
||||
url_opener: String,
|
||||
launch_cmd: Option<String>,
|
||||
},
|
||||
Battery {
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
},
|
||||
Backlight {
|
||||
#[serde(default)]
|
||||
device: String,
|
||||
device: Option<String>,
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
},
|
||||
#[cfg(feature = "pulseaudio-widget")]
|
||||
PulseAudio {
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
},
|
||||
#[cfg(feature = "alsa-widget")]
|
||||
AlsaSound {
|
||||
font: Option<String>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
},
|
||||
}
|
||||
|
||||
impl Default for OperationMode {
|
||||
fn default() -> OperationMode {
|
||||
OperationMode::LayerSurface((1024, 768))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub struct Config {
|
||||
pub font_paths: Option<HashMap<String, String>>,
|
||||
pub widget: Widget,
|
||||
pub mode: OperationMode,
|
||||
pub background: Option<u32>,
|
||||
pub server: Option<bool>,
|
||||
}
|
||||
|
||||
impl Config {
|
||||
pub fn generate_v1() -> Config {
|
||||
let has_battery = Battery::detect();
|
||||
let has_backlight = Backlight::detect();
|
||||
|
||||
let mut bars = Vec::new();
|
||||
if has_battery {
|
||||
bars.push(Widget::Margin {
|
||||
margins: (0, 0, 0, 8),
|
||||
widget: Box::new(Widget::Battery {
|
||||
font: None,
|
||||
font_size: 24.,
|
||||
}),
|
||||
});
|
||||
}
|
||||
if has_backlight {
|
||||
bars.push(Widget::Margin {
|
||||
margins: (0, 0, 0, 8),
|
||||
widget: Box::new(Widget::Backlight {
|
||||
device: None,
|
||||
font: None,
|
||||
font_size: 24.,
|
||||
}),
|
||||
});
|
||||
}
|
||||
bars.push(Widget::Margin {
|
||||
margins: (0, 0, 0, 8),
|
||||
widget: Box::new(Widget::PulseAudio {
|
||||
font: None,
|
||||
font_size: 24.,
|
||||
}),
|
||||
});
|
||||
|
||||
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 {
|
||||
font_primary: None,
|
||||
font_secondary: None,
|
||||
font_size: 36.,
|
||||
sections_x: 3,
|
||||
sections_y: 1,
|
||||
},
|
||||
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: 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.,
|
||||
}),
|
||||
},
|
||||
Widget::Margin {
|
||||
margins: (16, 64, 0, 0),
|
||||
widget: Box::new(Widget::Date {
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn leak_or_default(v: Option<String>, d: &'static str) -> &'static str {
|
||||
match v {
|
||||
Some(v) => Box::leak(v.into_boxed_str()),
|
||||
None => d,
|
||||
}
|
||||
}
|
||||
|
||||
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<'a>(
|
||||
pub fn construct_widgets(
|
||||
self,
|
||||
time: NaiveDateTime,
|
||||
tx: Sender<Cmd>,
|
||||
fonts: &'a FontMap,
|
||||
) -> Option<Box<dyn widget::Widget + Send + 'a>> {
|
||||
v: &mut Vec<Box<dyn RealWidget>>,
|
||||
fm: &mut FontMap,
|
||||
events: &Arc<Mutex<Events>>,
|
||||
) {
|
||||
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),
|
||||
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,
|
||||
) {
|
||||
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),
|
||||
))),
|
||||
Widget::Date { font, font_size } => v.push(Box::new(Date::new(
|
||||
fm,
|
||||
leak_or_default(font, "sans"),
|
||||
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,
|
||||
),
|
||||
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,
|
||||
)),
|
||||
sections_x,
|
||||
sections_y,
|
||||
))),
|
||||
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),
|
||||
launch_cmd,
|
||||
} => v.push(Box::new(Interface::new(
|
||||
events.clone(),
|
||||
fm,
|
||||
leak_or_default(font, "sans"),
|
||||
font_size,
|
||||
length,
|
||||
tx,
|
||||
app_opener,
|
||||
term_opener,
|
||||
if url_opener.is_empty() {
|
||||
"xdg_open ".to_string()
|
||||
} else {
|
||||
url_opener
|
||||
},
|
||||
)),
|
||||
Widget::Battery {
|
||||
font,
|
||||
launch_cmd,
|
||||
))),
|
||||
Widget::Battery { font, font_size } => v.push(Box::new(Battery::new(
|
||||
events.clone(),
|
||||
fm,
|
||||
leak_or_default(font, "sans"),
|
||||
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,
|
||||
} => v.push(Box::new(Backlight::new(
|
||||
leak_or_none(device),
|
||||
fm,
|
||||
leak_or_default(font, "sans"),
|
||||
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,
|
||||
))),
|
||||
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,
|
||||
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,
|
||||
}
|
||||
))),
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
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 output_mode: OutputMode,
|
||||
pub scale: u32,
|
||||
pub background: Color,
|
||||
pub widget: Widget,
|
||||
|
||||
#[serde(default = "default_fonts")]
|
||||
pub fonts: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl Default for Config {
|
||||
fn default() -> Self {
|
||||
Config {
|
||||
widget: Widget::Margin {
|
||||
margins: (20, 20, 20, 20),
|
||||
widget: Box::new(Widget::VerticalLayout(vec![
|
||||
Widget::HorizontalLayout(vec![
|
||||
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),
|
||||
widget: Box::new(Widget::Battery {
|
||||
font: None,
|
||||
font_size: 24.0,
|
||||
length: 0,
|
||||
}),
|
||||
},
|
||||
Widget::Margin {
|
||||
margins: (0, 0, 0, 8),
|
||||
widget: Box::new(Widget::Backlight {
|
||||
device: "intel_backlight".to_string(),
|
||||
font: None,
|
||||
font_size: 24.0,
|
||||
length: 0,
|
||||
}),
|
||||
},
|
||||
#[cfg(feature = "pulseaudio-widget")]
|
||||
Widget::Margin {
|
||||
margins: (0, 0, 0, 8),
|
||||
widget: Box::new(Widget::PulseAudio {
|
||||
font: None,
|
||||
font_size: 24.0,
|
||||
length: 0,
|
||||
}),
|
||||
},
|
||||
]),
|
||||
]),
|
||||
Widget::Calendar {
|
||||
font_primary: None,
|
||||
font_secondary: None,
|
||||
font_size: 16.0,
|
||||
sections: 3,
|
||||
},
|
||||
Widget::Launcher {
|
||||
font: None,
|
||||
font_size: 32.0,
|
||||
length: 0,
|
||||
app_opener: "".to_string(),
|
||||
term_opener: "".to_string(),
|
||||
url_opener: "".to_string(),
|
||||
},
|
||||
])),
|
||||
},
|
||||
output_mode: Default::default(),
|
||||
scale: 1,
|
||||
background: Color::new(0.0, 0.0, 0.0, 0.9),
|
||||
fonts: default_fonts(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn get_font<'a>(name: &str, map: &'a FontMap) -> FontRef<'a> {
|
||||
match map.get(name) {
|
||||
Some(f) => f,
|
||||
None => panic!(format!("Font {} is missing from the config", name)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
extern crate ini;
|
||||
use ini::{Ini, ParseOption};
|
||||
use std::cmp::Ordering;
|
||||
use std::env;
|
||||
use std::error::Error;
|
||||
use std::fs;
|
||||
use std::io::Error as io_error;
|
||||
use std::io::ErrorKind;
|
||||
|
||||
#[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 fs::read_dir(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")
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -1,266 +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;
|
||||
}
|
||||
}
|
||||
+95
-86
@@ -1,27 +1,26 @@
|
||||
use crate::buffer::Buffer;
|
||||
use crate::buffer::BufferView;
|
||||
use crate::{color::Color, fonts::FontRef};
|
||||
|
||||
use rusttype::{point, Scale};
|
||||
use rusttype::{Scale, point};
|
||||
use std::collections::HashMap;
|
||||
|
||||
struct CachedGlyph {
|
||||
dimensions: (u32, u32),
|
||||
origin: (i32, i32),
|
||||
render: Vec<f32>,
|
||||
advance: i32,
|
||||
}
|
||||
|
||||
impl CachedGlyph {
|
||||
fn new(font: FontRef, size: f32, ch: char) -> CachedGlyph {
|
||||
let scale = Scale::uniform(size);
|
||||
let v_metrics = font.v_metrics(scale);
|
||||
let glyph = font
|
||||
.glyph(ch)
|
||||
.scaled(scale)
|
||||
.positioned(point(0.0, v_metrics.ascent));
|
||||
let glyph = font.glyph(ch).scaled(scale);
|
||||
let advance = glyph.h_metrics().advance_width as i32;
|
||||
let glyph = glyph.positioned(point(0.0, v_metrics.ascent));
|
||||
|
||||
if let Some(bounding_box) = glyph.pixel_bounding_box() {
|
||||
let origin = (bounding_box.min.x, bounding_box.min.y);
|
||||
|
||||
let dimensions = (
|
||||
(bounding_box.max.x - bounding_box.min.x) as u32,
|
||||
(bounding_box.max.y - bounding_box.min.y) as u32,
|
||||
@@ -35,29 +34,38 @@ impl CachedGlyph {
|
||||
origin,
|
||||
dimensions,
|
||||
render,
|
||||
advance,
|
||||
}
|
||||
} else {
|
||||
CachedGlyph {
|
||||
origin: (0, 0),
|
||||
dimensions: ((size / 4.0) as u32, 0),
|
||||
dimensions: (0, 0),
|
||||
render: Vec::new(),
|
||||
advance: (size / 4.0) as i32,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 y = 0;
|
||||
for v in &self.render {
|
||||
let _ = buf.put(
|
||||
buf.put_raw(
|
||||
(
|
||||
(x + pos.0 + self.origin.0) as u32,
|
||||
(y + pos.1 + self.origin.1) as u32,
|
||||
(x + pos.0 + self.origin.0 + offset.0) 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;
|
||||
x = 0;
|
||||
} else {
|
||||
@@ -74,7 +82,7 @@ pub struct 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 {
|
||||
glyphs: HashMap::new(),
|
||||
font,
|
||||
@@ -82,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) {
|
||||
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);
|
||||
self.glyphs.insert(ch, glyph);
|
||||
}
|
||||
@@ -93,9 +107,8 @@ impl<'a> Font<'a> {
|
||||
|
||||
pub fn draw_text(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
s: &str,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
let mut x_off = 0;
|
||||
@@ -105,10 +118,10 @@ impl<'a> Font<'a> {
|
||||
let glyph = match self.glyphs.get(&ch) {
|
||||
Some(glyph) => glyph,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!("glyph for {:} not in cache", ch),
|
||||
))
|
||||
return Err(::std::io::Error::other(format!(
|
||||
"glyph for {:} not in cache",
|
||||
ch
|
||||
)));
|
||||
}
|
||||
};
|
||||
glyphs.push(glyph);
|
||||
@@ -117,8 +130,8 @@ impl<'a> Font<'a> {
|
||||
}
|
||||
}
|
||||
for glyph in glyphs {
|
||||
glyph.draw(buf, (x_off, -off), bg, c);
|
||||
x_off += glyph.dimensions.0 as i32 + glyph.origin.0;
|
||||
glyph.draw(buf, (x_off, -off), c);
|
||||
x_off += glyph.advance;
|
||||
}
|
||||
|
||||
Ok((x_off as u32, self.size as u32))
|
||||
@@ -126,9 +139,8 @@ impl<'a> Font<'a> {
|
||||
|
||||
pub fn draw_text_with_cursor(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
s: &str,
|
||||
cursor: usize,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
@@ -139,10 +151,10 @@ impl<'a> Font<'a> {
|
||||
let glyph = match self.glyphs.get(&ch) {
|
||||
Some(glyph) => glyph,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!("glyph for {:} not in cache", ch),
|
||||
))
|
||||
return Err(::std::io::Error::other(format!(
|
||||
"glyph for {:} not in cache",
|
||||
ch
|
||||
)));
|
||||
}
|
||||
};
|
||||
glyphs.push(glyph);
|
||||
@@ -151,13 +163,13 @@ impl<'a> Font<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
let height = buf.get_bounds().3;
|
||||
let height = self.size.ceil() as u32;
|
||||
for (i, glyph) in glyphs.iter().enumerate() {
|
||||
if i == cursor {
|
||||
self.draw_cursor(buf, c, x_off as u32, height)?;
|
||||
}
|
||||
glyph.draw(buf, (x_off, -off), bg, c);
|
||||
x_off += glyph.dimensions.0 as i32 + glyph.origin.0;
|
||||
glyph.draw(buf, (x_off, -off), c);
|
||||
x_off += glyph.advance;
|
||||
}
|
||||
if cursor == glyphs.len() {
|
||||
self.draw_cursor(buf, c, x_off as u32, height)?;
|
||||
@@ -166,16 +178,20 @@ impl<'a> Font<'a> {
|
||||
Ok((x_off as u32, self.size as u32))
|
||||
}
|
||||
|
||||
pub fn draw_cursor(
|
||||
fn draw_cursor(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
offset: u32,
|
||||
height: u32,
|
||||
) -> Result<(), ::std::io::Error> {
|
||||
// draw cursor
|
||||
for i in 1..height {
|
||||
buf.put((offset, i), c)?
|
||||
let bounds = buf.get_bounds();
|
||||
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(())
|
||||
}
|
||||
@@ -187,13 +203,13 @@ impl<'a> Font<'a> {
|
||||
let glyph = match self.glyphs.get(&ch) {
|
||||
Some(glyph) => glyph,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!("glyph for {:} not in cache", ch),
|
||||
))
|
||||
return Err(::std::io::Error::other(format!(
|
||||
"glyph for {:} not in cache",
|
||||
ch
|
||||
)));
|
||||
}
|
||||
};
|
||||
let width = glyph.dimensions.0 as i32 + glyph.origin.0;
|
||||
let width = glyph.advance;
|
||||
if width > max {
|
||||
max = width
|
||||
}
|
||||
@@ -203,32 +219,29 @@ impl<'a> Font<'a> {
|
||||
|
||||
pub fn auto_draw_text(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
s: &str,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
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(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
s: &str,
|
||||
cursor: usize,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
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(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
c: &Color,
|
||||
buf: &mut BufferView,
|
||||
c: Color,
|
||||
distances: &[u32],
|
||||
s: &str,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
@@ -239,10 +252,10 @@ impl<'a> Font<'a> {
|
||||
let glyph = match self.glyphs.get(&ch) {
|
||||
Some(glyph) => glyph,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!("glyph for {:} not in cache", ch),
|
||||
))
|
||||
return Err(::std::io::Error::other(format!(
|
||||
"glyph for {:} not in cache",
|
||||
ch
|
||||
)));
|
||||
}
|
||||
};
|
||||
glyphs.push(glyph);
|
||||
@@ -251,7 +264,7 @@ impl<'a> Font<'a> {
|
||||
}
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -260,8 +273,7 @@ impl<'a> Font<'a> {
|
||||
|
||||
pub fn draw_text_individual_colors(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
buf: &mut BufferView,
|
||||
color: &[Color],
|
||||
s: &str,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
@@ -272,10 +284,10 @@ impl<'a> Font<'a> {
|
||||
let glyph = match self.glyphs.get(&ch) {
|
||||
Some(glyph) => glyph,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
format!("glyph for {:} not in cache", ch),
|
||||
))
|
||||
return Err(::std::io::Error::other(format!(
|
||||
"glyph for {:} not in cache",
|
||||
ch
|
||||
)));
|
||||
}
|
||||
};
|
||||
glyphs.push(glyph);
|
||||
@@ -284,8 +296,8 @@ impl<'a> Font<'a> {
|
||||
}
|
||||
}
|
||||
for (idx, glyph) in glyphs.into_iter().enumerate() {
|
||||
glyph.draw(buf, (x_off, -off), bg, &color[idx]);
|
||||
x_off += glyph.dimensions.0 as i32 + glyph.origin.0;
|
||||
glyph.draw(buf, (x_off, -off), color[idx]);
|
||||
x_off += glyph.advance;
|
||||
}
|
||||
|
||||
Ok((x_off as u32, self.size as u32))
|
||||
@@ -293,32 +305,30 @@ impl<'a> Font<'a> {
|
||||
|
||||
pub fn auto_draw_text_individual_colors(
|
||||
&mut self,
|
||||
buf: &mut Buffer,
|
||||
bg: &Color,
|
||||
buf: &mut BufferView,
|
||||
color: &[Color],
|
||||
s: &str,
|
||||
) -> Result<(u32, u32), ::std::io::Error> {
|
||||
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> {
|
||||
for x in 0..dim.0 {
|
||||
let _ = buf.put((x, 0), c);
|
||||
let _ = buf.put((x, dim.1 - 1), c);
|
||||
}
|
||||
for y in 0..dim.1 {
|
||||
buf.put((0, y), c)?;
|
||||
buf.put((dim.0 - 1, y), c)?;
|
||||
pub fn draw_box(buf: &mut BufferView, c: Color, dim: (u32, u32)) -> Result<(), ::std::io::Error> {
|
||||
let bounds = buf.get_bounds();
|
||||
buf.put_line_raw((bounds.0, bounds.1), dim.0, c);
|
||||
buf.put_line_raw((bounds.0, bounds.1 + dim.1 - 1), dim.0, c);
|
||||
for y in bounds.1..dim.1 + bounds.1 {
|
||||
buf.put_raw((bounds.0, y), c);
|
||||
buf.put_raw((bounds.0 + dim.0 - 1, y), c);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn draw_bar(
|
||||
buf: &mut Buffer,
|
||||
color: &Color,
|
||||
buf: &mut BufferView,
|
||||
color: Color,
|
||||
length: u32,
|
||||
height: u32,
|
||||
fill: f32,
|
||||
@@ -327,10 +337,9 @@ pub fn draw_bar(
|
||||
if fill_pos > length {
|
||||
fill_pos = length;
|
||||
}
|
||||
for y in 0..height {
|
||||
for x in 0..fill_pos {
|
||||
let _ = buf.put((x, y), color);
|
||||
}
|
||||
let bounds = buf.get_bounds();
|
||||
for y in bounds.1..height + bounds.1 {
|
||||
buf.put_line_raw((bounds.0, y), fill_pos, color);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -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
@@ -1,30 +1,56 @@
|
||||
//! Utility module for fonts
|
||||
|
||||
use fontconfig::Fontconfig as FontConfig;
|
||||
use crate::draw;
|
||||
use rusttype::Font;
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
fs::File,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
cell::RefCell, collections::HashMap, fs::File, hash, io::Read, mem, path::Path, rc::Rc, thread,
|
||||
};
|
||||
|
||||
/// 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.
|
||||
pub type FontRef<'a> = &'a rusttype::Font<'a>;
|
||||
|
||||
/// FontSeeker is a marker struct that is used to look up fonts
|
||||
pub(crate) struct FontSeeker;
|
||||
/// Ask fontconfig for the file backing a font name ("sans", "monospace",
|
||||
/// "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 {
|
||||
/// Acts like fc-match.
|
||||
/// Given a string, it matches it to a font file and returns its path.
|
||||
pub(crate) fn from_string(name: &str) -> PathBuf {
|
||||
let fc = FontConfig::new().unwrap();
|
||||
fc.find(name, None).unwrap().path
|
||||
#[cfg(not(feature = "fontconfig"))]
|
||||
pub(crate) fn find_font(name: &str) -> Result<String, String> {
|
||||
Err(format!(
|
||||
"no path configured for font {:?} and wldash was built without fontconfig",
|
||||
name
|
||||
))
|
||||
}
|
||||
|
||||
/// 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
|
||||
@@ -32,16 +58,168 @@ pub(crate) struct FontLoader;
|
||||
|
||||
impl FontLoader {
|
||||
/// 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
|
||||
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() {
|
||||
Ok(metadata) => Vec::with_capacity(metadata.len() as usize),
|
||||
Err(_) => vec![],
|
||||
};
|
||||
file.read_to_end(&mut data).unwrap();
|
||||
Font::try_from_vec(data)
|
||||
file.read_to_end(&mut 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"));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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.")
|
||||
};
|
||||
);
|
||||
|
||||
@@ -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 */
|
||||
+114
-292
@@ -12,327 +12,149 @@
|
||||
//! calloop event loop. Not doing so will prevent key repetition to work
|
||||
//! (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;
|
||||
use wayland_client::{
|
||||
protocol::{wl_keyboard, wl_seat, wl_surface},
|
||||
Attached,
|
||||
};
|
||||
pub use xkbcommon::xkb::keysyms;
|
||||
|
||||
mod ffi;
|
||||
mod repeat;
|
||||
mod state;
|
||||
pub mod keysyms;
|
||||
|
||||
pub use self::repeat::{KeyRepeatSource, RepeatMessage};
|
||||
use self::state::KbState;
|
||||
pub use self::state::{ModifiersState, RMLVO};
|
||||
pub use self::state::ModifiersState;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// An error that occurred while trying to initialize a mapped keyboard
|
||||
pub enum Error {
|
||||
/// libxkbcommon is not available
|
||||
XKBNotFound,
|
||||
/// Provided RMLVO specified a keymap that would not be loaded
|
||||
BadNames,
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyEvent {
|
||||
#[allow(dead_code)]
|
||||
pub rawkey: u32,
|
||||
pub keysym: u32,
|
||||
pub state: WEnum<wl_keyboard::KeyState>,
|
||||
pub modifiers: ModifiersState,
|
||||
pub utf8: Option<String>,
|
||||
pub repeats: bool,
|
||||
}
|
||||
|
||||
/// Events received from a mapped keyboard
|
||||
pub enum Event<'a> {
|
||||
/// 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,
|
||||
},
|
||||
pub struct KeymapDescription {
|
||||
fd: OwnedFd,
|
||||
size: u32,
|
||||
}
|
||||
|
||||
/// 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())
|
||||
impl KeymapDescription {
|
||||
fn new(fd: OwnedFd, size: u32) -> KeymapDescription {
|
||||
KeymapDescription { fd, size }
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
* Classic handling
|
||||
*/
|
||||
|
||||
type KbdCallback = dyn FnMut(Event<'_>, wl_keyboard::WlKeyboard, wayland_client::DispatchData<'_>);
|
||||
|
||||
struct KbdHandler {
|
||||
state: Rc<RefCell<KbState>>,
|
||||
callback: Rc<RefCell<KbdCallback>>,
|
||||
impl Clone for KeymapDescription {
|
||||
fn clone(&self) -> Self {
|
||||
KeymapDescription::new(self.fd.try_clone().expect("unable to clone fd"), self.size)
|
||||
}
|
||||
}
|
||||
|
||||
impl KbdHandler {
|
||||
fn event(
|
||||
&mut self,
|
||||
kbd: wl_keyboard::WlKeyboard,
|
||||
event: wl_keyboard::Event,
|
||||
dispatch_data: wayland_client::DispatchData,
|
||||
) {
|
||||
use wl_keyboard::Event;
|
||||
pub struct Keyboard {
|
||||
state: Option<KbState>,
|
||||
desc: Option<KeymapDescription>,
|
||||
}
|
||||
|
||||
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),
|
||||
_ => {}
|
||||
impl Keyboard {
|
||||
pub fn new() -> Keyboard {
|
||||
Keyboard {
|
||||
state: None,
|
||||
desc: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn keymap(
|
||||
&mut self,
|
||||
_: wl_keyboard::WlKeyboard,
|
||||
format: wl_keyboard::KeymapFormat,
|
||||
fd: RawFd,
|
||||
size: u32,
|
||||
) {
|
||||
let mut state = self.state.borrow_mut();
|
||||
if state.locked() {
|
||||
// state is locked, ignore keymap updates
|
||||
return;
|
||||
}
|
||||
if state.ready() {
|
||||
// new keymap, we first deinit to free resources
|
||||
unsafe {
|
||||
state.de_init();
|
||||
}
|
||||
}
|
||||
pub fn keymap(&mut self, format: WEnum<wl_keyboard::KeymapFormat>, fd: OwnedFd, size: u32) {
|
||||
match format {
|
||||
wl_keyboard::KeymapFormat::XkbV1 => unsafe {
|
||||
state.init_with_fd(fd, size as usize);
|
||||
},
|
||||
wl_keyboard::KeymapFormat::NoKeymap => {
|
||||
WEnum::Value(wl_keyboard::KeymapFormat::XkbV1) => {
|
||||
self.desc = Some(KeymapDescription::new(fd, size));
|
||||
}
|
||||
WEnum::Value(wl_keyboard::KeymapFormat::NoKeymap) => {
|
||||
// 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(
|
||||
&mut self,
|
||||
object: wl_keyboard::WlKeyboard,
|
||||
serial: u32,
|
||||
surface: wl_surface::WlSurface,
|
||||
keys: Vec<u8>,
|
||||
dispatch_data: wayland_client::DispatchData,
|
||||
) {
|
||||
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,
|
||||
);
|
||||
}
|
||||
pub fn resolve(&mut self) {
|
||||
if self.state.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
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(
|
||||
&mut self,
|
||||
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 mut state = self.state.borrow_mut();
|
||||
// Get the values to generate a key event
|
||||
let sym = state.get_one_sym_raw(key);
|
||||
let utf8 = if key_state == wl_keyboard::KeyState::Pressed {
|
||||
match state.compose_feed(sym) {
|
||||
Some(ffi::xkb_compose_feed_result::XKB_COMPOSE_FEED_ACCEPTED) => {
|
||||
if let Some(status) = state.compose_status() {
|
||||
match status {
|
||||
ffi::xkb_compose_status::XKB_COMPOSE_COMPOSED => {
|
||||
state.compose_get_utf8()
|
||||
}
|
||||
ffi::xkb_compose_status::XKB_COMPOSE_NOTHING => {
|
||||
state.get_utf8_raw(key)
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
state.get_utf8_raw(key)
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// XKB_COMPOSE_FEED_IGNORED
|
||||
None
|
||||
}
|
||||
None => {
|
||||
// XKB COMPOSE is not initialized
|
||||
state.get_utf8_raw(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let repeats = unsafe { state.key_repeats(key + 8) };
|
||||
(sym, utf8, repeats)
|
||||
};
|
||||
|
||||
(&mut *self.callback.borrow_mut())(
|
||||
Event::Key {
|
||||
serial,
|
||||
time,
|
||||
rawkey: key,
|
||||
keysym: sym,
|
||||
state: key_state,
|
||||
utf8,
|
||||
repeats,
|
||||
},
|
||||
object,
|
||||
dispatch_data,
|
||||
);
|
||||
}
|
||||
|
||||
fn modifiers(
|
||||
&mut self,
|
||||
object: wl_keyboard::WlKeyboard,
|
||||
mods_depressed: u32,
|
||||
mods_latched: u32,
|
||||
mods_locked: u32,
|
||||
group: u32,
|
||||
dispatch_data: wayland_client::DispatchData,
|
||||
) {
|
||||
{
|
||||
let mut state = self.state.borrow_mut();
|
||||
state.update_modifiers(mods_depressed, mods_latched, mods_locked, group);
|
||||
(&mut *self.callback.borrow_mut())(
|
||||
Event::Modifiers {
|
||||
modifiers: state.mods_state(),
|
||||
},
|
||||
object,
|
||||
dispatch_data,
|
||||
if let Some(desc) = self.desc.take() {
|
||||
self.state = Some(
|
||||
KbState::new_from_fd(desc.fd, desc.size as usize).expect("unable to load keymap"),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
pub fn key(&mut self, key: u32, key_state: WEnum<wl_keyboard::KeyState>) -> KeyEvent {
|
||||
if let Some(state) = &mut self.state {
|
||||
let (sym, utf8, repeats) = {
|
||||
// Get the values to generate a key event
|
||||
let sym = state.get_one_sym_raw(key);
|
||||
let utf8 = if key_state == WEnum::Value(wl_keyboard::KeyState::Pressed) {
|
||||
match state.compose_feed(sym) {
|
||||
Some(xkb::compose::FeedResult::Accepted) => {
|
||||
if let Some(status) = state.compose_status() {
|
||||
match status {
|
||||
xkb::compose::Status::Composed => state.compose_get_utf8(),
|
||||
xkb::compose::Status::Nothing => state.get_utf8_raw(key),
|
||||
_ => None,
|
||||
}
|
||||
} else {
|
||||
state.get_utf8_raw(key)
|
||||
}
|
||||
}
|
||||
Some(_) => {
|
||||
// XKB_COMPOSE_FEED_IGNORED
|
||||
None
|
||||
}
|
||||
None => {
|
||||
// XKB COMPOSE is not initialized
|
||||
state.get_utf8_raw(key)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let repeats = state.key_repeats(xkb::Keycode::new(key + 8));
|
||||
(sym, utf8, repeats)
|
||||
};
|
||||
KeyEvent {
|
||||
rawkey: key,
|
||||
keysym: sym,
|
||||
state: key_state,
|
||||
modifiers: state.mods_state(),
|
||||
utf8,
|
||||
repeats,
|
||||
}
|
||||
} else {
|
||||
KeyEvent {
|
||||
rawkey: key,
|
||||
keysym: 0,
|
||||
state: key_state,
|
||||
modifiers: Default::default(),
|
||||
utf8: None,
|
||||
repeats: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn modifiers(
|
||||
&mut self,
|
||||
mods_depressed: u32,
|
||||
mods_latched: u32,
|
||||
mods_locked: u32,
|
||||
group: u32,
|
||||
) {
|
||||
if let Some(state) = &mut self.state {
|
||||
state.update_modifiers(mods_depressed, mods_latched, mods_locked, group);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
-314
@@ -1,48 +1,14 @@
|
||||
use std::{
|
||||
env,
|
||||
ffi::CString,
|
||||
fs::File,
|
||||
os::raw::c_char,
|
||||
os::unix::{
|
||||
ffi::OsStringExt,
|
||||
io::{FromRawFd, RawFd},
|
||||
},
|
||||
ptr,
|
||||
};
|
||||
use std::{env, os::unix::io::OwnedFd};
|
||||
|
||||
use memmap::MmapOptions;
|
||||
|
||||
use super::ffi::{self, xkb_state_component, XKBCOMMON_HANDLE as XKBH};
|
||||
use super::Error;
|
||||
use xkbcommon::xkb;
|
||||
|
||||
pub struct KbState {
|
||||
xkb_context: *mut ffi::xkb_context,
|
||||
xkb_keymap: *mut ffi::xkb_keymap,
|
||||
xkb_state: *mut ffi::xkb_state,
|
||||
xkb_compose_table: *mut ffi::xkb_compose_table,
|
||||
xkb_compose_state: *mut ffi::xkb_compose_state,
|
||||
xkb_context: xkb::Context,
|
||||
xkb_keymap: xkb::Keymap,
|
||||
xkb_state: xkb::State,
|
||||
xkb_compose_table: Option<xkb::compose::Table>,
|
||||
xkb_compose_state: Option<xkb::compose::State>,
|
||||
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
|
||||
@@ -74,52 +40,19 @@ impl ModifiersState {
|
||||
ModifiersState::default()
|
||||
}
|
||||
|
||||
fn update_with(&mut self, state: *mut ffi::xkb_state) {
|
||||
self.ctrl = unsafe {
|
||||
(XKBH.xkb_state_mod_name_is_active)(
|
||||
state,
|
||||
ffi::XKB_MOD_NAME_CTRL.as_ptr() as *const c_char,
|
||||
xkb_state_component::XKB_STATE_MODS_EFFECTIVE,
|
||||
) > 0
|
||||
};
|
||||
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
|
||||
};
|
||||
fn update_with(&mut self, state: &xkb::State) {
|
||||
self.ctrl = state.mod_name_is_active("Control", xkb::STATE_MODS_EFFECTIVE);
|
||||
self.alt = state.mod_name_is_active("Mod1", xkb::STATE_MODS_EFFECTIVE);
|
||||
self.shift = state.mod_name_is_active("Shift", xkb::STATE_MODS_EFFECTIVE);
|
||||
self.caps_lock = state.mod_name_is_active("Lock", xkb::STATE_MODS_EFFECTIVE);
|
||||
self.num_lock = state.mod_name_is_active("Mod2", xkb::STATE_MODS_EFFECTIVE);
|
||||
self.logo = state.mod_name_is_active("Mod4", xkb::STATE_MODS_EFFECTIVE);
|
||||
}
|
||||
}
|
||||
|
||||
// Safety: No.
|
||||
unsafe impl Send for KbState {}
|
||||
|
||||
impl KbState {
|
||||
pub(crate) fn update_modifiers(
|
||||
&mut self,
|
||||
@@ -128,257 +61,97 @@ impl KbState {
|
||||
mods_locked: u32,
|
||||
group: u32,
|
||||
) {
|
||||
if !self.ready() {
|
||||
return;
|
||||
}
|
||||
let mask = unsafe {
|
||||
(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) {
|
||||
let mask =
|
||||
self.xkb_state
|
||||
.update_mask(mods_depressed, mods_latched, mods_locked, 0, 0, group);
|
||||
if (mask & xkb::STATE_MODS_EFFECTIVE) > 0 {
|
||||
// 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 {
|
||||
if !self.ready() {
|
||||
return 0;
|
||||
}
|
||||
unsafe { (XKBH.xkb_state_key_get_one_sym)(self.xkb_state, keycode + 8) }
|
||||
self.xkb_state
|
||||
.key_get_one_sym(xkb::Keycode::new(keycode + 8))
|
||||
.raw()
|
||||
}
|
||||
|
||||
pub(crate) fn get_utf8_raw(&mut self, keycode: u32) -> Option<String> {
|
||||
if !self.ready() {
|
||||
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) })
|
||||
Some(self.xkb_state.key_get_utf8(xkb::Keycode::new(keycode + 8)))
|
||||
}
|
||||
|
||||
pub(crate) fn compose_feed(&mut self, keysym: u32) -> Option<ffi::xkb_compose_feed_result> {
|
||||
if !self.ready() || self.xkb_compose_state.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { (XKBH.xkb_compose_state_feed)(self.xkb_compose_state, keysym) })
|
||||
pub(crate) fn compose_feed(&mut self, keysym: u32) -> Option<xkb::compose::FeedResult> {
|
||||
self.xkb_compose_state
|
||||
.as_mut()
|
||||
.map(|compose_state| compose_state.feed(xkb::Keysym::new(keysym)))
|
||||
}
|
||||
|
||||
pub(crate) fn compose_status(&mut self) -> Option<ffi::xkb_compose_status> {
|
||||
if !self.ready() || self.xkb_compose_state.is_null() {
|
||||
return None;
|
||||
}
|
||||
Some(unsafe { (XKBH.xkb_compose_state_get_status)(self.xkb_compose_state) })
|
||||
pub(crate) fn compose_status(&mut self) -> Option<xkb::compose::Status> {
|
||||
self.xkb_compose_state
|
||||
.as_mut()
|
||||
.map(|compose_state| compose_state.status())
|
||||
}
|
||||
|
||||
pub(crate) fn compose_get_utf8(&mut self) -> Option<String> {
|
||||
if !self.ready() || self.xkb_compose_state.is_null() {
|
||||
return None;
|
||||
match &mut self.xkb_compose_state {
|
||||
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> {
|
||||
let xkbh = match ffi::XKBCOMMON_OPTION.as_ref() {
|
||||
Some(h) => h,
|
||||
None => return Err(Error::XKBNotFound),
|
||||
pub(crate) fn new_from_fd(fd: OwnedFd, size: usize) -> Result<KbState, std::io::Error> {
|
||||
let xkb_context = xkb::Context::new(xkb::CONTEXT_NO_FLAGS);
|
||||
let xkb_keymap = unsafe {
|
||||
xkb::Keymap::new_from_fd(
|
||||
&xkb_context,
|
||||
fd,
|
||||
size,
|
||||
xkb::KEYMAP_FORMAT_TEXT_V1,
|
||||
xkb::KEYMAP_COMPILE_NO_FLAGS,
|
||||
)?
|
||||
.unwrap()
|
||||
};
|
||||
let xkb_context =
|
||||
unsafe { (xkbh.xkb_context_new)(ffi::xkb_context_flags::XKB_CONTEXT_NO_FLAGS) };
|
||||
if xkb_context.is_null() {
|
||||
return Err(Error::XKBNotFound);
|
||||
}
|
||||
|
||||
let xkb_state = xkb::State::new(&xkb_keymap);
|
||||
let mut me = KbState {
|
||||
xkb_context,
|
||||
xkb_keymap: ptr::null_mut(),
|
||||
xkb_state: ptr::null_mut(),
|
||||
xkb_compose_table: ptr::null_mut(),
|
||||
xkb_compose_state: ptr::null_mut(),
|
||||
xkb_keymap,
|
||||
xkb_state,
|
||||
xkb_compose_table: None,
|
||||
xkb_compose_state: None,
|
||||
mods_state: ModifiersState::new(),
|
||||
locked: false,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
me.init_compose();
|
||||
}
|
||||
me.init_compose();
|
||||
|
||||
Ok(me)
|
||||
}
|
||||
|
||||
pub(crate) fn from_rmlvo(rmlvo: RMLVO) -> Result<KbState, Error> {
|
||||
fn to_cstring(s: Option<String>) -> Result<Option<CString>, Error> {
|
||||
s.map_or(Ok(None), |s| CString::new(s).map(Option::Some))
|
||||
.map_err(|_| Error::BadNames)
|
||||
}
|
||||
pub(crate) fn init_compose(&mut self) {
|
||||
let locale = env::var_os("LC_ALL")
|
||||
.filter(|v| !v.is_empty())
|
||||
.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 rules = to_cstring(rmlvo.rules)?;
|
||||
let model = to_cstring(rmlvo.model)?;
|
||||
let layout = to_cstring(rmlvo.layout)?;
|
||||
let variant = to_cstring(rmlvo.variant)?;
|
||||
let options = to_cstring(rmlvo.options)?;
|
||||
|
||||
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()),
|
||||
let compose_table = match xkb::compose::Table::new_from_locale(
|
||||
&self.xkb_context,
|
||||
&locale,
|
||||
xkb::compose::COMPILE_NO_FLAGS,
|
||||
) {
|
||||
Ok(t) => t,
|
||||
Err(_) => return,
|
||||
};
|
||||
|
||||
unsafe {
|
||||
state.init_with_rmlvo(xkb_names)?;
|
||||
}
|
||||
let compose_state = xkb::compose::State::new(&compose_table, xkb::compose::STATE_NO_FLAGS);
|
||||
|
||||
state.locked = true;
|
||||
Ok(state)
|
||||
self.xkb_compose_table = Some(compose_table);
|
||||
self.xkb_compose_state = Some(compose_state);
|
||||
}
|
||||
|
||||
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
|
||||
pub(crate) fn key_repeats(&mut self, keycode: xkb::Keycode) -> bool {
|
||||
self.xkb_keymap.key_repeats(keycode)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -386,16 +159,3 @@ impl KbState {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+358
-351
@@ -1,385 +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 keyboard;
|
||||
mod cmd;
|
||||
mod color;
|
||||
mod config;
|
||||
mod configfmt;
|
||||
mod desktop;
|
||||
mod doublemempool;
|
||||
mod draw;
|
||||
mod event;
|
||||
mod fonts;
|
||||
mod widget;
|
||||
mod keyboard;
|
||||
mod state;
|
||||
mod utils;
|
||||
mod widgets;
|
||||
|
||||
#[macro_use]
|
||||
extern crate dlib;
|
||||
use wayland_client::{Connection, QueueHandle, WaylandSource};
|
||||
|
||||
use app::{App, OutputMode};
|
||||
use cmd::Cmd;
|
||||
use calloop::{
|
||||
EventLoop,
|
||||
timer::{TimeoutAction, Timer},
|
||||
};
|
||||
use chrono::{Duration, Local, Timelike};
|
||||
|
||||
use buffer::BufferView;
|
||||
use config::Config;
|
||||
use configfmt::ConfigFmt;
|
||||
use fonts::{FontLoader, FontMap, FontSeeker};
|
||||
use widget::WaitContext;
|
||||
use event::{Event, Events};
|
||||
use fonts::{FontMap, MaybeFontMap};
|
||||
use keyboard::{KeyRepeatSource, RepeatMessage};
|
||||
use state::State;
|
||||
use utils::xdg;
|
||||
use widgets::{Geometry, Widget};
|
||||
|
||||
enum Mode {
|
||||
Start,
|
||||
Daemonize,
|
||||
StartOrKill,
|
||||
ToggleVisible,
|
||||
PrintConfig(ConfigFmt),
|
||||
use std::{
|
||||
env, fs,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader, Write},
|
||||
os::unix::net::{UnixListener, UnixStream},
|
||||
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() {
|
||||
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") {
|
||||
Ok(dir) => dir + "/wldash",
|
||||
Err(_) => "/tmp/wldash".to_string(),
|
||||
};
|
||||
let config_home = match env::var("XDG_CONFIG_HOME") {
|
||||
Ok(dir) => dir + "/wldash",
|
||||
Err(_) => match env::var("HOME") {
|
||||
Ok(home) => home + "/.config/wldash",
|
||||
_ => panic!("unable to find user folder"),
|
||||
|
||||
let events = Events::new(ping_sender);
|
||||
|
||||
let mut fm = FontMap::new();
|
||||
|
||||
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`
|
||||
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()))
|
||||
if let Some(true) = config.server {
|
||||
if let Ok(mut socket) = UnixStream::connect(socket_path.clone()) {
|
||||
socket.write_all(b"kill\n").unwrap();
|
||||
return;
|
||||
};
|
||||
|
||||
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()) {
|
||||
socket.write_all(b"kill\n").unwrap();
|
||||
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 listener = UnixListener::bind(socket_path.clone()).unwrap();
|
||||
|
||||
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)
|
||||
{
|
||||
let _ = std::fs::remove_file(socket_path.clone());
|
||||
let _ = thread::Builder::new()
|
||||
.name("ipc_server".to_string())
|
||||
.spawn(move || {
|
||||
let listener = UnixListener::bind(socket_path.clone()).unwrap();
|
||||
loop {
|
||||
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 || {
|
||||
let _ = thread::Builder::new().name("ipc_client".to_string()).spawn(
|
||||
move || {
|
||||
let r = BufReader::new(stream);
|
||||
for line in r.lines() {
|
||||
match line {
|
||||
Ok(v) => match v.as_str() {
|
||||
"kill" => {
|
||||
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,
|
||||
Ok(ref cmd) if cmd == "kill" => {
|
||||
std::process::exit(0);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
client_pipe.write_all(&[0x1]).unwrap();
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if wait_ctx.target_time.is_some()
|
||||
&& wait_ctx.fds[wait_ctx.fds.len() - 1]
|
||||
.revents()
|
||||
.unwrap()
|
||||
.contains(PollFlags::POLLIN)
|
||||
{
|
||||
timer.read();
|
||||
let mut qq = q.lock().unwrap();
|
||||
qq.push_back(Cmd::KeyboardTest);
|
||||
qq.push_back(Cmd::Draw);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let layout = Rc::new(config.widget.construct_layout(&mut 0));
|
||||
let mut widgets: Vec<Box<dyn Widget>> = Vec::new();
|
||||
config
|
||||
.widget
|
||||
.construct_widgets(&mut widgets, &mut fm, &events);
|
||||
|
||||
let font_thread = thread::Builder::new()
|
||||
.name("fontloader".to_string())
|
||||
.spawn(move || {
|
||||
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
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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'"));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
pub mod desktop;
|
||||
mod inish;
|
||||
pub mod xdg;
|
||||
@@ -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
@@ -1,458 +0,0 @@
|
||||
use crate::buffer::Buffer;
|
||||
use crate::color::Color;
|
||||
use chrono::NaiveDateTime;
|
||||
use nix::poll::PollFd;
|
||||
pub use crate::keyboard::{KeyState, ModifiersState};
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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)),
|
||||
_ => (),
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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) {}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
+77
-105
@@ -1,27 +1,23 @@
|
||||
use crate::color::Color;
|
||||
use crate::widget::WaitContext;
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
widgets::bar_widget::{BarWidget, BarWidgetImpl},
|
||||
use std::{
|
||||
fs::{self, OpenOptions},
|
||||
io::{Error, Write},
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use std::fs;
|
||||
use std::fs::OpenOptions;
|
||||
use std::io::Write;
|
||||
use std::io::{Error, ErrorKind};
|
||||
use std::path::{Path, PathBuf};
|
||||
use walkdir::WalkDir;
|
||||
|
||||
pub struct Backlight {
|
||||
device_path: PathBuf,
|
||||
cur_brightness: u64,
|
||||
max_brightness: u64,
|
||||
}
|
||||
use crate::{
|
||||
color::Color,
|
||||
event::PointerButton,
|
||||
fonts::FontMap,
|
||||
widgets::bar_widget::{BarWidget, BarWidgetImpl},
|
||||
};
|
||||
|
||||
fn read_file_as_u64(path: &Path) -> Result<u64, Error> {
|
||||
let mut s = fs::read_to_string(path)?;
|
||||
s.pop();
|
||||
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> {
|
||||
@@ -29,113 +25,89 @@ fn write_file_as_u64(path: &Path, value: u64) -> Result<(), Error> {
|
||||
file.write_fmt(format_args!("{}", value))
|
||||
}
|
||||
|
||||
pub struct Backlight {
|
||||
device_path: 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 {
|
||||
pub fn update(&mut self) -> Result<(), Error> {
|
||||
self.cur_brightness = read_file_as_u64(self.device_path.join("brightness").as_path())?;
|
||||
self.max_brightness = read_file_as_u64(self.device_path.join("max_brightness").as_path())?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn sync(&mut self) -> Result<(), Error> {
|
||||
write_file_as_u64(
|
||||
self.device_path.join("brightness").as_path(),
|
||||
self.cur_brightness,
|
||||
)?;
|
||||
self.update()?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn brightness(&self) -> f32 {
|
||||
if self.cur_brightness > self.max_brightness {
|
||||
// what.
|
||||
return 1.0;
|
||||
fn update(&mut self) {
|
||||
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
|
||||
self.cur = cur;
|
||||
}
|
||||
if let Ok(max) = read_file_as_u64(self.device_path.join("max_brightness").as_path()) {
|
||||
self.max = max;
|
||||
}
|
||||
|
||||
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 {
|
||||
(self.cur_brightness as i64 + inc) as u64
|
||||
// Wraps the impl in the generic BarWidget on purpose.
|
||||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new(path: Option<&str>, fm: &mut FontMap, font: &'static str, size: f32) -> BarWidget {
|
||||
let device_path = match path {
|
||||
Some(path) => Some(Path::new("/sys/class/backlight").join(path)),
|
||||
None => find_device(),
|
||||
};
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn new<'a>(
|
||||
path: &str,
|
||||
font: FontRef<'a>,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
) -> Result<Box<BarWidget<'a>>, Error> {
|
||||
let mut dev = Backlight {
|
||||
device_path: Path::new("/sys/class/backlight").to_path_buf().join(path),
|
||||
cur_brightness: 0,
|
||||
max_brightness: 0,
|
||||
device_path: device_path.expect("did not find backlight device"),
|
||||
cur: 0,
|
||||
max: 0,
|
||||
dirty: true,
|
||||
};
|
||||
|
||||
dev.update()?;
|
||||
dev.update();
|
||||
BarWidget::new(Box::new(dev), fm, font, size)
|
||||
}
|
||||
|
||||
Ok(BarWidget::new_simple(
|
||||
font,
|
||||
font_size,
|
||||
length,
|
||||
Box::new(dev),
|
||||
))
|
||||
pub fn detect() -> bool {
|
||||
find_device().is_some()
|
||||
}
|
||||
|
||||
pub fn set(&mut self, brightness: f32) {
|
||||
let val = (self.max as f32 * brightness.clamp(0.0, 1.0)) as u64;
|
||||
let _ = write_file_as_u64(self.device_path.join("brightness").as_path(), val);
|
||||
if let Ok(cur) = read_file_as_u64(self.device_path.join("brightness").as_path()) {
|
||||
self.cur = cur;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidgetImpl for Backlight {
|
||||
fn wait(&mut self, _: &mut WaitContext) {}
|
||||
fn name(&self) -> &str {
|
||||
fn name(&self) -> &'static str {
|
||||
"backlight"
|
||||
}
|
||||
fn value(&self) -> f32 {
|
||||
self.brightness()
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
fn value(&mut self) -> f32 {
|
||||
if self.max == 0 || self.cur > self.max {
|
||||
// what.
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
self.dirty = false;
|
||||
self.cur as f32 / self.max as f32
|
||||
}
|
||||
fn color(&self) -> Color {
|
||||
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||
Color::WHITE
|
||||
}
|
||||
fn inc(&mut self, inc: f32) {
|
||||
self.add(inc).unwrap();
|
||||
match self.sync() {
|
||||
Ok(val) => val,
|
||||
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
|
||||
fn click(&mut self, pos: f32, btn: PointerButton) {
|
||||
self.dirty = true;
|
||||
if let PointerButton::Left = btn {
|
||||
self.set(pos)
|
||||
};
|
||||
match self.sync() {
|
||||
Ok(val) => val,
|
||||
Err(err) => {
|
||||
eprintln!("Error while trying to change brightness: {}", err);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+89
-144
@@ -1,163 +1,108 @@
|
||||
use crate::color::Color;
|
||||
use crate::draw::{draw_bar, draw_box, Font};
|
||||
use std::{cmp::min, default::Default};
|
||||
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget},
|
||||
buffer::BufferView,
|
||||
color::Color,
|
||||
draw::{draw_bar, draw_box},
|
||||
event::{Event, PointerButton},
|
||||
fonts::FontMap,
|
||||
widgets::{Geometry, Widget},
|
||||
};
|
||||
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
pub trait BarWidgetImpl {
|
||||
fn wait(&mut self, ctx: &mut WaitContext);
|
||||
fn name(&self) -> &str;
|
||||
fn value(&self) -> f32;
|
||||
fn get_dirty(&self) -> bool {
|
||||
false
|
||||
}
|
||||
fn name(&self) -> &'static str;
|
||||
fn value(&mut self) -> f32;
|
||||
fn color(&self) -> Color;
|
||||
fn inc(&mut self, inc: f32);
|
||||
fn set(&mut self, val: f32);
|
||||
fn toggle(&mut self);
|
||||
fn click(&mut self, _pos: f32, _btn: PointerButton) {}
|
||||
}
|
||||
|
||||
pub struct BarWidget<'a> {
|
||||
bar_impl: Box<dyn BarWidgetImpl + Send>,
|
||||
font: Font<'a>,
|
||||
font_size: u32,
|
||||
length: u32,
|
||||
dirty: Arc<Mutex<bool>>,
|
||||
pub struct BarWidget {
|
||||
font: &'static str,
|
||||
size: f32,
|
||||
geometry: Geometry,
|
||||
inner_widget: Box<dyn BarWidgetImpl>,
|
||||
}
|
||||
|
||||
impl<'a> BarWidget<'a> {
|
||||
pub fn new_simple(
|
||||
font: FontRef,
|
||||
font_size: f32,
|
||||
length: u32,
|
||||
w: Box<dyn BarWidgetImpl + Send>,
|
||||
) -> Box<BarWidget> {
|
||||
let mut font = Font::new(font, font_size);
|
||||
font.add_str_to_cache(w.name());
|
||||
|
||||
Box::new(BarWidget {
|
||||
bar_impl: w,
|
||||
dirty: Arc::new(Mutex::new(true)),
|
||||
impl BarWidget {
|
||||
pub fn new(
|
||||
inner_widget: Box<dyn BarWidgetImpl>,
|
||||
fm: &mut FontMap,
|
||||
font: &'static str,
|
||||
size: f32,
|
||||
) -> BarWidget {
|
||||
fm.queue_font(font, size, inner_widget.name());
|
||||
BarWidget {
|
||||
geometry: Default::default(),
|
||||
inner_widget,
|
||||
font,
|
||||
font_size: font_size as u32,
|
||||
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,
|
||||
}))
|
||||
size,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for BarWidget<'a> {
|
||||
fn wait(&mut self, ctx: &mut WaitContext) {
|
||||
self.bar_impl.wait(ctx);
|
||||
}
|
||||
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))?;
|
||||
buf.memset(ctx.bg);
|
||||
|
||||
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,
|
||||
})
|
||||
impl Widget for BarWidget {
|
||||
fn geometry(&self) -> Geometry {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
|
||||
fn mouse_click(&mut self, button: u32, (x, _): (u32, u32)) {
|
||||
*self.dirty.lock().unwrap() = true;
|
||||
match button {
|
||||
272 => {
|
||||
let offset = 5 * self.font_size;
|
||||
if x > offset {
|
||||
self.bar_impl
|
||||
.set(((x - offset) + 1) as f32 / (self.length - offset) as f32);
|
||||
}
|
||||
}
|
||||
273 => self.bar_impl.toggle(),
|
||||
x => {
|
||||
dbg!(x);
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.inner_widget.get_dirty()
|
||||
}
|
||||
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
let fg = Color::WHITE;
|
||||
|
||||
font.draw_text(view, fg, self.inner_widget.name()).unwrap();
|
||||
let size = self.size.ceil() as u32;
|
||||
let bar_offset = 4 * size;
|
||||
let val = self.inner_widget.value();
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
fn event(&mut self, event: &Event) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
+160
-214
@@ -1,240 +1,186 @@
|
||||
use crate::cmd::Cmd;
|
||||
use crate::color::Color;
|
||||
use crate::widget::WaitContext;
|
||||
use std::{
|
||||
error::Error,
|
||||
sync::{Arc, Mutex},
|
||||
thread,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use dbus::blocking::{
|
||||
LocalConnection,
|
||||
stdintf::org_freedesktop_dbus::{Properties, PropertiesPropertiesChanged},
|
||||
};
|
||||
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
color::Color,
|
||||
event::{Event, Events},
|
||||
fonts::FontMap,
|
||||
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 {
|
||||
Unknown,
|
||||
Charging,
|
||||
Discharging,
|
||||
Empty,
|
||||
Full,
|
||||
NotCharging,
|
||||
Unknown,
|
||||
FullyCharged,
|
||||
PendingCharge,
|
||||
PendingDischarge,
|
||||
}
|
||||
|
||||
impl UpowerBattery {
|
||||
pub fn from_device(
|
||||
dirty: Arc<Mutex<bool>>,
|
||||
sender: Sender<Cmd>,
|
||||
device: &str,
|
||||
) -> Result<Self, ::std::io::Error> {
|
||||
let con = dbus::Connection::get_private(dbus::BusType::System).map_err(|_| {
|
||||
::std::io::Error::new(::std::io::ErrorKind::Other, "unable to open dbus")
|
||||
})?;
|
||||
let device_path = format!("/org/freedesktop/UPower/devices/{}", device);
|
||||
impl UpowerBatteryState {
|
||||
fn from_dbus(val: u64) -> UpowerBatteryState {
|
||||
match val {
|
||||
1 => UpowerBatteryState::Charging,
|
||||
2 => UpowerBatteryState::Discharging,
|
||||
3 => UpowerBatteryState::Empty,
|
||||
4 => UpowerBatteryState::FullyCharged,
|
||||
5 => UpowerBatteryState::PendingCharge,
|
||||
6 => UpowerBatteryState::PendingDischarge,
|
||||
_ => UpowerBatteryState::Unknown,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let rule = format!(
|
||||
"type='signal',\
|
||||
path='{}',\
|
||||
interface='org.freedesktop.DBus.Properties',\
|
||||
member='PropertiesChanged'",
|
||||
&device_path
|
||||
);
|
||||
con.add_match(&rule).map_err(|_| {
|
||||
::std::io::Error::new(
|
||||
::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",
|
||||
));
|
||||
struct InnerBattery {
|
||||
value: f32,
|
||||
state: UpowerBatteryState,
|
||||
dirty: bool,
|
||||
}
|
||||
|
||||
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),
|
||||
);
|
||||
let present: bool = proxy.get(DEVICE_IFACE, "IsPresent")?;
|
||||
Ok(present)
|
||||
}
|
||||
|
||||
fn monitor(
|
||||
inner: Arc<Mutex<InnerBattery>>,
|
||||
events: Arc<Mutex<Events>>,
|
||||
) -> Result<(), Box<dyn Error>> {
|
||||
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 mut inner = inner.lock().unwrap();
|
||||
inner.value = capacity as f32 / 100.;
|
||||
inner.state = UpowerBatteryState::from_dbus(state as u64);
|
||||
inner.dirty = true;
|
||||
}
|
||||
events.lock().unwrap().add_event(Event::PowerUpdate);
|
||||
|
||||
proxy.match_signal(
|
||||
move |c: PropertiesPropertiesChanged, _: &LocalConnection, _: &dbus::Message| {
|
||||
if c.interface_name != DEVICE_IFACE {
|
||||
return true;
|
||||
}
|
||||
let mut inner = inner.lock().unwrap();
|
||||
for (key, value) in c.changed_properties {
|
||||
match key.as_str() {
|
||||
"State" => {
|
||||
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);
|
||||
}
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
};
|
||||
|
||||
// https://upower.freedesktop.org/docs/Device.html#Device:Type
|
||||
if upower_type.0 != 2 {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
"UPower device is not a battery.",
|
||||
));
|
||||
}
|
||||
|
||||
let capacity: f64 = match get_upower_property(&con, &device_path, "Percentage")?
|
||||
.get1::<dbus::arg::Variant<f64>>()
|
||||
{
|
||||
Some(v) => v.0,
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
"no such upower device",
|
||||
));
|
||||
}
|
||||
};
|
||||
let state: UpowerBatteryState = match get_upower_property(&con, &device_path, "State")?
|
||||
.get1::<dbus::arg::Variant<u32>>()
|
||||
{
|
||||
Some(v) => match v.0 {
|
||||
1 => UpowerBatteryState::Charging,
|
||||
2 => UpowerBatteryState::Discharging,
|
||||
3 => UpowerBatteryState::Empty,
|
||||
4 => UpowerBatteryState::Full,
|
||||
5 => UpowerBatteryState::NotCharging,
|
||||
6 => UpowerBatteryState::Discharging,
|
||||
_ => UpowerBatteryState::Unknown,
|
||||
},
|
||||
None => {
|
||||
return Err(::std::io::Error::new(
|
||||
::std::io::ErrorKind::Other,
|
||||
"no such upower device",
|
||||
));
|
||||
}
|
||||
};
|
||||
true
|
||||
},
|
||||
)?;
|
||||
|
||||
let fds = con.watch_fds();
|
||||
if fds.len() != 1 {
|
||||
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))
|
||||
})
|
||||
loop {
|
||||
conn.process(Duration::from_millis(60000))?;
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidgetImpl for UpowerBattery {
|
||||
fn wait(&mut self, ctx: &mut WaitContext) {
|
||||
for _ in self
|
||||
.con
|
||||
.as_ref()
|
||||
.watch_handle(self.watch.fd(), dbus::WatchEvent::Readable as u32)
|
||||
{
|
||||
let capacity = get_upower_property(self.con.as_ref(), &self.device_path, "Percentage")
|
||||
.unwrap()
|
||||
.get1::<dbus::arg::Variant<f64>>()
|
||||
.unwrap()
|
||||
.0;
|
||||
let state = match get_upower_property(self.con.as_ref(), &self.device_path, "State")
|
||||
.unwrap()
|
||||
.get1::<dbus::arg::Variant<u32>>()
|
||||
.unwrap()
|
||||
.0
|
||||
{
|
||||
1 => UpowerBatteryState::Charging,
|
||||
2 => UpowerBatteryState::Discharging,
|
||||
3 => UpowerBatteryState::Empty,
|
||||
4 => UpowerBatteryState::Full,
|
||||
5 => UpowerBatteryState::NotCharging,
|
||||
6 => UpowerBatteryState::Discharging,
|
||||
_ => UpowerBatteryState::Unknown,
|
||||
};
|
||||
self.state = state;
|
||||
self.capacity = capacity;
|
||||
*self.dirty.lock().unwrap() = true;
|
||||
self.sender.send(Cmd::Draw).unwrap();
|
||||
}
|
||||
fn start_monitor(inner: Arc<Mutex<InnerBattery>>, events: Arc<Mutex<Events>>) {
|
||||
thread::Builder::new()
|
||||
.name("battmon".to_string())
|
||||
.spawn(move || {
|
||||
// The release profile uses panic = "abort": a panic here would take
|
||||
// the whole dashboard down, so report and leave the bar at 0.
|
||||
if let Err(e) = monitor(inner, events) {
|
||||
eprintln!("wldash: battery monitor stopped: {}", e);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
ctx.fds
|
||||
.push(PollFd::new(self.watch.fd(), PollFlags::POLLIN));
|
||||
pub struct Battery {
|
||||
inner: Arc<Mutex<InnerBattery>>,
|
||||
}
|
||||
|
||||
impl Battery {
|
||||
// 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 battery = Battery {
|
||||
inner: Arc::new(Mutex::new(InnerBattery {
|
||||
value: 0.,
|
||||
state: UpowerBatteryState::Unknown,
|
||||
dirty: false,
|
||||
})),
|
||||
};
|
||||
start_monitor(battery.inner.clone(), events);
|
||||
BarWidget::new(Box::new(battery), fm, font, size)
|
||||
}
|
||||
fn name(&self) -> &str {
|
||||
|
||||
pub fn detect() -> bool {
|
||||
battery_present().unwrap_or(false)
|
||||
}
|
||||
}
|
||||
|
||||
impl BarWidgetImpl for Battery {
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.inner.lock().unwrap().dirty
|
||||
}
|
||||
fn name(&self) -> &'static str {
|
||||
"battery"
|
||||
}
|
||||
fn value(&self) -> f32 {
|
||||
(self.capacity as f32) / 100.0
|
||||
fn value(&mut self) -> f32 {
|
||||
let mut inner = self.inner.lock().unwrap();
|
||||
inner.dirty = false;
|
||||
inner.value
|
||||
}
|
||||
fn color(&self) -> Color {
|
||||
match self.state {
|
||||
UpowerBatteryState::Discharging | UpowerBatteryState::Unknown => {
|
||||
if self.capacity > 10.0 {
|
||||
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||
} else {
|
||||
Color::new(1.0, 0.5, 0.0, 1.0)
|
||||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
let inner = self.inner.lock().unwrap();
|
||||
match inner.state {
|
||||
UpowerBatteryState::Charging | UpowerBatteryState::FullyCharged => Color::LIGHTGREEN,
|
||||
_ if inner.value > 0.25 => Color::WHITE,
|
||||
_ if inner.value > 0.1 => Color::DARKORANGE,
|
||||
_ => Color::RED,
|
||||
}
|
||||
}
|
||||
fn inc(&mut self, _: f32) {}
|
||||
fn set(&mut self, _: f32) {}
|
||||
fn toggle(&mut self) {}
|
||||
}
|
||||
|
||||
+282
-217
@@ -1,37 +1,114 @@
|
||||
use crate::buffer::Buffer;
|
||||
use crate::color::Color;
|
||||
use crate::draw::Font;
|
||||
use std::default::Default;
|
||||
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget},
|
||||
buffer::BufferView,
|
||||
color::Color,
|
||||
event::Event,
|
||||
fonts::FontMap,
|
||||
widgets::{Geometry, Widget},
|
||||
};
|
||||
|
||||
use chrono::{Datelike, NaiveDate, NaiveDateTime};
|
||||
use chrono::{Datelike, Local, NaiveDate};
|
||||
|
||||
pub struct Calendar<'a> {
|
||||
cur_date: NaiveDate,
|
||||
pub struct Calendar {
|
||||
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,
|
||||
offset: f64,
|
||||
sections: u32,
|
||||
font_size: u32,
|
||||
calendar_cache: Font<'a>,
|
||||
month_cache: Font<'a>,
|
||||
year_cache: Font<'a>,
|
||||
day_cache: Font<'a>,
|
||||
geometry: Geometry,
|
||||
year_width: u32,
|
||||
date_width: u32,
|
||||
day_width: u32,
|
||||
}
|
||||
|
||||
impl<'a> Calendar<'a> {
|
||||
fn draw_month(
|
||||
&self,
|
||||
buf: &mut Buffer,
|
||||
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 DAY_FACTOR: f32 = 0.6;
|
||||
const YEAR_FACTOR: f32 = 0.75;
|
||||
const MONTH_FACTOR: f32 = 1.25;
|
||||
const DATE_FACTOR: f32 = 1.;
|
||||
|
||||
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() {
|
||||
1 => "January",
|
||||
2 => "February",
|
||||
@@ -47,234 +124,222 @@ impl<'a> Calendar<'a> {
|
||||
12 => "December",
|
||||
_ => 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() {
|
||||
self.year_cache.draw_text(
|
||||
&mut buf.offset((self.font_size * 20, 0))?,
|
||||
background_color,
|
||||
&Color::new(0.8, 0.8, 0.8, 1.0),
|
||||
&format!("{:}", time.year()),
|
||||
)?;
|
||||
let year_font = fonts.get_font(self.font, self.size * YEAR_FACTOR);
|
||||
year_font
|
||||
.draw_text(
|
||||
&mut view.offset((cal_width - self.year_width, 0)),
|
||||
Color::GREY80,
|
||||
&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
|
||||
//
|
||||
let wk = time.iso_week();
|
||||
self.calendar_cache.draw_text(
|
||||
&mut buf.offset((0, y_off * self.font_size * 2 + self.font_size * 4))?,
|
||||
background_color,
|
||||
&Color::new(0.75, 0.75, 0.75, 1.0),
|
||||
&format!("{:02}", wk.week()),
|
||||
)?;
|
||||
x_pos += 1;
|
||||
cal_font
|
||||
.draw_text(
|
||||
&mut view.offset((
|
||||
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()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
//
|
||||
// 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() {
|
||||
Color::new(1.0, 1.0, 1.0, 1.0)
|
||||
Color::WHITE
|
||||
} else if x_pos < 6 {
|
||||
Color::GREY50
|
||||
} else {
|
||||
Color::new(0.5, 0.5, 0.5, 1.0)
|
||||
Color::GREY35
|
||||
};
|
||||
|
||||
self.calendar_cache.draw_text(
|
||||
&mut buf.offset((
|
||||
x_pos * self.font_size * 3,
|
||||
y_off * self.font_size * 2 + self.font_size * 4,
|
||||
))?,
|
||||
background_color,
|
||||
&c,
|
||||
&format!("{:02}", time.day()),
|
||||
)?;
|
||||
cal_font
|
||||
.draw_text(
|
||||
&mut view.offset((
|
||||
x_pos * date_offset,
|
||||
y_off * (self.size * LINE_HEIGHT).ceil() as u32
|
||||
+ (self.size * MONTH_FACTOR).ceil() as u32,
|
||||
)),
|
||||
c,
|
||||
&format!("{:02}", time.day()),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let t = time.with_day(time.day() + 1);
|
||||
if t.is_none() {
|
||||
done = true;
|
||||
break;
|
||||
break 'row;
|
||||
}
|
||||
time = t.unwrap();
|
||||
x_pos += 1;
|
||||
}
|
||||
|
||||
y_off += 1;
|
||||
}
|
||||
Ok(buf.get_signed_bounds())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Calendar<'a> {
|
||||
pub fn new(
|
||||
time: NaiveDateTime,
|
||||
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,
|
||||
)
|
||||
impl Widget for Calendar {
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn draw(
|
||||
&mut self,
|
||||
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));
|
||||
fn geometry(&self) -> Geometry {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn event(&mut self, event: &Event) {
|
||||
if let Event::NewMinute = event {
|
||||
self.dirty = true;
|
||||
}
|
||||
self.dirty = false;
|
||||
self.cur_date = ctx.time.date();
|
||||
}
|
||||
|
||||
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
|
||||
buf.memset(ctx.bg);
|
||||
let time = ctx.time.date();
|
||||
let mut t = time.with_day(1).unwrap();
|
||||
let o = (self.offset / 100.0) as i32;
|
||||
|
||||
let cals = self.sections - 1;
|
||||
let pre_cals = cals / 2;
|
||||
for _ in 0..pre_cals {
|
||||
t = t.pred().with_day(1).unwrap();
|
||||
}
|
||||
|
||||
if o != 0 {
|
||||
let mut month = (t.month() - 1) as i32 + o;
|
||||
let mut year = t.year();
|
||||
while month > 11 {
|
||||
year += 1;
|
||||
month -= 12;
|
||||
}
|
||||
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.with_year(t.year() + 1).unwrap().with_month(1).unwrap()
|
||||
} else {
|
||||
t.with_month(t.month() + 1).unwrap()
|
||||
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,
|
||||
};
|
||||
}
|
||||
Ok(DrawReport {
|
||||
width,
|
||||
height,
|
||||
damage: vec![buf.get_signed_bounds()],
|
||||
full_damage: false,
|
||||
})
|
||||
|
||||
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 cals = self.shown_sections_y * self.shown_sections_x;
|
||||
if cals >= 3 {
|
||||
t = t.pred_opt().unwrap().with_day(1).unwrap();
|
||||
}
|
||||
|
||||
for ydx in 0..self.shown_sections_y {
|
||||
for idx in 0..self.shown_sections_x {
|
||||
self.draw_month(
|
||||
fonts,
|
||||
&mut view.offset(((cal_width + cal_pad) * idx, cal_height * ydx)),
|
||||
time,
|
||||
t,
|
||||
);
|
||||
t = if t.month() == 12 {
|
||||
t.with_year(t.year() + 1).unwrap().with_month(1).unwrap()
|
||||
} else {
|
||||
t.with_month(t.month() + 1).unwrap()
|
||||
};
|
||||
}
|
||||
}
|
||||
self.dirty = false;
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
|
||||
fn mouse_click(&mut self, _: u32, (x, _): (u32, u32)) {
|
||||
let cal_pad = 7 * self.font_size * 3 + self.font_size * 5;
|
||||
let cals = self.sections - 1;
|
||||
let pre_cals = cals / 2;
|
||||
if x < pre_cals * cal_pad {
|
||||
self.offset -= 100.0;
|
||||
} else if x >= (pre_cals + 1) * cal_pad {
|
||||
self.offset += 100.0;
|
||||
} else {
|
||||
self.offset = 0.0;
|
||||
fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
|
||||
let year_font = fonts.get_font(self.font, self.size * YEAR_FACTOR);
|
||||
self.year_width = year_font.auto_widest("0123456789").unwrap() * 4;
|
||||
let cal_font = fonts.get_font(self.font, self.size * DATE_FACTOR);
|
||||
self.date_width = cal_font.auto_widest("0123456789").unwrap() * 2;
|
||||
let day_font = fonts.get_font(self.font, self.size * DAY_FACTOR);
|
||||
self.day_width = day_font.auto_widest("ADEFHIMNORSTUW").unwrap() * 3;
|
||||
|
||||
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;
|
||||
}
|
||||
self.dirty = true;
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
fn mouse_scroll(&mut self, (_, y): (f64, f64), _: (u32, u32)) {
|
||||
self.offset += y;
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+80
-79
@@ -1,92 +1,93 @@
|
||||
use crate::color::Color;
|
||||
use crate::draw::Font;
|
||||
use std::default::Default;
|
||||
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget},
|
||||
buffer::BufferView,
|
||||
color::Color,
|
||||
event::Event,
|
||||
fonts::FontMap,
|
||||
widgets::{Geometry, Widget},
|
||||
};
|
||||
|
||||
use chrono::{Duration, NaiveDateTime, Timelike};
|
||||
use chrono::{Local, Timelike};
|
||||
|
||||
pub struct Clock<'a> {
|
||||
cur_time: NaiveDateTime,
|
||||
clock_cache: Font<'a>,
|
||||
pub struct Clock {
|
||||
font: &'static str,
|
||||
size: f32,
|
||||
digit: u32,
|
||||
colon: u32,
|
||||
dirty: bool,
|
||||
geometry: Geometry,
|
||||
digit_width: u32,
|
||||
colon_width: u32,
|
||||
}
|
||||
|
||||
impl<'a> Clock<'a> {
|
||||
pub fn new(time: NaiveDateTime, font: FontRef, size: f32) -> ::std::io::Result<Box<Clock>> {
|
||||
let mut clock_cache = Font::new(font, size);
|
||||
clock_cache.add_str_to_cache("0123456789:");
|
||||
|
||||
let digit = clock_cache.auto_widest("123456789")?;
|
||||
let colon = (clock_cache.auto_widest(":")? as f32 * 1.25) as u32;
|
||||
|
||||
Ok(Box::new(Clock {
|
||||
cur_time: time,
|
||||
clock_cache,
|
||||
impl Clock {
|
||||
pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> Clock {
|
||||
fm.queue_font(font, size, "0123456789: ");
|
||||
Clock {
|
||||
font,
|
||||
size,
|
||||
digit,
|
||||
colon,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Clock<'a> {
|
||||
fn wait(&mut self, ctx: &mut WaitContext) {
|
||||
let target = (self.cur_time + Duration::seconds(60))
|
||||
.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(
|
||||
&mut self,
|
||||
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));
|
||||
dirty: true,
|
||||
geometry: Default::default(),
|
||||
digit_width: 0,
|
||||
colon_width: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cur_time = ctx.time;
|
||||
|
||||
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
|
||||
buf.memset(ctx.bg);
|
||||
|
||||
let digit = self.digit;
|
||||
let colon = self.colon;
|
||||
self.clock_cache.draw_text_fixed_width(
|
||||
buf,
|
||||
ctx.bg,
|
||||
&Color::new(1.0, 1.0, 1.0, 1.0),
|
||||
&[digit, digit, colon, digit, digit],
|
||||
&format!("{:02}:{:02}", ctx.time.hour(), ctx.time.minute()),
|
||||
)?;
|
||||
|
||||
Ok(DrawReport {
|
||||
width,
|
||||
height,
|
||||
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
|
||||
full_damage: false,
|
||||
})
|
||||
impl Widget for Clock {
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
|
||||
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {}
|
||||
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {}
|
||||
fn geometry(&self) -> Geometry {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn event(&mut self, event: &Event) {
|
||||
if let Event::NewMinute = event {
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let time = Local::now().naive_local();
|
||||
let fg = Color::WHITE;
|
||||
|
||||
let digit = self.digit_width;
|
||||
let colon = self.colon_width;
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
font.draw_text_fixed_width(
|
||||
view,
|
||||
fg,
|
||||
&[digit, digit, colon, digit, digit],
|
||||
&format!("{:02}:{:02}", time.hour(), time.minute()),
|
||||
)
|
||||
.unwrap();
|
||||
self.dirty = false;
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+76
-82
@@ -1,95 +1,89 @@
|
||||
use crate::color::Color;
|
||||
use crate::draw::Font;
|
||||
use std::default::Default;
|
||||
|
||||
use crate::{
|
||||
fonts::FontRef,
|
||||
widget::{DrawContext, DrawReport, KeyState, ModifiersState, WaitContext, Widget},
|
||||
buffer::BufferView,
|
||||
color::Color,
|
||||
event::Event,
|
||||
fonts::FontMap,
|
||||
widgets::{Geometry, Widget},
|
||||
};
|
||||
|
||||
use chrono::{Datelike, NaiveDateTime};
|
||||
use chrono::{Datelike, Local};
|
||||
|
||||
pub struct Date<'a> {
|
||||
cur_time: NaiveDateTime,
|
||||
date_cache: Font<'a>,
|
||||
pub struct Date {
|
||||
font: &'static str,
|
||||
size: f32,
|
||||
ch_width: u32,
|
||||
digit_width: u32,
|
||||
spacing_width: u32,
|
||||
dirty: bool,
|
||||
geometry: Geometry,
|
||||
}
|
||||
|
||||
impl<'a> Date<'a> {
|
||||
pub fn new(time: NaiveDateTime, font: FontRef, size: f32) -> ::std::io::Result<Box<Date>> {
|
||||
let mut date_cache = Font::new(font, size);
|
||||
let chs = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let digits = "0123456789,/ ";
|
||||
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,
|
||||
impl Date {
|
||||
pub fn new(fm: &mut FontMap, font: &'static str, size: f32) -> Date {
|
||||
fm.queue_font(font, size, "0123456789/, adehinortuFMSTW");
|
||||
Date {
|
||||
font,
|
||||
size,
|
||||
ch_width,
|
||||
digit_width,
|
||||
spacing_width,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> Widget for Date<'a> {
|
||||
fn wait(&mut self, _: &mut WaitContext) {}
|
||||
fn enter(&mut self) {}
|
||||
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(
|
||||
&mut self,
|
||||
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));
|
||||
dirty: true,
|
||||
geometry: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
self.cur_time = ctx.time;
|
||||
|
||||
let buf = &mut ctx.buf.subdimensions((pos.0, pos.1, width, height))?;
|
||||
buf.memset(ctx.bg);
|
||||
self.date_cache.draw_text(
|
||||
buf,
|
||||
ctx.bg,
|
||||
&Color::new(1.0, 1.0, 1.0, 1.0),
|
||||
&format!(
|
||||
"{:?}, {:02}/{:02}/{:4}",
|
||||
ctx.time.weekday(),
|
||||
ctx.time.day(),
|
||||
ctx.time.month(),
|
||||
ctx.time.year()
|
||||
),
|
||||
)?;
|
||||
|
||||
Ok(DrawReport {
|
||||
width,
|
||||
height,
|
||||
damage: vec![(pos.0 as i32, pos.1 as i32, width as i32, height as i32)],
|
||||
full_damage: false,
|
||||
})
|
||||
impl Widget for Date {
|
||||
fn get_dirty(&self) -> bool {
|
||||
self.dirty
|
||||
}
|
||||
|
||||
fn keyboard_input(&mut self, _: u32, _: ModifiersState, _: KeyState, _: Option<String>) {}
|
||||
fn mouse_click(&mut self, _: u32, _: (u32, u32)) {}
|
||||
fn mouse_scroll(&mut self, _: (f64, f64), _: (u32, u32)) {}
|
||||
fn geometry(&self) -> Geometry {
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn event(&mut self, event: &Event) {
|
||||
if let Event::NewMinute = event {
|
||||
self.dirty = true;
|
||||
}
|
||||
}
|
||||
|
||||
fn draw(&mut self, fonts: &mut FontMap, view: &mut BufferView) -> Geometry {
|
||||
let time = Local::now().naive_local();
|
||||
let fg = Color::WHITE;
|
||||
let font = fonts.get_font(self.font, self.size);
|
||||
font.draw_text(
|
||||
view,
|
||||
fg,
|
||||
&format!(
|
||||
"{}, {:02}/{:02}/{:4}",
|
||||
time.weekday(),
|
||||
time.day(),
|
||||
time.month(),
|
||||
time.year()
|
||||
),
|
||||
)
|
||||
.unwrap();
|
||||
self.dirty = false;
|
||||
self.geometry
|
||||
}
|
||||
|
||||
fn geometry_update(&mut self, fonts: &mut FontMap, geometry: &Geometry) -> Geometry {
|
||||
let width = (3. + 2. + 2. + 2. + 4.) * self.size / 2.;
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+832
-407
File diff suppressed because it is too large
Load Diff
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
@@ -1,10 +1,27 @@
|
||||
pub mod backlight;
|
||||
pub mod bar_widget;
|
||||
pub mod battery;
|
||||
pub mod calendar;
|
||||
pub mod clock;
|
||||
pub mod date;
|
||||
pub mod launcher;
|
||||
//mod alsaaudio;
|
||||
mod backlight;
|
||||
mod bar_widget;
|
||||
mod battery;
|
||||
mod calendar;
|
||||
mod clock;
|
||||
mod date;
|
||||
mod launcher;
|
||||
mod layout;
|
||||
mod line;
|
||||
mod pulseaudio;
|
||||
mod widget;
|
||||
|
||||
#[cfg(any(feature = "alsa-widget", feature = "pulseaudio-widget"))]
|
||||
pub mod audio;
|
||||
//pub use alsaaudio::AlsaAudio;
|
||||
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};
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user