Refine shell status icons and window controls; add installer

This commit is contained in:
Samuel Onoja committed 2026-09-26 05:46:15 +01:00
1 parent 956c739940
commit 686b0477d4
28 files changed
+519 -147

No files matched your search

Generated
+5
View File
@@ -2355,6 +2355,7 @@ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
"glam", "glam",
"iced_accessibility",
"lilt", "lilt",
"log", "log",
"mime 0.1.0", "mime 0.1.0",
@@ -2445,6 +2446,7 @@ dependencies = [
"bytes", "bytes",
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
"iced_accessibility",
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"raw-window-handle", "raw-window-handle",
@@ -2509,6 +2511,7 @@ dependencies = [
"build_helpers", "build_helpers",
"cosmic-client-toolkit", "cosmic-client-toolkit",
"dnd", "dnd",
"iced_accessibility",
"iced_renderer", "iced_renderer",
"iced_runtime", "iced_runtime",
"log", "log",
@@ -2529,6 +2532,7 @@ dependencies = [
"cosmic-client-toolkit", "cosmic-client-toolkit",
"cursor-icon", "cursor-icon",
"dnd", "dnd",
"iced_accessibility",
"iced_debug", "iced_debug",
"iced_futures", "iced_futures",
"iced_graphics", "iced_graphics",
@@ -3030,6 +3034,7 @@ dependencies = [
"i18n-embed", "i18n-embed",
"i18n-embed-fl", "i18n-embed-fl",
"iced", "iced",
"iced_accessibility",
"iced_core", "iced_core",
"iced_futures", "iced_futures",
"iced_renderer", "iced_renderer",
+2 -1
View File
@@ -165,7 +165,8 @@ target/release/ferese --backend nested --grant-effects --grant-shell-control --
``` ```
The Ferese mark enters overview, where workspace indicators become available. The Ferese mark enters overview, where workspace indicators become available.
The focused app appears on the left; status controls and day/time sit on the The active workspace's window list appears on the left, with click-to-focus
and a highlighted active window; status controls and day/time sit on the
right. The 28px bar has square screen edges, 19px icons, and no border or drop right. The 28px bar has square screen edges, 19px icons, and no border or drop
shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under shadow. Window spacing uses the workspace outer gap; `top_bar_window_gap` under
`[theme.geometry]` adds optional extra space (default 0). Solid and translucent `[theme.geometry]` adds optional extra space (default 0). Solid and translucent
+21 -1
View File
@@ -1,6 +1,26 @@
# Fedora / SDDM installation # Fedora / SDDM installation
Build and test in the checkout: From the checkout, run the installer as your normal user:
```sh
./scripts/install.sh
```
It builds all three release binaries with the locked dependencies, then uses
`sudo` (or `pkexec`) for installation. Install the Rust toolchain, native build
dependencies, `desktop-file-utils`, and D-Bus first. Cargo reports missing native
libraries during the build. The shell requires Rust 1.93 or newer.
Useful options:
```sh
./scripts/install.sh --dry-run
./scripts/install.sh --offline --release-id my-demo
./scripts/install.sh --skip-build --release-id existing-build
```
The script works from any directory and preserves your user configuration.
Use `--help` for all options. To test and install manually:
```sh ```sh
cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell --locked cargo test -p ferese -p feresectl -p ferese-animation -p ferese-core -p ferese-layout -p ferese-shell --locked
+82
View File
@@ -0,0 +1,82 @@
#!/usr/bin/env bash
# Build as the invoking user; elevate only the versioned system installation.
set -euo pipefail
usage() {
cat <<'EOF'
Usage: scripts/install.sh [options]
Build and install Ferese's compositor, shell, CLI, and login-screen session.
Run from any directory as your normal user. Existing user config is preserved.
--release-id ID Name the installed release (default: UTC timestamp + PID)
--skip-build Install existing target/release binaries
--offline Build using only cached Cargo dependencies
--dry-run Print commands without building or installing
-h, --help Show this help
The previous release remains available for rollback. Administrator
authentication is requested only after a successful build.
EOF
}
fail() { echo "ferese installer: $*" >&2; exit 1; }
repo_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
release_id="$(date -u +%Y%m%d-%H%M%S)-$$"
skip_build=false
offline=false
dry_run=false
while (($#)); do
case $1 in
--release-id)
(($# >= 2)) || fail '--release-id requires a value'
release_id=$2
shift 2
;;
--skip-build) skip_build=true; shift ;;
--offline) offline=true; shift ;;
--dry-run) dry_run=true; shift ;;
-h|--help) usage; exit 0 ;;
*) fail "unknown option: $1 (see --help)" ;;
esac
done
[[ $release_id =~ ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ ]] || fail 'invalid release ID'
[[ $(uname -s) == Linux ]] || fail 'Ferese requires Linux'
for tool in desktop-file-validate dbus-run-session; do
command -v "$tool" >/dev/null || fail "required command not found: $tool"
done
desktop-file-validate "$repo_dir/packaging/ferese.desktop"
build=(cargo build --manifest-path "$repo_dir/Cargo.toml"
--target-dir "$repo_dir/target" --release --locked
-p ferese -p ferese-shell -p feresectl)
if $offline; then build+=(--offline); fi
if ! $skip_build; then
((EUID != 0)) || fail 'build as your normal user, without sudo (or use --skip-build)'
command -v cargo >/dev/null || fail 'install the Rust toolchain and Cargo first'
fi
installer=(bash "$repo_dir/scripts/install-session.sh" "$release_id")
if ((EUID != 0)); then
if command -v sudo >/dev/null; then
installer=(sudo "${installer[@]}")
elif command -v pkexec >/dev/null; then
installer=(pkexec /usr/bin/bash "$repo_dir/scripts/install-session.sh" "$release_id")
else
fail 'system installation requires sudo or pkexec'
fi
fi
if $dry_run; then
if ! $skip_build; then printf 'Build: '; printf '%q ' "${build[@]}"; printf '\n'; fi
printf 'Install: '; printf '%q ' "${installer[@]}"; printf '\n'
exit 0
fi
cd -- "$repo_dir"
if ! $skip_build; then "${build[@]}"; fi
for name in ferese ferese-shell feresectl; do
[[ -x $repo_dir/target/release/$name ]] || fail "missing release binary: $name"
done
"${installer[@]}"
echo 'Installation complete. Log out and select Ferese at the login screen.'
+1 -1
View File
@@ -10,7 +10,7 @@ ferese-protocols = { path = "../../crates/ferese-protocols", default-features =
jiff.workspace = true jiff.workspace = true
# Keep GPU rendering available as a fallback/override. The shell defaults to # Keep GPU rendering available as a fallback/override. The shell defaults to
# tiny-skia to avoid a second GPU initialization during desktop startup. # tiny-skia to avoid a second GPU initialization during desktop startup.
libcosmic = { workspace = true, features = ["wgpu"] } libcosmic = { workspace = true, features = ["wgpu", "a11y"] }
serde.workspace = true serde.workspace = true
toml.workspace = true toml.workspace = true
wayland-client.workspace = true wayland-client.workspace = true
@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 25">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="3" height="6" rx="0.9" fill="currentColor" stroke="none"/> <rect x="5" y="9" width="4.75" height="6" rx="1.2" fill="#e0654f"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 344 B

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 50">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="6" height="6" rx="0.9" fill="currentColor" stroke="none"/> <rect x="5" y="9" width="9.5" height="6" rx="1.2" fill="#3d7be6"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 343 B

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery 75">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="9" height="6" rx="0.9" fill="currentColor" stroke="none"/> <rect x="5" y="9" width="14.25" height="6" rx="1.2" fill="#3d7be6"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 354 B

After

Width:  |  Height:  |  Size: 345 B

@@ -1,5 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery charging">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<path d="M12.2 7.8 8.1 12.7h3l-1 3.5 4.5-5.2h-3Z" fill="currentColor" stroke="none"/> <rect x="5" y="9" width="19" height="6" rx="1.2" fill="#3d7be6" opacity="0.35"/>
<path d="M16,7 L12,13 H15 L14,17 L19,11 H16 Z" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 356 B

After

Width:  |  Height:  |  Size: 432 B

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery empty">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 268 B

After

Width:  |  Height:  |  Size: 279 B

@@ -1,5 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 24" role="img" aria-label="Battery full">
<rect x="2" y="6" width="18" height="12" rx="2.8"/> <rect x="2" y="6" width="25" height="12" rx="3" fill="none" stroke="currentColor" stroke-width="1.5"/>
<path d="M22 10v4"/> <rect x="28" y="10" width="2" height="4" rx="1" fill="currentColor"/>
<rect x="5" y="9" width="12" height="6" rx="0.9" fill="currentColor" stroke="none"/> <rect x="5" y="9" width="19" height="6" rx="1.2" fill="#63c168"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 355 B

After

Width:  |  Height:  |  Size: 344 B

@@ -1,5 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/> <g transform="translate(-11 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g>
<circle cx="3.5" cy="12" r="1.1" fill="currentColor" stroke="none"/> <g transform="translate(-11 -4)"><circle cx="49" cy="34" r="4" fill="#3d7be6"/></g>
<circle cx="20.5" cy="12" r="1.1" fill="currentColor" stroke="none"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 383 B

After

Width:  |  Height:  |  Size: 382 B

@@ -1,4 +1,4 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth off">
<path d="M12 3l5.5 4.5-3.2 2.9M12 3v5M7.5 16.5l2.7-2.4M12 16v5l3.5-2.9"/> <g transform="translate(-6 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" opacity="0.5"/></g>
<path d="M4 4l16 16"/> <path d="M8,8 L52,52" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 292 B

After

Width:  |  Height:  |  Size: 413 B

@@ -1,3 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Bluetooth">
<path d="M7.5 7.5l10 9L12 21V3l5.5 4.5-10 9"/> <g transform="translate(-6 -4)"><path d="M30,10 L30,58 M30,10 L42,22 L30,34 L42,46 L30,58" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" stroke-linejoin="round"/></g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 240 B

After

Width:  |  Height:  |  Size: 297 B

@@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Control center">
<rect x="3" y="4" width="18" height="6" rx="3"/> <g transform="translate(0 -4)">
<rect x="3" y="14" width="18" height="6" rx="3"/> <rect x="10" y="14" width="40" height="16" rx="8" fill="none" stroke="currentColor" stroke-width="3"/>
<circle cx="7" cy="7" r="1.35" fill="currentColor" stroke="none"/> <circle cx="38" cy="22" r="6" fill="#3d7be6"/>
<circle cx="17" cy="17" r="1.35" fill="currentColor" stroke="none"/> <rect x="10" y="38" width="40" height="16" rx="8" fill="none" stroke="currentColor" stroke-width="3"/>
<circle cx="22" cy="46" r="6" fill="currentColor"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 434 B

After

Width:  |  Height:  |  Size: 449 B

@@ -1,4 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification off">
<path d="M10.3 4.8A5.5 5.5 0 0 1 17.5 10v3M6.7 8.6a5.5 5.5 0 0 0-.2 1.4v4.5L5 17h10M10 20a2.3 2.3 0 0 0 4 0"/> <g transform="translate(0 -2)">
<path d="M4 4l16 16"/> <path d="M30,10 A16,16 0 0,1 46,26 L46,38 L14,38 L14,26 A16,16 0 0,1 30,10 Z" fill="currentColor" opacity="0.5"/>
<rect x="10" y="38" width="40" height="5" rx="2" fill="currentColor" opacity="0.5"/>
<circle cx="30" cy="50" r="4" fill="currentColor" opacity="0.5"/>
<path d="M8,8 L52,56" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 329 B

After

Width:  |  Height:  |  Size: 509 B

@@ -1,4 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Notification">
<path d="M5 17h14l-1.5-2.5V10a5.5 5.5 0 0 0-11 0v4.5Z"/> <g transform="translate(0 -2)">
<path d="M10 20a2.3 2.3 0 0 0 4 0M12 3v1.5"/> <path d="M30,10 A16,16 0 0,1 46,26 L46,38 L14,38 L14,26 A16,16 0 0,1 30,10 Z" fill="currentColor"/>
<rect x="10" y="38" width="40" height="5" rx="2" fill="currentColor"/>
<circle cx="30" cy="50" r="4" fill="#3d7be6"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 298 B

After

Width:  |  Height:  |  Size: 360 B

@@ -1,5 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<path d="M3 9h4l5-4v14l-5-4H3Z"/> <g transform="translate(3 0)">
<path d="M15.2 8.5a5 5 0 0 1 0 7"/> <path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13"/> <path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<path d="M40,15 Q50,30 40,45" fill="none" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 308 B

After

Width:  |  Height:  |  Size: 431 B

@@ -1,5 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<path d="M3 9h4l5-4v14l-5-4H3Z"/> <g transform="translate(3 0)">
<path d="M15.2 8.5a5 5 0 0 1 0 7" opacity="0.24"/> <path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/> <path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<path d="M40,15 Q50,30 40,45" fill="none" opacity="0.18" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 338 B

After

Width:  |  Height:  |  Size: 446 B

@@ -1,5 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio">
<path d="M3 9h4l5-4v14l-5-4H3Z"/> <g transform="translate(3 0)">
<path d="M15.2 8.5a5 5 0 0 1 0 7"/> <path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor"/>
<path d="M18.4 5.5a9.2 9.2 0 0 1 0 13" opacity="0.24"/> <path d="M34,22 Q40,30 34,38" fill="none" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
<path d="M40,15 Q50,30 40,45" fill="none" opacity="0.35" stroke="#3d7be6" stroke-width="4" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 323 B

After

Width:  |  Height:  |  Size: 446 B

@@ -1,4 +1,6 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Audio off">
<path d="M3 9h4l5-4v14l-5-4H3Z"/> <g transform="translate(3 0)">
<path d="M16 9l5 6m0-6-5 6"/> <path d="M27.5,12.5 L15,22.5 L5,22.5 L5,37.5 L15,37.5 L27.5,47.5 Z" fill="currentColor" opacity="0.5"/>
<path d="M6,6 L54,54" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 259 B

After

Width:  |  Height:  |  Size: 340 B

@@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0"/> <g transform="translate(3 -1.5) scale(0.9)">
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/> <circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/> <path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/> <path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<path d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 394 B

After

Width:  |  Height:  |  Size: 510 B

@@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/> <g transform="translate(3 -1.5) scale(0.9)">
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0" opacity="0.24"/> <circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/> <path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/> <path opacity="0.25" d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<path opacity="0.25" d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 424 B

After

Width:  |  Height:  |  Size: 540 B

@@ -1,6 +1,8 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi">
<path d="M2.5 7.8a14.5 14.5 0 0 1 19 0" opacity="0.24"/> <g transform="translate(3 -1.5) scale(0.9)">
<path d="M5.7 11.4a9.6 9.6 0 0 1 12.6 0"/> <circle cx="30" cy="53" r="3" fill="#3d7be6"/>
<path d="M8.9 15a4.7 4.7 0 0 1 6.2 0"/> <path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/> <path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
<path opacity="0.25" d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 409 B

After

Width:  |  Height:  |  Size: 525 B

@@ -1,5 +1,9 @@
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round"> <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" role="img" aria-label="Wifi off">
<path d="M2.5 7.8a14.5 14.5 0 0 1 2.2-1.5M9 4.8a14.5 14.5 0 0 1 12.5 3M5.7 11.4a9.6 9.6 0 0 1 2.1-1.3M13.5 9.1a9.6 9.6 0 0 1 4.8 2.3M8.9 15a4.7 4.7 0 0 1 6.2 0"/> <g transform="translate(3 -1.5) scale(0.9)">
<circle cx="12" cy="18.4" r="1.2" fill="currentColor" stroke="none"/> <circle cx="30" cy="53" r="3" fill="currentColor" opacity="0.5"/>
<path d="M4 4l16 16"/> <path d="M21,44 Q30,34 39,44" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
<path d="M12,35 Q30,15 48,35" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
<path d="M3,26 Q30,5 57,26" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round" opacity="0.5"/>
<path d="M6,6 L54,54" fill="none" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</g>
</svg> </svg>

Before

Width:  |  Height:  |  Size: 453 B

After

Width:  |  Height:  |  Size: 673 B

+10 -2
View File
@@ -41,6 +41,7 @@ pub(crate) struct WorkspaceSnapshot {
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub(crate) struct WindowSnapshot { pub(crate) struct WindowSnapshot {
pub(crate) id: u64,
pub(crate) workspace: u64, pub(crate) workspace: u64,
pub(crate) app_id: String, pub(crate) app_id: String,
pub(crate) title: String, pub(crate) title: String,
@@ -120,6 +121,12 @@ impl ShellControl {
let _ = self.connection.flush(); let _ = self.connection.flush();
} }
pub(crate) fn activate_window(&self, id: u64) {
let (hi, lo) = split_id(id);
self.shell.activate_window(hi, lo);
let _ = self.connection.flush();
}
pub(crate) fn set_overview_active(&self, active: bool) { pub(crate) fn set_overview_active(&self, active: bool) {
if active { if active {
self.shell.enter_overview(); self.shell.enter_overview();
@@ -218,8 +225,8 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
}); });
} }
ferese_shell_v1::Event::Window { ferese_shell_v1::Event::Window {
window_hi: _, window_hi,
window_lo: _, window_lo,
workspace_hi, workspace_hi,
workspace_lo, workspace_lo,
app_id, app_id,
@@ -235,6 +242,7 @@ impl Dispatch<FereseShellV1, ()> for ControlState {
}; };
state.pending.windows.push(WindowSnapshot { state.pending.windows.push(WindowSnapshot {
id: join_id(window_hi, window_lo),
workspace: join_id(workspace_hi, workspace_lo), workspace: join_id(workspace_hi, workspace_lo),
app_id, app_id,
title, title,
+157 -34
View File
@@ -144,6 +144,7 @@ enum Message {
), ),
Tick, Tick,
ActivateWorkspace(u64), ActivateWorkspace(u64),
ActivateWindow(u64),
ToggleOverview, ToggleOverview,
StatusTick, StatusTick,
AnimateMenu, AnimateMenu,
@@ -383,6 +384,12 @@ impl cosmic::Application for FereseShell {
} }
Task::none() Task::none()
} }
Message::ActivateWindow(id) => {
if let Some(control) = &self.control {
control.activate_window(id);
}
Task::none()
}
Message::ActivateWorkspace(id) => { Message::ActivateWorkspace(id) => {
if let Some(control) = &self.control { if let Some(control) = &self.control {
control.activate_workspace(id); control.activate_workspace(id);
@@ -597,32 +604,41 @@ impl FereseShell {
workspace_row = workspace_row.push(workspace_button); workspace_row = workspace_row.push(workspace_button);
} }
let app_name = self
.snapshot
.windows
.iter()
.find(|window| {
window.focused
&& focused_output
.is_none_or(|output| window.workspace == output.active_workspace)
})
.map(display_app_name)
.unwrap_or_else(|| "Desktop".to_owned());
let foreground = color(shell_theme.text_primary); let foreground = color(shell_theme.text_primary);
let app_name = if self.overview_active { let mut windows = row::with_capacity(self.snapshot.windows.len())
"Overview".to_owned() .spacing(4)
.align_y(cosmic::iced::Alignment::Center);
for window in self.snapshot.windows.iter().filter(|window| {
focused_output.is_some_and(|output| window.workspace == output.active_workspace)
}) {
let label = compact_window_label(window);
let foreground = color(if window.focused {
shell_theme.text_primary
} else { } else {
app_name shell_theme.text_muted
}; });
let left = row![ windows = windows.push(
workspace_row, button::custom(
text(app_name) text(label)
.size(bar.text_size) .size(bar.text_size)
.font(cosmic::font::semibold())
.width(Length::Fill)
.wrapping(cosmic::iced::core::text::Wrapping::None) .wrapping(cosmic::iced::core::text::Wrapping::None)
.class(theme::Text::Color(foreground)) .class(theme::Text::Color(foreground)),
] )
.padding([0, 8])
.height(bar.control_height)
.class(status_ui::button_style(foreground, window.focused, 1.0))
.on_press(cosmic::Action::App(Message::ActivateWindow(window.id))),
);
}
let windows = cosmic::iced::widget::scrollable(windows)
.direction(cosmic::iced::widget::scrollable::Direction::Horizontal(
cosmic::iced::widget::scrollable::Scrollbar::default()
.width(0)
.scroller_width(0),
))
.width(Length::Fill)
.height(bar.control_height);
let left = row![workspace_row, windows]
.spacing(8) .spacing(8)
.align_y(cosmic::iced::Alignment::Center); .align_y(cosmic::iced::Alignment::Center);
let clock = container( let clock = container(
@@ -680,23 +696,49 @@ impl FereseShell {
} }
} }
fn symbolic_icon(source: &'static [u8], size: u16) -> icon::Icon { fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon {
icon::from_svg_bytes(source) accented_icon(source, size, foreground, foreground)
.symbolic(true)
.icon()
.size(size)
} }
fn bar_icon(source: &'static [u8], size: u16, foreground: Color) -> icon::Icon { fn accented_icon(source: &'static [u8], size: u16, foreground: Color, accent: Color) -> icon::Icon {
// COSMIC's transparent containers replace inherited foreground colors. let svg = std::str::from_utf8(source).expect("embedded SVG must be UTF-8");
// Pin the bar glyph color rather than relying on those container defaults. // Battery canvases are wider; reserve that space instead of stretching
symbolic_icon(source, size).class(theme::Svg::custom(move |_| { // or clipping them into the square slot used by the other status icons.
cosmic::iced::widget::svg::Style { let aspect = if svg.contains("viewBox=\"0 0 32 24\"") {
color: Some(foreground), 4.0 / 3.0
} } else {
1.0
};
icon::from_svg_bytes(tinted_svg(source, foreground, accent))
.symbolic(false)
.icon()
.size(size)
.width(Length::Fixed(f32::from(size) * aspect))
.content_fit(ContentFit::Contain)
.class(theme::Svg::custom(move |_| {
cosmic::iced::widget::svg::Style { color: None }
})) }))
} }
fn tinted_svg(source: &[u8], foreground: Color, accent: Color) -> Vec<u8> {
let rgba = |foreground: Color| {
format!(
"rgba({},{},{},{})",
(foreground.r * 255.0).round() as u8,
(foreground.g * 255.0).round() as u8,
(foreground.b * 255.0).round() as u8,
foreground.a,
)
};
std::str::from_utf8(source)
.expect("embedded icons must be UTF-8 SVG")
.replace("currentColor", &rgba(foreground))
// The source artwork's blue swatch is the semantic theme accent.
// Battery warning/success swatches remain status colors.
.replace("#3d7be6", &rgba(accent))
.into_bytes()
}
fn overview_control( fn overview_control(
bar: BarMetrics, bar: BarMetrics,
foreground: Color, foreground: Color,
@@ -806,6 +848,22 @@ fn current_time() -> String {
Zoned::now().strftime("%a · %H:%M").to_string() Zoned::now().strftime("%a · %H:%M").to_string()
} }
fn compact_window_label(window: &crate::control::WindowSnapshot) -> String {
let title = window.title.trim();
let label = if title.is_empty() {
display_app_name(window)
} else {
title.to_owned()
};
let mut characters = label.chars();
let prefix: String = characters.by_ref().take(24).collect();
if characters.next().is_some() {
format!("{prefix}…")
} else {
prefix
}
}
fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style { fn shell_surface_style(theme: ShellTheme) -> cosmic::iced::theme::Style {
cosmic::iced::theme::Style { cosmic::iced::theme::Style {
background_color: Color::TRANSPARENT, background_color: Color::TRANSPARENT,
@@ -967,6 +1025,70 @@ mod tests {
use super::*; use super::*;
use crate::control::{OutputSnapshot, WindowSnapshot}; use crate::control::{OutputSnapshot, WindowSnapshot};
#[test]
fn icons_use_theme_accent_and_preserve_battery_status_colors() {
for (source, accent) in [
(
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
"#63c168",
),
(
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
"#3d7be6",
),
(
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
"#e0654f",
),
(
include_bytes!("../assets/icons/status/wifi-full.svg").as_slice(),
"#3d7be6",
),
(
include_bytes!("../assets/icons/status/control-center.svg").as_slice(),
"#3d7be6",
),
] {
let svg = String::from_utf8(tinted_svg(
source,
Color::from_rgb8(205, 214, 244),
Color::from_rgb8(203, 166, 247),
))
.unwrap();
assert!(!svg.contains("currentColor"));
assert!(svg.contains("rgba(205,214,244,1)"));
assert!(!svg.contains("#3d7be6"));
assert!(svg.contains(if accent == "#3d7be6" {
"rgba(203,166,247,1)"
} else {
accent
}));
}
}
#[test]
fn window_list_labels_use_titles_and_fall_back_to_the_app_name() {
let snapshot = snapshot_with_fullscreen_window(7);
let mut window = snapshot.windows[0].clone();
window.title = " editor.rs — Project ".to_owned();
assert_eq!(compact_window_label(&window), "editor.rs — Project");
window.title = " ".to_owned();
assert_eq!(compact_window_label(&window), display_app_name(&window));
}
#[test]
fn window_list_labels_truncate_unicode_without_breaking_characters() {
let snapshot = snapshot_with_fullscreen_window(7);
let mut window = snapshot.windows[0].clone();
window.title = "界".repeat(25);
assert_eq!(
compact_window_label(&window),
format!("{}…", "界".repeat(24))
);
window.title = "界".repeat(24);
assert_eq!(compact_window_label(&window), window.title);
}
#[test] #[test]
fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() { fn startup_surface_clear_is_transparent_and_bar_fallback_is_dark() {
let theme = ShellTheme::default(); let theme = ShellTheme::default();
@@ -1048,6 +1170,7 @@ mod tests {
}], }],
workspaces: Vec::new(), workspaces: Vec::new(),
windows: vec![WindowSnapshot { windows: vec![WindowSnapshot {
id: 1,
workspace, workspace,
app_id: "dev.ferese.Test".to_owned(), app_id: "dev.ferese.Test".to_owned(),
title: "Test".to_owned(), title: "Test".to_owned(),
+137 -31
View File
@@ -152,31 +152,24 @@ impl FereseShell {
let metrics = BarMetrics::from(theme); let metrics = BarMetrics::from(theme);
let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center); let mut controls = row::with_capacity(6).spacing(3).align_y(Alignment::Center);
for kind in [ for kind in [
Menu::System,
Menu::Network, Menu::Network,
Menu::Bluetooth, Menu::Bluetooth,
Menu::Audio, Menu::Audio,
Menu::Battery,
Menu::Notifications, Menu::Notifications,
Menu::System, Menu::Battery,
] { ] {
if !kind.available(&self.status) { if !kind.available(&self.status) {
continue; continue;
} }
let (source, active) = status_icon(kind, &self.status); let (source, _) = status_icon(kind, &self.status);
let warning = kind == Menu::Battery let foreground = color(theme.text_primary);
&& self.status.battery.as_ref().is_some_and(|b| { let mut content = row![accented_icon(
b.percent < self.config.status.low_battery_threshold && b.status != "Charging" source,
}); metrics.icon_size,
let foreground = if warning { foreground,
Color::from_rgb8(245, 193, 105) color(theme.accent)
} else { )]
color(if active {
theme.text_primary
} else {
theme.text_muted
})
};
let mut content = row![bar_icon(source, metrics.icon_size, foreground)]
.spacing(4) .spacing(4)
.align_y(Alignment::Center); .align_y(Alignment::Center);
if kind == Menu::Battery && self.config.status.battery_percentage { if kind == Menu::Battery && self.config.status.battery_percentage {
@@ -210,11 +203,16 @@ impl FereseShell {
// A fixed button height does not center its child in libcosmic. // A fixed button height does not center its child in libcosmic.
// Keep the visual content centered inside the entire click target. // Keep the visual content centered inside the entire click target.
let content = container(content) let content = container(content)
.height(metrics.control_height) .align_x(alignment::Horizontal::Center)
.height(metrics.height)
.align_y(alignment::Vertical::Center); .align_y(alignment::Vertical::Center);
let control = button::custom(content) let control = button::custom(content)
.padding([0, 7]) .name(status_label(kind, &self.status))
.height(metrics.control_height) .padding([
0.0,
((metrics.height - f32::from(metrics.icon_size)) * 0.5).max(7.0),
])
.height(metrics.height)
.class(button_style(foreground, selected, 1.0)) .class(button_style(foreground, selected, 1.0))
.on_press_with_rectangle(move |offset, bounds| { .on_press_with_rectangle(move |offset, bounds| {
cosmic::Action::App(Message::OpenMenu( cosmic::Action::App(Message::OpenMenu(
@@ -365,6 +363,7 @@ impl FereseShell {
}, },
primary, primary,
muted, muted,
color_with_opacity(theme.accent, p),
p, p,
)); ));
if connected { if connected {
@@ -408,6 +407,7 @@ impl FereseShell {
}, },
primary, primary,
muted, muted,
color_with_opacity(theme.accent, p),
p, p,
)); ));
if b.enabled && !b.devices.is_empty() { if b.enabled && !b.devices.is_empty() {
@@ -439,7 +439,14 @@ impl FereseShell {
] ]
.align_y(Alignment::Center), .align_y(Alignment::Center),
text(&a.output).size(12).class(theme::Text::Color(muted)), text(&a.output).size(12).class(theme::Text::Color(muted)),
slider_row(audio_icon(a.volume, a.muted), a.volume, false, primary, p,) slider_row(
audio_icon(a.volume, a.muted),
a.volume,
false,
primary,
color_with_opacity(theme.accent, p),
p,
)
] ]
.spacing(4); .spacing(4);
rows = rows.push(control_card(audio.into(), primary, p)); rows = rows.push(control_card(audio.into(), primary, p));
@@ -454,6 +461,7 @@ impl FereseShell {
value, value,
true, true,
primary, primary,
color_with_opacity(theme.accent, p),
p, p,
) )
] ]
@@ -502,7 +510,12 @@ impl FereseShell {
container( container(
row![ row![
text(format!("{}%", b.percent)).size(40).width(Length::Fill), text(format!("{}%", b.percent)).size(40).width(Length::Fill),
symbolic_icon(status_icon(Menu::Battery, &self.status).0, 40), accented_icon(
status_icon(Menu::Battery, &self.status).0,
40,
battery_color,
color_with_opacity(theme.accent, p),
),
] ]
.align_y(Alignment::Center), .align_y(Alignment::Center),
) )
@@ -639,9 +652,10 @@ fn status_summary<'a>(
subtitle: &'a str, subtitle: &'a str,
primary: Color, primary: Color,
muted: Color, muted: Color,
accent: Color,
opacity: f32, opacity: f32,
) -> Element<'a, cosmic::Action<Message>> { ) -> Element<'a, cosmic::Action<Message>> {
let badge = container(symbolic_icon(source, 24)) let badge = container(accented_icon(source, 24, primary, accent))
.width(48) .width(48)
.height(48) .height(48)
.align_x(alignment::Horizontal::Center) .align_x(alignment::Horizontal::Center)
@@ -742,8 +756,9 @@ fn connection_control<'a>(
opacity: f32, opacity: f32,
) -> Element<'a, cosmic::Action<Message>> { ) -> Element<'a, cosmic::Action<Message>> {
let foreground = color_with_opacity(theme.text_primary, opacity); let foreground = color_with_opacity(theme.text_primary, opacity);
let icon_accent = color_with_opacity(theme.accent, opacity);
let accent = color_with_opacity(theme.accent, opacity * 0.14); let accent = color_with_opacity(theme.accent, opacity * 0.14);
let icon = container(symbolic_icon(source, 18)) let icon = container(accented_icon(source, 18, foreground, icon_accent))
.width(32) .width(32)
.height(32) .height(32)
.align_x(alignment::Horizontal::Center) .align_x(alignment::Horizontal::Center)
@@ -850,6 +865,7 @@ fn slider_row(
value: u8, value: u8,
brightness: bool, brightness: bool,
foreground: Color, foreground: Color,
icon_accent: Color,
opacity: f32, opacity: f32,
) -> Element<'static, cosmic::Action<Message>> { ) -> Element<'static, cosmic::Action<Message>> {
let style = std::rc::Rc::new(move |_: &cosmic::Theme| { let style = std::rc::Rc::new(move |_: &cosmic::Theme| {
@@ -900,7 +916,7 @@ fn slider_row(
dragging: style, dragging: style,
}); });
row![ row![
symbolic_icon(source, 18), accented_icon(source, 18, foreground, icon_accent),
control, control,
text(format!("{value}%")).size(12).width(36) text(format!("{value}%")).size(12).width(36)
] ]
@@ -909,6 +925,36 @@ fn slider_row(
.into() .into()
} }
fn status_label(kind: Menu, s: &Snapshot) -> String {
match kind {
Menu::Network => match &s.network {
Some(n) if n.enabled => format!(
"Wi-Fi: {}",
n.connection.as_deref().unwrap_or("not connected")
),
_ => "Wi-Fi: off".into(),
},
Menu::Bluetooth => match &s.bluetooth {
Some(b) if b.enabled => format!("Bluetooth: on, {} devices connected", b.devices.len()),
_ => "Bluetooth: off".into(),
},
Menu::Audio => match &s.audio {
Some(a) if !a.muted && a.volume > 0 => format!("Audio: {}%", a.volume),
_ => "Audio: muted".into(),
},
Menu::Notifications => match &s.notifications {
Some(n) if n.dnd => "Notifications: do not disturb".into(),
Some(n) => format!("Notifications: {} unread", n.count),
_ => "Notifications: unavailable".into(),
},
Menu::Battery => s.battery.as_ref().map_or_else(
|| "Battery: unavailable".into(),
|b| format!("Battery: {}%, {}", b.percent, b.status),
),
Menu::System => "Control Center".into(),
}
}
fn audio_icon(volume: u8, muted: bool) -> &'static [u8] { fn audio_icon(volume: u8, muted: bool) -> &'static [u8] {
if muted || volume == 0 { if muted || volume == 0 {
include_bytes!("../assets/icons/status/volume-mute.svg") include_bytes!("../assets/icons/status/volume-mute.svg")
@@ -940,7 +986,7 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
} }
} }
Menu::Bluetooth => match &s.bluetooth { Menu::Bluetooth => match &s.bluetooth {
Some(b) if !b.devices.is_empty() => ( Some(b) if b.enabled && !b.devices.is_empty() => (
include_bytes!("../assets/icons/status/bluetooth-connected.svg"), include_bytes!("../assets/icons/status/bluetooth-connected.svg"),
true, true,
), ),
@@ -965,13 +1011,13 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
}; };
let icon: &'static [u8] = if b.status == "Charging" { let icon: &'static [u8] = if b.status == "Charging" {
include_bytes!("../assets/icons/status/battery-charging.svg") include_bytes!("../assets/icons/status/battery-charging.svg")
} else if b.percent > 87 { } else if b.percent >= 80 {
include_bytes!("../assets/icons/status/battery-full.svg") include_bytes!("../assets/icons/status/battery-full.svg")
} else if b.percent > 62 { } else if b.percent >= 50 {
include_bytes!("../assets/icons/status/battery-75.svg") include_bytes!("../assets/icons/status/battery-75.svg")
} else if b.percent > 37 { } else if b.percent >= 20 {
include_bytes!("../assets/icons/status/battery-50.svg") include_bytes!("../assets/icons/status/battery-50.svg")
} else if b.percent > 12 { } else if b.percent > 0 {
include_bytes!("../assets/icons/status/battery-25.svg") include_bytes!("../assets/icons/status/battery-25.svg")
} else { } else {
include_bytes!("../assets/icons/status/battery-empty.svg") include_bytes!("../assets/icons/status/battery-empty.svg")
@@ -980,7 +1026,10 @@ fn status_icon(kind: Menu, s: &Snapshot) -> (&'static [u8], bool) {
} }
Menu::Notifications => { Menu::Notifications => {
if s.notifications.as_ref().is_some_and(|n| n.dnd) { if s.notifications.as_ref().is_some_and(|n| n.dnd) {
(include_bytes!("../assets/icons/status/dnd.svg"), false) (
include_bytes!("../assets/icons/status/notifications-off.svg"),
false,
)
} else { } else {
( (
include_bytes!("../assets/icons/status/notifications.svg"), include_bytes!("../assets/icons/status/notifications.svg"),
@@ -1042,5 +1091,62 @@ mod tests {
status_icon(Menu::Battery, &s).0, status_icon(Menu::Battery, &s).0,
include_bytes!("../assets/icons/status/battery-charging.svg") include_bytes!("../assets/icons/status/battery-charging.svg")
); );
s.notifications = Some(status::Notifications {
count: 3,
dnd: true,
});
assert_eq!(
status_icon(Menu::Notifications, &s).0,
include_bytes!("../assets/icons/status/notifications-off.svg")
);
assert_eq!(
status_label(Menu::Notifications, &s),
"Notifications: do not disturb"
);
s.bluetooth = Some(status::Bluetooth {
enabled: false,
devices: vec!["Headphones".into()],
});
assert_eq!(
status_icon(Menu::Bluetooth, &s).0,
include_bytes!("../assets/icons/status/bluetooth-off.svg")
);
}
#[test]
fn battery_bands_and_live_label_follow_charge_state() {
let mut s = Snapshot::default();
for (percent, expected) in [
(
0,
include_bytes!("../assets/icons/status/battery-empty.svg").as_slice(),
),
(
19,
include_bytes!("../assets/icons/status/battery-25.svg").as_slice(),
),
(
20,
include_bytes!("../assets/icons/status/battery-50.svg").as_slice(),
),
(
50,
include_bytes!("../assets/icons/status/battery-75.svg").as_slice(),
),
(
80,
include_bytes!("../assets/icons/status/battery-full.svg").as_slice(),
),
] {
s.battery = Some(status::Battery {
percent,
status: "Discharging".into(),
});
assert_eq!(status_icon(Menu::Battery, &s).0, expected);
assert_eq!(
status_label(Menu::Battery, &s),
format!("Battery: {percent}%, Discharging")
);
}
} }
} }