47 Commits
Author SHA1 Message Date
Inorishio 5a8939bc1e changed the repetaer for the networkpopout to be a listview, changed some variables in the bluetooth popout
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 21s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 1m1s
Rust / fmt (pull_request) Successful in 52s
C++ / build (pull_request) Successful in 2m46s
Rust / build (pull_request) Successful in 2m25s
Rust / clippy (pull_request) Successful in 1m58s
Python / verify (pull_request) Successful in 3m16s
C++ / clang-tidy (pull_request) Successful in 6m59s
2026-08-17 22:04:49 +02:00
Inorishio 6071223009 Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 19s
Python / static (pull_request) Successful in 57s
Rust / fmt (pull_request) Successful in 1m9s
Rust / build (pull_request) Successful in 2m11s
C++ / build (pull_request) Successful in 2m34s
Rust / clippy (pull_request) Successful in 1m30s
Python / verify (pull_request) Successful in 2m55s
C++ / clang-tidy (pull_request) Successful in 3m59s
2026-08-16 22:57:54 +02:00
zach 03ef51cd0c Merge pull request 'Feat: Peripheral status and integration' (#140) from feat/peripheral-integration into main
Reviewed-on: #140
Reviewed-by: AramJonghu <aramjonghu@tutamail.com>
2026-08-16 18:03:42 +02:00
zach 33c9432390 fix python errors
C++ / fmt (pull_request) Successful in 10s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 15s
Python / static (pull_request) Successful in 53s
Rust / fmt (pull_request) Successful in 1m6s
Rust / build (pull_request) Successful in 2m10s
C++ / build (pull_request) Successful in 2m37s
Rust / clippy (pull_request) Successful in 1m33s
Python / verify (pull_request) Successful in 2m53s
C++ / clang-tidy (pull_request) Successful in 3m59s
2026-08-16 13:11:17 +02:00
zach bfaba488f3 Merge branch 'main' into feat/peripheral-integration
C++ / fmt (pull_request) Successful in 7s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 15s
Python / static (pull_request) Successful in 59s
Rust / fmt (pull_request) Successful in 50s
Python / verify (pull_request) Failing after 2m37s
Rust / build (pull_request) Successful in 2m1s
C++ / build (pull_request) Successful in 4m7s
Rust / clippy (pull_request) Successful in 1m36s
C++ / clang-tidy (pull_request) Successful in 5m17s
2026-08-16 12:51:07 +02:00
zach c42feec312 clean up code in updates singleton and popout 2026-08-16 12:50:39 +02:00
zach ba567efc22 fix unused vars
C++ / fmt (pull_request) Successful in 7s
Python / static (pull_request) Failing after 2s
JS/TS / fmt (pull_request) Successful in 25s
JS/TS / lint (pull_request) Successful in 21s
Rust / fmt (pull_request) Successful in 55s
Rust / build (pull_request) Successful in 2m22s
Python / verify (pull_request) Failing after 2m41s
Rust / clippy (pull_request) Successful in 1m45s
C++ / build (pull_request) Successful in 3m38s
C++ / clang-tidy (pull_request) Successful in 4m46s
2026-08-16 11:23:57 +02:00
zach da21675f03 Merge branch 'main' into feat/peripheral-integration
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 18s
JS/TS / lint (pull_request) Successful in 20s
Python / static (pull_request) Failing after 59s
Rust / fmt (pull_request) Successful in 1m4s
Rust / build (pull_request) Successful in 1m56s
C++ / build (pull_request) Successful in 2m31s
Python / verify (pull_request) Failing after 2m26s
Rust / clippy (pull_request) Successful in 1m22s
C++ / clang-tidy (pull_request) Successful in 3m53s
2026-08-16 11:11:36 +02:00
AramJonghu 8883d98b00 chore: added missing dependency to pyproject
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 21s
Python / static (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 1m16s
C++ / build (pull_request) Successful in 2m26s
Rust / build (pull_request) Successful in 2m7s
Rust / clippy (pull_request) Successful in 1m4s
Python / verify (pull_request) Failing after 3m16s
C++ / clang-tidy (pull_request) Successful in 3m53s
2026-08-15 22:07:45 +02:00
AramJonghu cf907089da ci(python): rework of jobs + fix import issue 2026-08-15 22:07:45 +02:00
zach 070fe7add0 Merge branch 'main' into feat/peripheral-integration
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 20s
JS/TS / lint (pull_request) Successful in 18s
Python / lint (pull_request) Failing after 30s
Python / fmt (pull_request) Successful in 35s
Python / test (pull_request) Failing after 1m1s
Python / typecheck (pull_request) Failing after 1m24s
Rust / fmt (pull_request) Successful in 33s
Rust / build (pull_request) Successful in 1m51s
C++ / build (pull_request) Successful in 2m42s
Rust / clippy (pull_request) Successful in 1m1s
Python / buildcheck (pull_request) Successful in 2m34s
C++ / clang-tidy (pull_request) Successful in 4m2s
2026-08-15 20:54:59 +02:00
Inorishio 2d40ce33d1 Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 16s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 41s
Python / test (pull_request) Successful in 1m5s
Python / typecheck (pull_request) Successful in 1m15s
Rust / clippy (pull_request) Failing after 12m43s
Rust / fmt (pull_request) Failing after 12m57s
Rust / build (pull_request) Failing after 13m33s
Python / buildcheck (pull_request) Failing after 13m45s
C++ / clang-tidy (pull_request) Failing after 14m23s
C++ / build (pull_request) Failing after 14m25s
2026-08-15 20:38:54 +02:00
zach bf5a8b8049 simplify dedupe
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 16s
JS/TS / lint (pull_request) Successful in 19s
Python / fmt (pull_request) Successful in 34s
Python / lint (pull_request) Failing after 31s
Python / test (pull_request) Failing after 59s
Python / typecheck (pull_request) Failing after 1m7s
C++ / build (pull_request) Successful in 1m54s
Rust / fmt (pull_request) Successful in 49s
Rust / build (pull_request) Successful in 1m41s
Rust / clippy (pull_request) Successful in 1m33s
Python / buildcheck (pull_request) Successful in 2m38s
C++ / clang-tidy (pull_request) Successful in 3m31s
2026-07-13 22:10:41 +02:00
zach a9bbb21f54 improve device detection when plugging/unplugging
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 19s
Python / fmt (pull_request) Successful in 36s
Python / lint (pull_request) Failing after 34s
Python / test (pull_request) Failing after 1m2s
Python / typecheck (pull_request) Failing after 1m11s
C++ / build (pull_request) Successful in 2m11s
Rust / fmt (pull_request) Successful in 52s
Rust / build (pull_request) Successful in 1m45s
Rust / clippy (pull_request) Successful in 1m35s
Python / buildcheck (pull_request) Successful in 2m44s
C++ / clang-tidy (pull_request) Successful in 3m37s
2026-07-13 19:31:40 +02:00
zach bb50cf756d fix: cmake nuitka command again
C++ / fmt (pull_request) Successful in 9s
JS/TS / fmt (pull_request) Successful in 13s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 43s
Python / lint (pull_request) Failing after 32s
Python / test (pull_request) Failing after 57s
Python / typecheck (pull_request) Failing after 1m0s
C++ / build (pull_request) Successful in 3m5s
Rust / fmt (pull_request) Successful in 41s
Rust / build (pull_request) Successful in 1m39s
Python / buildcheck (pull_request) Successful in 2m36s
Rust / clippy (pull_request) Successful in 1m21s
C++ / clang-tidy (pull_request) Successful in 4m49s
2026-07-13 15:03:16 +02:00
zach 57147dc7c1 fix: cmake nuitka command
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 16s
Python / lint (pull_request) Failing after 32s
Python / fmt (pull_request) Successful in 36s
Python / test (pull_request) Failing after 1m3s
C++ / build (pull_request) Successful in 1m39s
Python / typecheck (pull_request) Failing after 1m9s
Rust / fmt (pull_request) Successful in 38s
Rust / build (pull_request) Successful in 1m33s
Python / buildcheck (pull_request) Successful in 2m48s
Rust / clippy (pull_request) Successful in 2m1s
C++ / clang-tidy (pull_request) Successful in 5m51s
2026-07-13 14:59:52 +02:00
zach 620fe001a4 backend now more robust, threaded writes use locks
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 21s
Python / lint (pull_request) Failing after 31s
Python / fmt (pull_request) Successful in 38s
Python / test (pull_request) Failing after 1m0s
Python / typecheck (pull_request) Failing after 1m1s
C++ / build (pull_request) Successful in 2m0s
Rust / fmt (pull_request) Successful in 44s
Rust / build (pull_request) Successful in 1m40s
Rust / clippy (pull_request) Successful in 1m34s
Python / buildcheck (pull_request) Successful in 2m35s
C++ / clang-tidy (pull_request) Successful in 3m44s
2026-07-13 14:56:57 +02:00
zach 85320e9ea1 prevent writing battery json when there has been no change, filter out devices with unknown state
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Successful in 29s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m4s
Python / typecheck (pull_request) Failing after 1m10s
C++ / build (pull_request) Successful in 1m35s
Rust / fmt (pull_request) Successful in 36s
Rust / build (pull_request) Successful in 1m34s
Rust / clippy (pull_request) Successful in 1m28s
Python / buildcheck (pull_request) Successful in 2m35s
C++ / clang-tidy (pull_request) Successful in 3m32s
2026-07-13 01:33:55 +02:00
zach 259a4dde7a gitignore
C++ / fmt (pull_request) Successful in 8s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 15s
Python / fmt (pull_request) Successful in 26s
Python / lint (pull_request) Successful in 31s
Python / test (pull_request) Successful in 54s
Python / typecheck (pull_request) Failing after 50s
C++ / build (pull_request) Successful in 3m19s
Rust / fmt (pull_request) Successful in 41s
Rust / build (pull_request) Successful in 1m43s
Python / buildcheck (pull_request) Successful in 2m35s
Rust / clippy (pull_request) Successful in 1m27s
C++ / clang-tidy (pull_request) Successful in 4m52s
2026-07-13 01:13:57 +02:00
zach d6230bef34 Fix cmake nuitka build command
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 12s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Successful in 31s
Python / lint (pull_request) Successful in 33s
Python / test (pull_request) Successful in 1m0s
Python / typecheck (pull_request) Failing after 1m5s
C++ / build (pull_request) Successful in 1m39s
Rust / fmt (pull_request) Successful in 50s
Rust / build (pull_request) Successful in 1m41s
Python / buildcheck (pull_request) Successful in 2m35s
Rust / clippy (pull_request) Successful in 2m5s
C++ / clang-tidy (pull_request) Successful in 6m13s
2026-07-13 01:12:55 +02:00
zach 4ee9bb3f12 add support for all UPower peripherals
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 17s
JS/TS / lint (pull_request) Successful in 16s
Python / fmt (pull_request) Successful in 34s
Python / lint (pull_request) Successful in 35s
Python / typecheck (pull_request) Failing after 1m5s
C++ / build (pull_request) Successful in 1m36s
Python / test (pull_request) Successful in 1m26s
Rust / fmt (pull_request) Successful in 48s
Rust / build (pull_request) Successful in 1m41s
Rust / clippy (pull_request) Successful in 1m37s
Python / buildcheck (pull_request) Successful in 2m41s
C++ / clang-tidy (pull_request) Successful in 3m32s
2026-07-13 00:48:05 +02:00
zach 8789d2d322 initial addition of battery display
JS/TS / fmt (pull_request) Successful in 7s
JS/TS / lint (pull_request) Successful in 9s
Python / fmt (pull_request) Successful in 15s
Python / lint (pull_request) Failing after 19s
Python / typecheck (pull_request) Failing after 1m5s
C++ / fmt (pull_request) Successful in 6s
Python / test (pull_request) Successful in 1m20s
Rust / fmt (pull_request) Successful in 1m42s
Python / buildcheck (pull_request) Successful in 2m23s
Rust / clippy (pull_request) Successful in 2m42s
Rust / build (pull_request) Successful in 2m53s
C++ / build (pull_request) Successful in 2m22s
C++ / clang-tidy (pull_request) Successful in 3m17s
2026-07-12 17:47:25 +02:00
zach 954dc10fbb bluetooth model delegate animations
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 21s
Python / fmt (pull_request) Successful in 35s
Python / lint (pull_request) Successful in 36s
Python / test (pull_request) Successful in 1m3s
Python / typecheck (pull_request) Failing after 1m4s
C++ / build (pull_request) Successful in 1m52s
Rust / build (pull_request) Successful in 1m41s
Rust / fmt (pull_request) Successful in 1m6s
Python / buildcheck (pull_request) Successful in 2m28s
Rust / clippy (pull_request) Successful in 1m55s
C++ / clang-tidy (pull_request) Successful in 3m36s
2026-07-11 14:00:08 +02:00
zach 15ccfd24c6 Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 16s
Python / fmt (pull_request) Successful in 32s
Python / lint (pull_request) Successful in 32s
Python / test (pull_request) Successful in 1m3s
Python / typecheck (pull_request) Failing after 1m3s
C++ / build (pull_request) Successful in 1m41s
Rust / build (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 1m17s
Python / buildcheck (pull_request) Successful in 2m43s
Rust / clippy (pull_request) Successful in 2m7s
C++ / clang-tidy (pull_request) Successful in 3m43s
2026-07-10 18:20:25 +02:00
zach 33b08523e9 add preliminary bluetooth widget + popout
C++ / fmt (pull_request) Successful in 3s
JS/TS / fmt (pull_request) Successful in 15s
JS/TS / lint (pull_request) Successful in 17s
Python / fmt (pull_request) Successful in 35s
Python / lint (pull_request) Successful in 34s
Python / test (pull_request) Successful in 1m0s
C++ / build (pull_request) Successful in 1m46s
Rust / fmt (pull_request) Successful in 1m19s
Rust / build (pull_request) Successful in 1m54s
Python / buildcheck (pull_request) Successful in 2m42s
Rust / clippy (pull_request) Successful in 1m54s
C++ / clang-tidy (pull_request) Successful in 3m42s
2026-07-10 16:17:47 +02:00
zach 742bece30b Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 7s
JS/TS / fmt (pull_request) Successful in 14s
JS/TS / lint (pull_request) Successful in 20s
Python / fmt (pull_request) Successful in 28s
Python / lint (pull_request) Successful in 29s
Python / test (pull_request) Successful in 59s
Rust / fmt (pull_request) Successful in 57s
C++ / build (pull_request) Successful in 3m47s
Rust / build (pull_request) Successful in 2m10s
Python / buildcheck (pull_request) Successful in 2m51s
Rust / clippy (pull_request) Successful in 1m32s
C++ / clang-tidy (pull_request) Successful in 5m9s
2026-07-10 15:23:23 +02:00
zach a64eadd14a feat: add password overlay for network popout + redesign
C++ / fmt (pull_request) Successful in 4s
JS/TS / fmt (pull_request) Successful in 11s
JS/TS / lint (pull_request) Successful in 24s
Python / fmt (pull_request) Successful in 30s
Python / lint (pull_request) Successful in 34s
Python / test (pull_request) Successful in 53s
C++ / build (pull_request) Successful in 1m52s
Rust / fmt (pull_request) Successful in 1m14s
Rust / build (pull_request) Successful in 1m57s
Rust / clippy (pull_request) Successful in 2m0s
Python / buildcheck (pull_request) Successful in 2m37s
C++ / clang-tidy (pull_request) Successful in 3m43s
2026-07-09 17:33:13 +02:00
zach f9eeb793db Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 18s
JS/TS / lint (pull_request) Successful in 19s
Python / fmt (pull_request) Successful in 32s
Python / lint (pull_request) Successful in 36s
Python / test (pull_request) Successful in 55s
C++ / build (pull_request) Successful in 1m55s
Rust / fmt (pull_request) Successful in 1m15s
Rust / build (pull_request) Successful in 1m59s
Python / buildcheck (pull_request) Successful in 2m45s
Rust / clippy (pull_request) Successful in 1m55s
C++ / clang-tidy (pull_request) Successful in 3m41s
2026-07-09 14:22:34 +02:00
zach 6bb193c78c Merge branch 'main' into module/wifi
C++ / fmt (pull_request) Successful in 5s
JS/TS / fmt (pull_request) Successful in 19s
JS/TS / lint (pull_request) Successful in 22s
Python / lint (pull_request) Successful in 33s
Python / fmt (pull_request) Successful in 36s
Python / test (pull_request) Successful in 1m5s
C++ / build (pull_request) Successful in 1m42s
Rust / fmt (pull_request) Successful in 1m11s
Rust / build (pull_request) Successful in 1m56s
Python / buildcheck (pull_request) Successful in 2m40s
Rust / clippy (pull_request) Successful in 1m48s
C++ / clang-tidy (pull_request) Successful in 3m42s
2026-07-08 22:56:46 +02:00
Inorishio aadecbfe44 Chore: created Spacer component, moved x available networks, added connected networks
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 17s
Python / test (pull_request) Successful in 59s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m31s
C++ / build (pull_request) Successful in 2m37s
2026-07-08 22:22:03 +02:00
zach 53f0aa80ae Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 42s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m36s
C++ / build (pull_request) Successful in 2m20s
2026-07-08 13:41:27 +02:00
Inorishio d9856c9409 Chore: created NetworkItem component, renamed network header to Wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m51s
C++ / build (pull_request) Successful in 2m36s
2026-07-02 22:46:09 +02:00
zach 8dc9753e2b feat: don't add the same network twice
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 30s
Python / lint-format (pull_request) Successful in 48s
Python / test (pull_request) Successful in 40s
C++ / build (pull_request) Successful in 2m8s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m13s
2026-07-02 22:09:25 +02:00
zach 2a4bd3dfdd chore: remove old code
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 17s
Python / lint-format (pull_request) Successful in 31s
Python / test (pull_request) Successful in 39s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m31s
C++ / build (pull_request) Successful in 2m17s
2026-07-02 21:58:01 +02:00
zach f75f51ef50 fix: sort is now correct
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 18s
Python / lint-format (pull_request) Successful in 21s
Python / test (pull_request) Successful in 48s
C++ / build (pull_request) Successful in 2m59s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m12s
2026-07-02 21:54:58 +02:00
zach e0873304d1 add sorting by strength
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 15s
Python / lint-format (pull_request) Successful in 32s
Python / test (pull_request) Successful in 39s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
C++ / build (pull_request) Successful in 2m47s
2026-07-02 21:53:42 +02:00
zach 99a0155c17 Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 14s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m30s
C++ / build (pull_request) Successful in 2m32s
2026-07-02 21:46:15 +02:00
zach f309debea5 set scannerEnabled when popout is visible + remove scan button
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 18s
Python / lint-format (pull_request) Successful in 20s
Python / test (pull_request) Successful in 47s
C++ / build (pull_request) Successful in 3m9s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m15s
2026-07-02 16:06:52 +02:00
zach 0979fd014d Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 16s
Python / lint-format (pull_request) Successful in 32s
Python / test (pull_request) Successful in 38s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m49s
C++ / build (pull_request) Successful in 3m5s
2026-07-02 15:16:22 +02:00
zach 656bd0a54d Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m37s
C++ / build (pull_request) Successful in 2m22s
2026-06-30 22:42:51 +02:00
zach f01001dcfa merge main into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 9s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 49s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m42s
C++ / build (pull_request) Successful in 2m28s
2026-06-30 18:15:43 +02:00
zach bc6b0d50ab Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 34s
Python / lint-format (pull_request) Successful in 35s
Python / test (pull_request) Successful in 1m11s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m58s
C++ / build (pull_request) Successful in 4m31s
2026-06-29 22:10:45 +02:00
zach f27c9afc25 more methods for wifi and scanning + layout
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 15s
Python / test (pull_request) Successful in 29s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m6s
2026-06-26 15:15:03 +02:00
Inorishio 5962fc8354 Merge branch 'main' into module/wifi
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 17s
Python / lint-format (pull_request) Successful in 27s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m39s
2026-06-24 18:58:36 +02:00
Inorishio 1adcb9841c Added network properties, nicNames, netDevices, networkName < reminder I should use layouts.
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 10s
Python / lint-format (pull_request) Successful in 16s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m52s
2026-06-22 23:34:58 +02:00
Inorishio 0e0f8555ed fixing singleton in helpers for network + getting output from systray 2026-06-15 20:48:01 +02:00
Inorishio 1d63e67554 removed network module and moved it to tray, have a popout window without content! everything still in wi.i.p. 2026-06-13 18:22:03 +02:00
33 changed files with 2409 additions and 268 deletions
+22 -87
View File
@@ -1,12 +1,15 @@
name: Python
env:
APT_DEPS: git python3 python3-pip python3-venv python3-gi python3-cairo python3-dbus python3-pyudev python3-psutil python3-evdev python3-yaml python3-xlib python3-pillow gir1.2-gtk-3.0
on:
pull_request:
jobs:
fmt:
runs-on: alpine
container: node:26-alpine
static:
runs-on: debian
container: node:26-trixie-slim
steps:
- name: Checkout
@@ -14,10 +17,8 @@ jobs:
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip
apt-get update
apt-get install -y --no-install-recommends git python3 python3-pip python3-venv
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir ruff
@@ -27,32 +28,15 @@ jobs:
. .venv/bin/activate
ruff format --check .
lint:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir ruff
- name: Lint
run: |
. .venv/bin/activate
ruff check .
test:
runs-on: alpine
container: node:26-alpine
verify:
if: always()
runs-on: debian
container: node:26-trixie-slim
steps:
- name: Checkout
@@ -60,20 +44,16 @@ jobs:
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip \
py3-pillow \
build-base
python3 -m venv .venv
apt-get update
apt-get install -y --no-install-recommends $APT_DEPS build-essential python3-dev
python3 -m venv --system-site-packages .venv
. .venv/bin/activate
pip install --no-cache-dir \
typer \
pillow \
materialyoucolor \
jinja2 \
pytest
pip install --no-cache-dir basedpyright nuitka .
- name: Type check
run: |
. .venv/bin/activate
basedpyright
- name: Test
run: |
@@ -81,52 +61,7 @@ jobs:
cd cli
python -m pytest tests/ -v
typecheck:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir basedpyright typer pillow materialyoucolor jinja2
- name: Type check
run: |
. .venv/bin/activate
basedpyright
buildcheck:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git \
python3 \
py3-pip \
build-base \
python3-dev \
gcc \
g++
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir nuitka
- name: Nuitka module check
run: |
. .venv/bin/activate
nuitka --module --include-package=zshell cli/src/zshell/
nuitka --module --include-package=zshell cli/src/zshell/
+2
View File
@@ -16,3 +16,5 @@ dist/
**/test-plugins/
**/Charts/
network-dev/
**/zshell.build/
**/zshell.dist/
+2
View File
@@ -93,6 +93,8 @@ if("shell" IN_LIST ENABLE_MODULES)
${NUITKA_EXECUTABLE}
--standalone
--include-data-dir=${CMAKE_SOURCE_DIR}/cli/src/zshell/assets=zshell/assets
--include-package=gi.overrides
--include-package-data=solaar
--output-dir=${ZSHELL_CLI_BUILD_DIR}
--output-filename=zshell-cli
${CMAKE_SOURCE_DIR}/cli/src/zshell/
+1 -1
View File
@@ -15,7 +15,7 @@ CustomRect {
property color activeOnColor
property bool checked
property real checkedRadius: Tokens.rounding.medium
property real defaultRadius: Tokens.rounding.large
property real defaultRadius: Math.min(width, height) / 2
property color disabledColor: Qt.alpha(Colors.palette.m3onSurface, 0.1)
property color disabledOnColor: Qt.alpha(Colors.palette.m3onSurface, 0.38)
property bool fillWidth
+2
View File
@@ -34,6 +34,7 @@ BusyIndicator {
anchors.fill: parent
bgColor: root.bgColor
fgColor: root.fgColor
hasEndIndicator: false
padding: root.padding
rotation: manager.rotation
startAngle: manager.startFraction * 360
@@ -53,6 +54,7 @@ BusyIndicator {
Anim {
duration: manager.completeEndDuration * Tokens.anim.durations.scale
properties: "opacity,internalStrokeWidth"
type: Anim.DefaultEffects
}
}
+2
View File
@@ -23,6 +23,7 @@ TextFieldBase {
property string leadingIcon
readonly property int leadingOffset: leadingIcon ? leadingIconLoader.width + leadingIconLoader.anchors.leftMargin : 0
property int radius: Tokens.rounding.small
property alias sLayer: stateLayer
readonly property real smallFontScale: smallFontSize / font.pointSize
property int smallFontSize: Tokens.font.size.small
property string supportingText
@@ -145,6 +146,7 @@ TextFieldBase {
sourceComponent: MaterialIcon {
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.larger
text: root.leadingIcon
}
}
+1 -1
View File
@@ -220,7 +220,7 @@ Item {
}
}
if (Config.dock.enable && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y))
if (Config.dock.enabled && Config.dock.hoverToReveal && !root.visibilities.dock && !root.visibilities.launcher && root.inBottomPanel(root.panels.dock, x, y))
root.visibilities.dock = true;
if (root.geometry.barContains(x, y))
+12 -1
View File
@@ -11,6 +11,7 @@ import ZShell.Blobs
import qs.Daemons
import qs.Components
import qs.Modules.Bar
import qs.Modules.Bar.Popouts.Network as N
import ZShell.Config
import qs.Helpers
import qs.Drawers
@@ -138,7 +139,17 @@ CustomWindow {
HyprlandFocusGrab {
id: focusGrab
active: visibilities.dock || visibilities.resources || visibilities.launcher || visibilities.sidebar || visibilities.dashboard || visibilities.settings || visibilities.clipboard || (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!Config.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
active: {
const v = visibilities;
const conf = Config;
if (!N.PopupManager.closed && panels.popouts.hasCurrent)
return true;
if ((v.launcher && conf.launcher.enabled) || (v.sidebar && conf.sidebar.enabled) || (v.resources && conf.dashboard.performance.enabled) || (v.dashboard && conf.dashboard.enabled) || v.settings || (v.clipboard && conf.clipboard.enabled) || (v.dock && conf.dock.enabled))
return true;
if (panels.popouts.hasCurrent && panels.popouts.currentName.startsWith("traymenu") && (!conf.bar.tray.showOnHover || (panels.popouts.current as StackView)?.depth > 1))
return true;
return false;
}
windows: [root]
onCleared: {
+40 -12
View File
@@ -1,35 +1,63 @@
pragma Singleton
import Quickshell
import Quickshell.Io
import Quickshell.Services.UPower
import ZShell.Config
import qs.Services
import qs.Paths
Singleton {
id: root
readonly property var colors: {
if (deviceState === UPowerDeviceState.Charging || deviceState === UPowerDeviceState.FullyCharged)
readonly property list<var> allPeripherals: [...logiDevices, ...upowerDevices.filter(d => !Battery.logiDevices.some(l => l.nativePath === d.nativePath))]
readonly property real currentPerc: UPower.displayDevice.percentage
readonly property var deviceState: UPower.displayDevice.state
readonly property string deviceStateString: UPowerDeviceState.toString(deviceState)
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
readonly property alias logiDevices: adapter.devices
readonly property var lowestPeripheral: allPeripherals.reduce((lowest, current) => current.percentage < lowest.percentage ? current : lowest)
readonly property bool onBattery: UPower.onBattery
readonly property bool ready: UPower.displayDevice.ready
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
readonly property real timeToFull: UPower.displayDevice.timeToFull
readonly property list<UPowerDevice> upowerDevices: UPower.devices.values.filter(d => d.state !== UPowerDeviceState.Unknown)
function getColors(percentage: real, state: string): var {
if (state === "charging" || state === "full" || state === "recharging")
return {
fg: Colors.swapRG(Colors.palette.m3error),
bg: Colors.swapRG(Colors.palette.m3onError)
};
else if (currentPerc <= 0.2)
else if (percentage <= 0.2)
return {
fg: Colors.palette.m3error,
bg: Colors.palette.m3onError
};
else
return {
fg: Colors.palette.m3onSurface,
bg: Colors.palette.m3surface
fg: Colors.palette.m3tertiary,
bg: Colors.palette.m3onTertiary
};
}
readonly property real currentPerc: UPower.displayDevice.percentage
readonly property var deviceState: UPower.displayDevice.state
readonly property bool isLaptop: UPower.displayDevice.isLaptopBattery
readonly property bool onBattery: UPower.onBattery
readonly property bool ready: UPower.displayDevice.ready
readonly property real timeToEmpty: UPower.displayDevice.timeToEmpty
readonly property real timeToFull: UPower.displayDevice.timeToFull
function startDaemon(): void {
Quickshell.execDetached(["zshell-cli", "battery", "daemon"]);
}
FileView {
id: fileView
path: `${Paths.cache}/battery.json`
watchChanges: true
onFileChanged: reload()
JsonAdapter {
id: adapter
property list<var> devices: []
property real updated: 0.0
}
}
}
-10
View File
@@ -77,16 +77,6 @@ Singleton {
"99": "thunderstorm"
})
function getAppCategoryIcon(name: string, fallback: string): string {
const categories = DesktopEntries.heuristicLookup(name)?.categories;
if (categories)
for (const [key, value] of Object.entries(categoryIcons))
if (categories.includes(key))
return value;
return fallback;
}
function getAppIcon(name: string, fallback: string): string {
const icon = DesktopEntries.heuristicLookup(name)?.icon;
if (fallback !== "undefined")
+114
View File
@@ -0,0 +1,114 @@
pragma Singleton
import Quickshell
import Quickshell.Networking
import QtQuick
Singleton {
id: root
property bool active: false
readonly property list<Network> connectedNetworks: networks.filter(n => n.connected)
property Network connectingNetwork
readonly property list<NetworkDevice> devices: Networking.devices.values
property Network failedNetwork
readonly property list<Network> knownNetworks: networks.filter(n => n.known && !n.connected)
readonly property list<Network> networks: {
const list = [];
for (const d of wifiDevices) {
for (const n of d.networks.values) {
if (!list.includes(n))
list.push(n);
}
}
return list;
}
property bool scanning: false
readonly property list<Network> unknownNetworks: networks.filter(n => !n.known)
readonly property list<WifiDevice> wifiDevices: devices.filter(d => wifiDevice(d))
readonly property bool wifiEnabled: Networking.wifiEnabled
signal networkConnected
function isSecure(security): bool {
return (security === WifiSecurityType.WpaPsk || security === WifiSecurityType.Wpa2Psk || security === WifiSecurityType.Sae);
}
function requestConnect(network: Network, secret = undefined): void {
connectingNetwork = network;
if (secret === undefined || secret === "") {
network.connect();
} else {
(network as WifiNetwork).connectWithPsk(secret);
}
}
function setScan(value: bool): void {
for (const d of wifiDevices) {
d.scannerEnabled = value;
}
}
function setWifi(value: bool): void {
Networking.wifiEnabled = value;
}
function wifiDevice(dev): bool {
return dev.type === DeviceType.Wifi;
}
onActiveChanged: {
for (const d of wifiDevices)
d.scannerEnabled = active;
}
Timer {
id: flushFailed
interval: 5000
running: root.failedNetwork
onTriggered: {
root.failedNetwork = null;
}
}
Timer {
id: checkConnection
interval: 500
repeat: true
running: root.connectingNetwork
onTriggered: {
var completedNetwork;
switch (root.connectingNetwork.state) {
case ConnectionState.Connecting:
break;
case ConnectionState.Connected:
completedNetwork = root.connectingNetwork;
root.networkConnected();
break;
case ConnectionState.Disconnected:
root.failedNetwork = root.connectingNetwork;
completedNetwork = root.connectingNetwork;
break;
}
if (completedNetwork) {
root.connectingNetwork = null;
}
}
}
Timer {
interval: 10000
running: checkConnection.running
onTriggered: {
root.failedNetwork = root.connectingNetwork;
root.connectingNetwork = null;
}
}
}
+8 -41
View File
@@ -11,8 +11,8 @@ Singleton {
id: root
property int availableUpdates: 0
property string cmd: ""
property bool commandReady
property bool hasHelper
property bool loaded
property double now: Date.now()
property var updates: ({})
@@ -38,18 +38,12 @@ Singleton {
}
function performPackageUpdate(pkg: string): void {
if (root.cmd === "pacman")
pkgUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Sy", pkg];
else
pkgUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Sy", pkg];
pkgUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Sy", pkg];
pkgUpdateProc.running = true;
}
function performSystemUpdate(): void {
if (root.cmd === "pacman")
sysUpdateProc.command = ["pkexec", root.cmd, "--noconfirm", "-Syu"];
else
sysUpdateProc.command = [root.cmd, "--noconfirm", "--sudo", "pkexec", "-Syu"];
sysUpdateProc.command = ["pkexec", "pacman", "--noconfirm", "-Syu"];
sysUpdateProc.running = true;
}
@@ -87,7 +81,7 @@ Singleton {
Process {
id: cmdDetect
command: ["sh", "-c", "command -v checkupdates || command -v yay || command -v paru"]
command: ["sh", "-c", "command -v checkupdates"]
running: true
stdout: StdioCollector {
@@ -96,47 +90,20 @@ Singleton {
let helper;
if (cmd.length > 0) {
helper = cmd.split("/").pop();
helper = true;
} else {
helper = "pacman";
}
if (helper === "checkupdates") {
updatesProc.command = [helper];
} else {
updatesProc.command = [helper, "-Qu"];
helper = false;
}
root.hasHelper = helper;
root.commandReady = true;
}
}
}
Process {
id: updateCmdDetect
command: ["sh", "-c", "command -v yay || command -v paru"]
running: true
stdout: StdioCollector {
onStreamFinished: {
const cmd = this.text.trim();
let helper;
if (cmd.length > 0) {
helper = cmd.split("/").pop();
} else {
helper = "pacman";
}
root.cmd = helper;
}
}
}
Process {
id: updatesProc
command: []
command: root.hasHelper ? ["checkupdates"] : []
running: false
stdout: StdioCollector {
-9
View File
@@ -203,15 +203,6 @@ GridLayout {
}
}
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
NetworkWidget {
}
}
}
DelegateChoice {
roleValue: "media"
@@ -0,0 +1,85 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers
Row {
id: root
property real batHeight: Tokens.padding.smaller * 2
property real batWidth: Tokens.padding.larger * 2
required property string devState
property real nubHeight: Tokens.padding.small
property real nubWidth: 2
required property real percentage
property real radius: Tokens.rounding.smallest / 2
spacing: 1
CustomRect {
id: track
anchors.verticalCenter: parent.verticalCenter
color: Battery.getColors(root.percentage, root.devState).bg
height: root.batHeight
radius: root.radius
width: root.batWidth
CustomText {
color: Battery.getColors(root.percentage, root.devState).fg
font.pointSize: Tokens.font.size.larger / 1.5
font.weight: 800
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100)
verticalAlignment: Text.AlignVCenter
width: track.width
}
Item {
clip: true
width: parent.width * root.percentage
anchors {
bottom: parent.bottom
left: parent.left
top: parent.top
}
CustomRect {
id: fill
color: Battery.getColors(root.percentage, root.devState).fg
height: track.height
radius: track.radius
width: track.width
CustomText {
id: batteryLabel
clip: true
color: Battery.getColors(root.percentage, root.devState).bg
font.pointSize: Tokens.font.size.larger / 1.5
font.weight: 800
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(root.percentage * 100)
verticalAlignment: Text.AlignVCenter
width: track.width
}
}
}
}
CustomRect {
id: nub
anchors.verticalCenter: parent.verticalCenter
bottomRightRadius: 20
color: root.percentage < 0.99 ? track.color : fill.color
height: root.nubHeight
topRightRadius: 20
width: root.nubWidth
}
}
+22 -1
View File
@@ -105,6 +105,28 @@ WidgetBase {
}
}
DelegateChoice {
roleValue: "bluetooth"
delegate: EntryWrapper {
name: "bluetooth"
BluetoothWidget {
}
}
}
DelegateChoice {
roleValue: "network"
delegate: EntryWrapper {
name: "network"
NetworkWidget {
}
}
}
DelegateChoice {
roleValue: "power"
@@ -112,7 +134,6 @@ WidgetBase {
name: "upower"
UPowerWidget {
horizontal: root.horizontal
}
}
}
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "bluetooth"
}
@@ -0,0 +1,14 @@
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Services
import qs.Components
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
animate: true
color: Colors.palette.m3onSurface // Network.connected ? root.textColor : Colors.palette.m3error
fill: 1
font.pointSize: Tokens.font.size.larger
text: "android_wifi_4_bar"
}
@@ -2,16 +2,17 @@ import Quickshell.Services.UPower
import QtQuick
import QtQuick.Layouts
import ZShell.Config
import qs.Modules.Bar.Components.Common
import qs.Components
import qs.Helpers
Item {
id: root
required property bool horizontal
readonly property Item currentItem: batteryIconLoader?.item ?? peripheralIconLoader?.item ?? upowerIconLoader.item
implicitHeight: Battery.isLaptop ? batteryIconLoader.item.implicitHeight : upowerIconLoader.item.implicitHeight
implicitWidth: Battery.isLaptop ? batteryIconLoader.item.implicitWidth : upowerIconLoader.item.implicitWidth
implicitHeight: currentItem?.implicitHeight ?? 0
implicitWidth: currentItem?.implicitWidth ?? 0
Behavior on Layout.preferredHeight {
Anim {
@@ -30,95 +31,36 @@ Item {
}
}
Component.onCompleted: Battery.startDaemon()
Loader {
id: batteryIconLoader
active: Battery.isLaptop
anchors.centerIn: parent
sourceComponent: Row {
id: batteryIcon
sourceComponent: BatteryIcon {
devState: Battery.deviceStateString
percentage: Battery.currentPerc
}
}
property real batHeight: 16
property real batWidth: 30
property real nubHeight: 6
property real nubWidth: 2
property real radius: Tokens.rounding.smallest / 2
Loader {
id: peripheralIconLoader
spacing: 1
active: (Battery.lowestPeripheral?.isPresent ?? false) && !batteryIconLoader.active
anchors.centerIn: parent
CustomRect {
id: track
anchors.verticalCenter: parent.verticalCenter
color: Battery.colors.bg
height: batteryIcon.batHeight
radius: batteryIcon.radius
width: batteryIcon.batWidth
CustomText {
color: Battery.colors.fg
font.pointSize: Tokens.font.size.larger / 1.5
font.weight: 800
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(Battery.currentPerc * 100)
verticalAlignment: Text.AlignVCenter
width: track.width
}
Item {
clip: true
width: parent.width * Battery.currentPerc
anchors {
bottom: parent.bottom
left: parent.left
top: parent.top
}
CustomRect {
id: fill
color: Battery.colors.fg
height: track.height
radius: track.radius
width: track.width
CustomText {
id: batteryLabel
clip: true
color: Battery.colors.bg
font.pointSize: 7.5
font.weight: 800
height: track.height
horizontalAlignment: Text.AlignHCenter
text: Math.round(Battery.currentPerc * 100)
verticalAlignment: Text.AlignVCenter
width: track.width
}
}
}
}
CustomRect {
id: nub
anchors.verticalCenter: parent.verticalCenter
bottomRightRadius: 20
color: Battery.currentPerc < 0.99 ? track.color : fill.color
height: batteryIcon.nubHeight
topRightRadius: 20
width: batteryIcon.nubWidth
}
sourceComponent: BatteryIcon {
devState: Battery.lowestPeripheral?.state ?? ""
percentage: Battery.lowestPeripheral?.percentage ?? 0
}
}
Loader {
id: upowerIconLoader
active: !Battery.isLaptop
active: !batteryIconLoader.active && !peripheralIconLoader.active
anchors.centerIn: parent
sourceComponent: MaterialIcon {
+385
View File
@@ -0,0 +1,385 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQml.Models
import Quickshell
import Quickshell.Bluetooth
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers
CustomClippingRect {
id: root
readonly property list<var> btConnected: Bluetooth.devices.values.filter(d => d.connected)
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: 500
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.larger
spacing: Tokens.spacing.extraSmall / 2
CustomText {
Layout.rightMargin: Tokens.padding.extraSmall
Layout.topMargin: Tokens.padding.normal
font.pointSize: Tokens.font.size.large
text: qsTr("Bluetooth")
}
Toggle {
checked: Bluetooth.defaultAdapter?.enabled ?? false // qmllint disable unresolved-type
first: true
subtext: qsTr("Toggle bluetooth device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.enabled = checked;
}
}
Toggle {
checked: Bluetooth.defaultAdapter?.discovering ?? false // qmllint disable unresolved-type
last: root.btConnected.length === 0
subtext: qsTr("Enable scanning for new devices")
text: qsTr("Discover")
onToggled: {
const adapter = Bluetooth.defaultAdapter; // qmllint disable unresolved-type
if (adapter)
adapter.discovering = checked;
}
}
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: StatusDivider {
}
}
DelegateChoice {
delegate: BluetoothItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
objectProp: "address"
values: {
const byName = (a, b) => a.name.localeCompare(b.name);
return [...[...Bluetooth.devices.values].filter(d => d.connected).sort(byName).slice(0, 5),
{
isDivider: true
},
...[...Bluetooth.devices.values].filter(d => !d.connected).sort(byName).slice(0, 5)];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
}
component BluetoothItem: ConnectedRect {
id: btItem
readonly property int connectedCount: root.btConnected.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
required property int index
required property ListView list
readonly property bool loading: modelData.state === BluetoothDeviceState.Connecting || modelData.state === BluetoothDeviceState.Disconnecting // qmllint disable unresolved-type
required property BluetoothDevice modelData
property int verticalPadding: Tokens.padding.smaller
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
radius: Tokens.rounding.small
Timer {
interval: 50
running: btItem.ListView.delayRemove
onTriggered: btItem.ListView.delayRemove = false
}
Connections {
function onConnectedChanged(): void {
if (!btItem.modelData.connected)
btItem.ListView.delayRemove = true;
}
function onPairedChanged(): void {
if (btItem.modelData.paired)
btItem.modelData.connect();
}
target: btItem.modelData
}
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.leftMargin: btItem.horizontalPadding
anchors.rightMargin: btItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
elide: Text.ElideRight
text: btItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: btItem.modelData.connected ? (btItem.modelData.batteryAvailable ? qsTr("%1 battery left").arg(Math.round(btItem.modelData.battery * 100) + "%") : qsTr("Battery status unknown")) : btItem.modelData.address
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
font.pointSize: Tokens.font.size.large
opacity: btItem.connecting ? 0 : 1
text: btItem.modelData.batteryAvailable ? Icons.getBatteryIcon(btItem.modelData.battery) : "battery_unknown"
visible: btItem.modelData.state === BluetoothDeviceState.Connected // qmllint disable unresolved-type
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: btItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
ButtonRow {
spacing: Tokens.spacing.small
IconTextButton {
font.pointSize: Tokens.font.size.small
icon: btItem.modelData.connected ? "link_off" : btItem.modelData.paired ? "link" : "add_link"
text: btItem.modelData.connected ? qsTr("Disconnect") : btItem.modelData.paired ? qsTr("Connect") : qsTr("Pair")
onClicked: {
if (btItem.modelData.connected)
btItem.modelData.disconnect();
else if (btItem.modelData.paired)
btItem.modelData.connect();
else if (!btItem.modelData.paired)
btItem.modelData.pair();
else if (btItem.modelData.pairing)
btItem.modelData.cancelPair();
}
}
}
}
}
component ConnectedRect: CustomRect {
id: bg
property bool first
property bool last
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
Behavior on bottomLeftRadius {
Anim {
}
}
Behavior on bottomRightRadius {
Anim {
}
}
Behavior on topLeftRadius {
Anim {
}
}
Behavior on topRightRadius {
Anim {
}
}
}
component StatusDivider: CustomText {
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
const devices = Bluetooth.devices.values; // qmllint disable unresolved-type
const connectedCount = devices.filter(d => d.connected).length;
let available = qsTr("%1 device%2 available").arg(devices.length - connectedCount).arg(devices.length === 1 ? "" : "s");
return available;
}
topPadding: Tokens.spacing.small
}
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Tokens.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Tokens.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}
+7
View File
@@ -80,6 +80,13 @@ Item {
}
}
Popout {
name: "bluetooth"
sourceComponent: BluetoothPopout {
}
}
Popout {
name: "updates"
@@ -0,0 +1,71 @@
import QtQuick
import ZShell.Config
import qs.Services
import qs.Helpers
import qs.Components
CustomRect {
id: root
readonly property bool closing: PopupManager.closed
readonly property url currentPopup: PopupManager.currentPopup
property bool shouldBeVisible: loader.status === Loader.Ready
color: Qt.alpha(Colors.palette.m3shadow, 0.3)
opacity: shouldBeVisible && !closing ? 1 : 0
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
onVisibleChanged: {
if (!visible) {
PopupManager.requestClose();
PopupManager.currentPopup = "";
}
}
CustomMouseArea {
anchors.fill: parent
hoverEnabled: true
preventStealing: true
propagateComposedEvents: false
onClicked: {
const insideItemWidth = mouseX < loader.x + loader.item.width && mouseX > loader.x;
const insideItemHeight = mouseY < loader.y + loader.item.height && mouseY > loader.y;
if (insideItemHeight && insideItemWidth)
return;
PopupManager.requestClose();
}
}
Loader {
id: loader
anchors.centerIn: parent
Connections {
function onCurrentPopupChanged(): void {
if (PopupManager.currentPopup) {
loader.setSource(PopupManager.currentPopup, PopupManager.currentPopupProps);
} else {
loader.source = null;
}
}
target: PopupManager
}
}
Connections {
function onNetworkConnected(): void {
PopupManager.requestClose();
}
target: Network
}
}
+103
View File
@@ -0,0 +1,103 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import ZShell.Components
import ZShell.Config
import qs.Services
import qs.Components
import qs.Helpers
CustomClippingRect {
id: root
required property var network
color: Colors.palette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
implicitWidth: layout.implicitWidth + layout.anchors.margins * 2
radius: Tokens.rounding.large
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Tokens.padding.extraLarge
spacing: Tokens.spacing.normal
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
font.pointSize: Tokens.font.size.extraLarge * 2
text: "lock"
}
CustomText {
Layout.alignment: Qt.AlignHCenter
color: Colors.palette.m3onSurfaceVariant
text: qsTr("Connect to %1").arg(root.network.name)
}
CustomTextField {
id: passwordField
Layout.fillWidth: true
Layout.preferredWidth: 400
echoMode: CustomTextField.Password
errorText: qsTr("Failed to connect with given password")
isError: Network.failedNetwork
leadingIcon: "lock"
passwordMaskDelay: 5000
placeholderText: qsTr("Input password")
}
ButtonRow {
id: buttonRow
Layout.fillWidth: true
spacing: Tokens.spacing.smaller
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: "close"
inactiveColor: Colors.layer(Colors.palette.m3surfaceContainerHighest, 2)
inactiveOnColor: Colors.palette.m3onSurfaceVariant
isRound: true
isToggle: false
shapeMorph: true
text: "Cancel"
onClicked: PopupManager.requestClose()
}
IconTextButton {
fillWidth: true
font.pointSize: Tokens.font.size.normal
icon: Network.connectingNetwork ? "" : "check"
inactiveColor: Network.connectingNetwork ? Colors.palette.m3primaryContainer : Network.failedNetwork ? Colors.palette.m3error : Colors.palette.m3primary
inactiveOnColor: Network.connectingNetwork ? Colors.palette.m3onPrimaryContainer : Network.failedNetwork ? Colors.palette.m3onError : Colors.palette.m3onPrimary
isRound: true
isToggle: false
shapeMorph: true
text: Network.connectingNetwork ? "" : "Apply"
onClicked: Network.requestConnect(root.network, passwordField.text)
Loader {
active: opacity > 0
anchors.centerIn: parent
opacity: Network.connectingNetwork ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
implicitSize: buttonRow.height - Tokens.padding.normal
running: true
}
}
}
}
}
}
@@ -0,0 +1,22 @@
pragma Singleton
import Quickshell
import QtQuick
Singleton {
id: root
property bool closed: true
property url currentPopup: ""
property var currentPopupProps: ({})
function requestClose(): void {
closed = true;
}
function requestOpen(source: url, properties = {}): void {
currentPopupProps = properties;
currentPopup = source;
closed = false;
}
}
+342 -16
View File
@@ -1,36 +1,362 @@
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Networking
import QtQuick
import QtQuick.Layouts
import qs.Components
import Quickshell
import ZShell.Components
import ZShell.Config
import qs.Modules
import qs.Helpers as Helpers
import qs.Services
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
Item {
CustomClippingRect {
id: root
required property var wrapper
anchors.horizontalCenter: parent.horizontalCenter
implicitHeight: networkPopContent.height + networkPopContent.anchors.margins * 2
implicitWidth: 500 + 8 * 2
radius: (20 - Tokens.padding.small) * Tokens.rounding.scale
Component.onCompleted: Network.active = true
Component.onDestruction: Network.active = false
ColumnLayout {
id: layout
id: networkPopContent
spacing: 8
anchors.left: parent.left
anchors.margins: Tokens.padding.large
anchors.right: parent.right
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
Repeater {
model: Helpers.Network.devices
CustomText {
Layout.preferredHeight: visible ? implicitHeight : 0
Layout.rightMargin: Tokens.padding.extraSmall
font.pointSize: Tokens.font.size.large
text: qsTr("Wifi")
}
CustomRadioButton {
id: network
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: Network.connectedNetworks.length === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
required property NetworkDevice modelData
onToggled: Network.setWifi(checked)
}
checked: Helpers.Network.activeDevice?.name === modelData.name
text: modelData.description
visible: modelData.name !== "lo"
ListView {
id: rows
Layout.fillWidth: true
Layout.preferredHeight: contentHeight
interactive: false
section.property: "connected"
spacing: Tokens.spacing.extraSmall / 2
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
}
delegate: DelegateChooser {
role: "isDivider"
DelegateChoice {
roleValue: true
delegate: StatusDivider {
}
}
DelegateChoice {
delegate: NetworkItem {
list: rows
width: rows.width
}
}
}
displaced: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
model: ScriptModel {
values: {
const byStrengthName = (a, b) => b.signalStrength - a.signalStrength || a.name.localeCompare(b.name);
const cNetworks = [...Network.connectedNetworks];
const kNetworks = [...Network.knownNetworks];
const uNetworks = [...Network.unknownNetworks];
return [...cNetworks.sort(byStrengthName), ...(kNetworks.length > 0 ? [
{
isDivider: true,
type: "known"
},
...kNetworks.sort(byStrengthName)] : []), ...(uNetworks.length > 0 ? [
{
isDivider: true,
type: "unknown"
},
...uNetworks.sort(byStrengthName)] : [])];
}
}
move: Transition {
Anim {
easing.type: Easing.OutCubic
property: "y"
}
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
}
section.delegate: Rectangle {
required property string section
color: "transparent"
height: childrenRect.height
width: ListView.view.width
CustomText {
text: parent.section
}
}
}
}
NetworkOverlay {
anchors.fill: parent
}
component ConnectedRect: CustomRect {
id: bg
property bool first
property bool last
bottomLeftRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
bottomRightRadius: last ? Tokens.rounding.large : Tokens.rounding.extraSmall
color: Colors.tPalette.m3surfaceContainer
topLeftRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
topRightRadius: first ? Tokens.rounding.large : Tokens.rounding.extraSmall
}
component NetworkItem: ConnectedRect {
id: networkItem
readonly property int connectedCount: Network.connectedNetworks.length
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
readonly property bool inConnectedGroup: index < connectedCount
// sectioning first / last item in list reminder
// if index === network.connectedNetworks.length - 1
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length - 2
// if index === network.connectedNetworks.length - 1 + network.knownNetworks.length
// list.model.values.length - 1
required property int index
readonly property bool isSecure: Network.isSecure(modelData.security)
required property ListView list
required property var modelData
property int verticalPadding: Tokens.padding.smaller
Layout.fillWidth: true
first: !inConnectedGroup && index === connectedCount + 1
implicitHeight: rowLayout.implicitHeight + verticalPadding * 2
last: inConnectedGroup ? index === connectedCount - 1 : index === list.count - 1
radius: Tokens.rounding.small
RowLayout {
id: rowLayout
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
anchors.fill: parent
anchors.leftMargin: networkItem.horizontalPadding
anchors.rightMargin: networkItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
elide: Text.ElideRight
text: networkItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: networkItem.modelData.connected ? qsTr("Connected: %1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%") : qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: networkItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
}
StateLayer {
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: {
if (networkItem.isSecure && !networkItem.known) {
PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), {
network: networkItem.modelData
});
} else {
Network.requestConnect(networkItem.modelData);
}
}
}
}
component StatusDivider: CustomText {
required property var modelData
bottomPadding: Tokens.spacing.extraSmall
color: Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.smaller
text: {
if (modelData.type === "known") {
const count = Network.knownNetworks.filter(n => !n.connected).length;
return qsTr("%1 known network%2").arg(count).arg(count === 1 ? "" : "s");
}
if (modelData.type === "unknown") {
const count = Network.unknownNetworks.filter(n => !n.connected).length;
return qsTr("%1 network%2 available").arg(count).arg(count === 1 ? "" : "s");
}
return qsTr("object unknown");
}
topPadding: Tokens.spacing.small
}
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Tokens.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Tokens.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}
+28 -1
View File
@@ -1,8 +1,10 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.UPower
import ZShell.Config
import qs.Modules.Bar.Components.Common
import qs.Helpers
import qs.Components
import qs.Services
@@ -22,6 +24,31 @@ Column {
}
}
Repeater {
model: Battery.allPeripherals
RowLayout {
id: layout
required property var modelData
anchors.left: parent.left
anchors.leftMargin: Tokens.padding.normal
anchors.right: parent.right
anchors.rightMargin: Tokens.padding.normal
CustomText {
Layout.fillWidth: true
text: layout.modelData.model
}
BatteryIcon {
devState: layout.modelData.state
percentage: layout.modelData.percentage
}
}
}
CustomText {
function formatSeconds(s: int, fallback: string): string {
const day = Math.floor(s / 86400);
@@ -75,7 +102,7 @@ Column {
CustomText {
anchors.verticalCenter: parent.verticalCenter
color: Colors.palette.m3onError
font.family: Appearance.font.family.mono
font.family: Config.appearance.font.family.mono
text: qsTr("Performance Degraded")
}
-2
View File
@@ -78,8 +78,6 @@ CustomClippingRect {
required property var modelData
readonly property list<string> sections: modelData.update.split(" ")
// anchors.left: parent.left
// anchors.right: parent.right
color: Colors.tPalette.m3surfaceContainer
implicitHeight: root.itemHeight
implicitWidth: 600
+1 -1
View File
@@ -12,7 +12,7 @@ Item {
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels
required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enable
readonly property bool shouldBeActive: visibilities.dock && Config.dock.enabled
required property PersistentProperties visibilities
anchors.bottomMargin: (-implicitHeight - 5) * offsetScale
+6 -2
View File
@@ -2,8 +2,8 @@ pragma Singleton
import QtQuick
import QtQuick.Layouts
import qs.Components
import ZShell.Config
import qs.Components
import qs.Modules.Settings.Common
import qs.Modules.Settings.Pages
import qs.Modules.Settings.Pages.Wallpaper
@@ -45,7 +45,11 @@ QtObject {
// Connectivity
Component {
PlaceholderComp {
StackPage {
Component {
NetworkPage {
}
}
}
},
Component {
+240
View File
@@ -0,0 +1,240 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import ZShell.Config
import qs.Modules.Settings.Common
import qs.Modules.Bar.Popouts.Network
import qs.Components
import qs.Helpers
import qs.Services
PageBase {
id: root
title: qsTr("Network")
ColumnLayout {
id: networkContent
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
spacing: Tokens.spacing.extraSmall / 2
width: root.cappedWidth
Toggle {
Layout.preferredHeight: visible ? implicitHeight : 0
checked: Network.wifiEnabled
first: true
last: connected.count === 0
subtext: qsTr("Toggle WiFi device state")
text: checked ? qsTr("Enabled") : qsTr("Disabled")
onToggled: Network.setWifi(checked)
}
Repeater {
id: connected
visible: Network.connectedNetworks.length > 0
model: ScriptModel {
values: [...Network.connectedNetworks].sort((a, b) => {
return b.signalStrength - a.signalStrength;
})
}
NetworkItem {
first: false
known: true
repeater: connected
}
}
}
component NetworkItem: ConnectedRect {
id: networkItem
readonly property bool connecting: Network.connectingNetwork === modelData
property int horizontalPadding: Tokens.padding.largeIncreased
required property int index
readonly property bool isSecure: Network.isSecure(modelData.security)
required property bool known
required property var modelData
required property Repeater repeater
Layout.fillWidth: true
Layout.preferredHeight: visible ? rowLayout.implicitHeight + Tokens.padding.smaller * 2 : 0
first: index === 0
last: index === (repeater.count - 1)
radius: Tokens.rounding.small
RowLayout {
id: rowLayout
anchors.fill: parent
anchors.leftMargin: networkItem.horizontalPadding
anchors.rightMargin: networkItem.horizontalPadding
Column {
id: column
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
Layout.leftMargin: Tokens.spacing.extraSmall
Layout.rightMargin: Tokens.spacing.extraSmall
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurface
elide: Text.ElideRight
text: networkItem.modelData.name
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font.pointSize: Tokens.font.size.small
text: qsTr("%1 strength").arg(Math.round(networkItem.modelData.signalStrength * 100) + "%")
}
}
Item {
Layout.alignment: Qt.AlignRight
implicitHeight: icon.height
implicitWidth: icon.width
MaterialIcon {
id: icon
color: networkItem.modelData.active ? Colors.palette.m3primary : Colors.palette.m3onSurfaceVariant
font.pointSize: Tokens.font.size.large
opacity: networkItem.connecting ? 0 : 1
text: Icons.getNetworkIcon(networkItem.modelData.signalStrength * 100, networkItem.isSecure)
Behavior on opacity {
Anim {
}
}
}
Loader {
active: opacity > 0
anchors.fill: parent
opacity: networkItem.connecting ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: CircularIndicator {
bgColor: "transparent"
running: true
}
}
}
}
StateLayer {
color: networkItem.modelData.active ? Colors.palette.m3onPrimary : Colors.palette.m3onSurface
onClicked: {
if (networkItem.isSecure && !networkItem.known) {
PopupManager.requestOpen(Qt.resolvedUrl("Network/PSKOverlay.qml"), {
network: networkItem.modelData
});
} else {
Network.requestConnect(networkItem.modelData);
}
}
}
}
component Spacer: Item {
id: spacer
required property Repeater repeater
Layout.preferredHeight: repeater.count > 0 ? Tokens.spacing.extraSmall : 0
}
component Toggle: CustomSwitch {
id: toggleItem
readonly property alias bg: bg
property alias first: bg.first
property alias last: bg.last
property string subtext
Layout.fillWidth: true
cLayer: 2
horizontalPadding: Tokens.padding.largeIncreased
implicitHeight: Math.max(implicitContentHeight, implicitIndicatorHeight) + verticalPadding * 2
implicitWidth: implicitContentWidth + implicitIndicatorWidth + horizontalPadding * 2
indicator.anchors.right: right
indicator.anchors.rightMargin: toggleItem.horizontalPadding
indicator.anchors.verticalCenter: verticalCenter
verticalPadding: Tokens.padding.normal
background: ConnectedRect {
id: bg
StateLayer {
id: stateLayer
manualPressOverride: toggleItem.pressed
}
}
contentItem: Item {
anchors.left: parent.left
anchors.leftMargin: toggleItem.horizontalPadding
anchors.right: toggleItem.indicator.left
anchors.rightMargin: Tokens.spacing.normal
implicitHeight: column.implicitHeight
implicitWidth: column.implicitWidth
Column {
id: column
anchors.left: parent.left
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: 0
CustomText {
id: label
anchors.left: parent.left
anchors.right: parent.right
elide: Text.ElideRight
font: {
const f = Qt.font(label.font);
f.pointSize = Tokens.font.size.smaller;
return f;
}
text: toggleItem.text
}
CustomText {
id: subtext
anchors.left: parent.left
anchors.right: parent.right
color: Colors.palette.m3outline
elide: Text.ElideRight
font: {
const f = Qt.font(subtext.font);
f.pointSize = Tokens.font.size.small;
return f;
}
text: toggleItem.subtext
visible: toggleItem.subtext
}
}
}
onPressed: stateLayer.press(stateLayer.mouseX, stateLayer.mouseY)
}
}
@@ -8,7 +8,9 @@ PageBase {
readonly property var builtinIcons: ({
audio: qsTr("Speakers"),
microphone: qsTr("Microphone"),
power: qsTr("Battery")
power: qsTr("Battery"),
network: qsTr("Network"),
bluetooth: qsTr("Bluetooth")
})
isSubPage: true
@@ -91,5 +93,15 @@ PageBase {
onToggled: Config.bar.popouts.statusIcons = checked
}
ToggleRow {
checked: Config.bar.popouts.network
last: true
settingAnchor: "bar-status-network-popout"
subtext: qsTr("Show a details popout when hovering the network icon")
text: qsTr("Network popout on hover")
onToggled: Config.bar.popouts.network = checked
}
}
}
+9 -1
View File
@@ -8,7 +8,14 @@ import typer
from typer._completion_classes import completion_init
from typer._completion_shared import _get_shell_name, install
from zshell.subcommands import record, scheme, screenshot, shell, wallpaper
from zshell.subcommands import (
battery,
record,
scheme,
screenshot,
shell,
wallpaper,
)
app = typer.Typer(name="zshell-cli", add_completion=False)
@@ -17,6 +24,7 @@ app.add_typer(scheme.app, name="scheme")
app.add_typer(screenshot.app, name="screenshot")
app.add_typer(wallpaper.app, name="wallpaper")
app.add_typer(record.app, name="record")
app.add_typer(battery.app, name="battery")
def _completion_installed() -> bool:
+819
View File
@@ -0,0 +1,819 @@
from __future__ import annotations
import contextlib
import fcntl
import json
import logging
import os
import re
import signal
import struct
import tempfile
import threading
import time
from collections.abc import Callable
from dataclasses import asdict, dataclass
from pathlib import Path
import typer
from logitech_receiver import hidpp10, hidpp20
from logitech_receiver.common import Battery, BatteryStatus, Notification
from logitech_receiver.hidpp10_constants import Registers
from logitech_receiver.hidpp20_constants import SupportedFeature
app = typer.Typer(
help="Read live battery status from Logitech HID++ peripherals."
)
logger = logging.getLogger("zshell.battery")
DEFAULT_OUTPUT = Path.home() / ".cache" / "zshell" / "battery.json"
_RUNTIME_DIR = Path(
os.environ.get("XDG_RUNTIME_DIR") or (Path.home() / ".cache" / "zshell")
)
DEFAULT_LOCK = _RUNTIME_DIR / "zshell-battery-daemon.lock"
def _acquire_singleton_lock(path: Path) -> int | None:
path.parent.mkdir(parents=True, exist_ok=True)
fd = os.open(path, os.O_CREAT | os.O_RDWR, 0o644)
try:
fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
except BlockingIOError:
os.close(fd)
return None
os.ftruncate(fd, 0)
os.write(fd, str(os.getpid()).encode())
return fd
@dataclass
class DeviceBattery:
model: str
nativePath: str | None
serial: str | None
type: str | None
wired: bool
percentage: float | None
state: str | None
charging: bool
isPresent: bool
_SYSFS_POWER_SUPPLY = Path("/sys/class/power_supply")
def _normalize_serial(s: str) -> str:
return re.sub(r"[^0-9A-Za-z]", "", s).upper()
def _find_upower_native_path(serial: str | None) -> str | None:
if not serial or not _SYSFS_POWER_SUPPLY.is_dir():
return None
target = _normalize_serial(serial)
for entry in _SYSFS_POWER_SUPPLY.glob("hidpp_battery_*"):
try:
raw = (entry / "serial_number").read_text().strip()
except OSError:
logger.debug(
"couldn't read serial_number under %s", entry, exc_info=True
)
continue
if _normalize_serial(raw) == target:
return entry.name
return None
def _process_battery_notification(dev, n) -> bool:
if not getattr(dev, "isDevice", False):
return False
if int(n.sub_id) & 0x80:
return False
if n.sub_id == Notification.NO_OPERATION:
return False
if getattr(dev, "protocol", None) is not None and dev.protocol < 2.0:
if n.sub_id in (Registers.BATTERY_STATUS, Registers.BATTERY_CHARGE):
if n.data[-1:] != b"\x00":
return False
data = bytes([n.address]) + n.data
dev.set_battery_info(hidpp10.parse_battery_status(n.sub_id, data))
return True
return False
features = getattr(dev, "features", None)
if not features:
return False
try:
feature = features.get_feature(n.sub_id)
except Exception:
return False
if feature == SupportedFeature.BATTERY_STATUS:
if n.address == 0x00:
dev.set_battery_info(hidpp20.decipher_battery_status(n.data)[1])
return True
return False
if feature == SupportedFeature.BATTERY_VOLTAGE:
if n.address == 0x00:
dev.set_battery_info(hidpp20.decipher_battery_voltage(n.data)[1])
return True
return False
if feature == SupportedFeature.UNIFIED_BATTERY:
if n.address == 0x00:
dev.set_battery_info(hidpp20.decipher_battery_unified(n.data)[1])
return True
return False
if feature == SupportedFeature.ADC_MEASUREMENT:
if n.address == 0x00:
result = hidpp20.decipher_adc_measurement(n.data)
if result:
dev.set_battery_info(result[1])
return True
return False
if feature == SupportedFeature.CENTURION_BATTERY_SOC:
dev.set_battery_info(hidpp20.decipher_battery_centurion(n.data)[1])
return True
if feature == SupportedFeature.SOLAR_DASHBOARD:
if n.data[5:9] == b"GOOD":
charge, lux, _ = struct.unpack("!BHH", n.data[:5])
status = BatteryStatus.DISCHARGING
if n.address == 0x10 and lux > 200:
status = BatteryStatus.RECHARGING
dev.set_battery_info(Battery(charge, None, status, None, lux))
return True
return False
return False
def _import_listener_deps():
try:
from logitech_receiver import base, device, receiver
from logitech_receiver.listener import EventsListener
except ImportError as e:
raise RuntimeError(
"logitech_receiver isn't importable -- install it with `pip install solaar`."
) from e
except ValueError as e:
raise RuntimeError(
"logitech_receiver failed to import because a GTK3 typelib is missing "
f"({e}). Install the same GTK3 + PyGObject packages Solaar's GUI needs "
"(e.g. python3-gi + gir1.2-gtk-3.0 on Debian/Ubuntu, or the gtk3/"
"python3-gobject equivalents on your distro)."
) from e
class _BatteryEventsListener(EventsListener):
def __init__(self, receiver_or_device, on_change):
super().__init__(receiver_or_device, self._handle_notification)
self._on_change = on_change
def has_started(self):
if not self.receiver.isDevice:
try:
self.receiver.notification_flags = (
self.receiver.enable_connection_notifications()
)
self.receiver.notify_devices()
except Exception:
logger.exception(
"failed enabling notifications for %s", self.receiver
)
def _handle_notification(self, n):
if self.receiver.isDevice:
try:
_process_battery_notification(self.receiver, n)
except Exception:
logger.exception("processing %s for %s", n, self.receiver)
self._on_change(self.receiver)
return
if n.devnumber == 0xFF:
try:
_process_battery_notification(self.receiver, n)
except Exception:
logger.exception("processing receiver notification %s", n)
self._on_change(self.receiver)
return
if not (0 < n.devnumber <= 16):
logger.warning(
"unexpected device number %s in %s", n.devnumber, n
)
return
try:
dev = self.receiver[n.devnumber]
except Exception:
logger.exception(
"resolving device number %s on %s",
n.devnumber,
self.receiver,
)
return
if not dev:
logger.warning(
"%s: received %s for invalid device %d",
self.receiver,
n,
n.devnumber,
)
return
try:
_process_battery_notification(dev, n)
except Exception:
logger.exception("processing %s for %s", n, dev)
self._on_change(dev)
return base, device, receiver, _BatteryEventsListener
def _start_hotplug_watcher(on_hotplug: Callable[[], None]):
try:
import pyudev
except ImportError:
logger.warning(
"pyudev isn't importable -- hotplugged devices will only be "
"noticed on the next --rescan-interval tick, not instantly. "
"Install it with `pip install pyudev` (it's a core Solaar "
"dependency, so it's normally already present)."
)
return None
debounce_lock = threading.Lock()
debounce_state: dict[str, threading.Timer | None] = {"timer": None}
DEBOUNCE_SECONDS = 0.5
def _fire():
with debounce_lock:
debounce_state["timer"] = None
on_hotplug()
def _handle_event(_device):
if _device.action not in ("add", "remove"):
return
with debounce_lock:
existing = debounce_state["timer"]
if existing is not None:
existing.cancel()
timer = threading.Timer(DEBOUNCE_SECONDS, _fire)
timer.daemon = True
debounce_state["timer"] = timer
timer.start()
context = pyudev.Context()
monitor = pyudev.Monitor.from_netlink(context)
monitor.filter_by(subsystem="hidraw")
observer = pyudev.MonitorObserver(monitor, callback=_handle_event)
observer.start()
return observer
class _ListenerRegistry:
def __init__(self, on_change):
self._on_change = on_change
self._entries = {}
self._last_change: dict[int, float] = {}
self._lock = threading.Lock()
self._rescan_lock = threading.Lock()
self._min_rescan_interval = 1.0
self._last_rescan_time = 0.0
self._hidpp_io_lock = threading.RLock()
def _touch_and_notify(self, dev_or_receiver, *args, **kwargs):
with self._lock:
self._last_change[id(dev_or_receiver)] = time.time()
self._on_change(dev_or_receiver, *args, **kwargs)
def last_change(self, dev) -> float:
with self._lock:
return self._last_change.get(id(dev), 0.0)
def rescan(self):
now = time.monotonic()
if now - self._last_rescan_time < self._min_rescan_interval:
return
if not self._rescan_lock.acquire(blocking=False):
return
try:
self._last_rescan_time = now
with self._hidpp_io_lock:
base, device, receiver, ListenerClass = _import_listener_deps()
with self._lock:
dead = [
p
for p, (_obj, listener) in self._entries.items()
if not listener._active
]
for p in dead:
dying_obj, _dying_listener = self._entries[p]
if dying_obj.isDevice:
ident = getattr(
dying_obj, "unitId", None
) or getattr(dying_obj, "serial", None)
info = getattr(dying_obj, "battery_info", None)
if ident and info is not None:
for (
other_path,
(other_obj, _other_listener),
) in self._entries.items():
if other_path == p or other_obj.isDevice:
continue
for child in other_obj:
child_ident = getattr(
child, "unitId", None
) or getattr(child, "serial", None)
if child_ident == ident:
child.set_battery_info(info)
break
del self._entries[p]
known_paths = set(self._entries.keys())
with self._lock:
id_to_entry = {}
for path, (obj, listener) in self._entries.items():
if obj.isDevice:
ident = obj.unitId or obj.serial
if ident:
id_to_entry[ident] = (path, obj, listener)
for dev_info in base.receivers_and_devices():
if dev_info.path in known_paths:
continue
try:
if dev_info.isDevice:
obj = device.create_device(base, dev_info)
else:
obj = receiver.create_receiver(base, dev_info)
except OSError as e:
if e.errno == 13:
logger.error(
"permission denied opening %s -- check the Solaar udev rule "
"(rules.d/42-logitech-unify-permissions.rules) is installed",
dev_info.path,
)
else:
logger.exception("failed opening %s", dev_info)
continue
except Exception:
logger.exception("failed opening %s", dev_info)
continue
if obj is None:
continue
listener = ListenerClass(obj, self._touch_and_notify)
to_join = None
if obj.isDevice:
ident = None
try:
if obj.protocol >= 2.0:
obj.get_ids()
ident = obj.unitId or obj.serial
except Exception:
pass
if ident:
with self._lock:
existing = id_to_entry.get(ident)
if existing:
old_path, old_obj, old_listener = existing
if old_path != dev_info.path:
logger.info(
"replacing device %s (old path %s) with new path %s",
ident,
old_path,
dev_info.path,
)
if (
getattr(obj, "battery_info", None)
is None
and getattr(
old_obj, "battery_info", None
)
is not None
):
obj.set_battery_info(
old_obj.battery_info
)
old_listener.stop()
del self._entries[old_path]
self._entries[dev_info.path] = (
obj,
listener,
)
self._last_change[id(obj)] = time.time()
to_join = old_listener
break
else:
pass
else:
if (
getattr(obj, "battery_info", None)
is None
):
for (
_,
(other_obj, _other_listener),
) in self._entries.items():
if other_obj.isDevice:
continue
for child in other_obj:
child_ident = getattr(
child, "unitId", None
) or getattr(
child, "serial", None
)
if child_ident == ident:
obj.set_battery_info(
child.battery_info
)
break
if (
getattr(
obj,
"battery_info",
None,
)
is not None
):
break
with self._lock:
if dev_info.path not in self._entries:
self._entries[dev_info.path] = (obj, listener)
self._last_change[id(obj)] = time.time()
listener.start()
logger.info(
"listening on %s (%s)",
dev_info.path,
"device" if dev_info.isDevice else "receiver",
)
if to_join is not None:
try:
to_join.join(timeout=1.0)
except Exception:
logger.exception("error joining replaced listener")
del to_join
finally:
self._rescan_lock.release()
def known_devices(self):
with self._lock:
entries = list(self._entries.values())
result = []
seen_idents = set()
for obj, _listener in entries:
if obj.isDevice:
ident = obj.unitId or obj.serial
if ident and ident in seen_idents:
continue
if ident:
seen_idents.add(ident)
result.append(obj)
else:
for child in obj:
ident = child.unitId or child.serial
if ident and ident in seen_idents:
continue
if ident:
seen_idents.add(ident)
result.append(child)
return result
def stop(self):
with self._lock:
entries = list(self._entries.values())
for _obj, listener in entries:
try:
listener.stop()
except Exception:
logger.exception("error stopping listener")
for _obj, listener in entries:
listener.join(timeout=2.0)
def _snapshot(registry: _ListenerRegistry) -> list[DeviceBattery]:
results: list[DeviceBattery] = []
with registry._hidpp_io_lock:
for dev in registry.known_devices():
info = dev.battery_info
if info is None:
try:
if dev.ping():
info = dev.battery()
except Exception:
logger.debug(
"initial battery() failed for %s", dev, exc_info=True
)
info = None
entry = _to_device_battery(dev, info)
if entry is not None:
results.append(entry)
return results
def _clean_json(path: Path) -> None:
_write_json_atomic(path, [])
def _iter_open_devices():
try:
from logitech_receiver import base, device, receiver
except ImportError as e:
raise RuntimeError(
"logitech_receiver isn't importable -- install it with `pip install solaar`."
) from e
except ValueError as e:
raise RuntimeError(
"logitech_receiver failed to import because a GTK3 typelib is missing "
f"({e}). Install the same GTK3 + PyGObject packages Solaar's GUI needs "
"(e.g. python3-gi + gir1.2-gtk-3.0 on Debian/Ubuntu, or the gtk3/"
"python3-gobject equivalents on your distro)."
) from e
for dev_info in base.receivers_and_devices():
try:
if dev_info.isDevice:
d = device.create_device(base, dev_info) # pyright: ignore[reportArgumentType]
if d is not None:
yield d
else:
d = receiver.create_receiver(base, dev_info) # pyright: ignore[reportArgumentType]
if d is not None:
yield from d
except OSError as e:
if e.errno == 13:
logger.error(
"permission denied opening %s -- check the Solaar udev rule "
"(rules.d/42-logitech-unify-permissions.rules) is installed "
"and you're in the right group",
dev_info.path,
)
else:
logger.exception("failed opening %s", dev_info)
continue
except Exception:
logger.exception("failed opening %s", dev_info)
continue
def _to_device_battery(dev, battery) -> DeviceBattery | None:
if battery is None:
return None
percentage = battery.level / 100 if isinstance(battery.level, int) else None
state = battery.status.name.lower() if battery.status is not None else None
serial = (
getattr(dev, "serial", None) or getattr(dev, "unitId", None) or None
)
return DeviceBattery(
model=dev.name or dev.codename or "Unknown device",
nativePath=_find_upower_native_path(serial),
serial=serial,
type=str(dev.kind) if dev.kind is not None else None,
wired=dev.receiver is None,
percentage=percentage,
state=state,
charging=battery.charging(),
isPresent=battery.ok(),
)
def _read_battery(dev) -> DeviceBattery | None:
try:
if not dev.ping():
return None
except Exception:
logger.debug("ping failed for %s", dev, exc_info=True)
return None
try:
battery = dev.battery()
except Exception:
logger.debug("battery() failed for %s", dev, exc_info=True)
return None
return _to_device_battery(dev, battery)
def poll_once() -> list[DeviceBattery]:
results: list[DeviceBattery] = []
seen_idents = set()
for dev in _iter_open_devices():
try:
info = _read_battery(dev)
if info is not None:
entry = _to_device_battery(dev, info)
if entry:
ident = entry.serial or (getattr(dev, "unitId", None))
if ident and ident in seen_idents:
continue
if ident:
seen_idents.add(ident)
results.append(entry)
finally:
with contextlib.suppress(Exception):
dev.close()
return results
def _write_json_atomic(path: Path, results: list[DeviceBattery]) -> None:
path.parent.mkdir(parents=True, exist_ok=True)
payload = {
"updated": time.time(),
"devices": [asdict(r) for r in results],
}
fd, tmp_path = tempfile.mkstemp(
dir=path.parent, prefix=".battery-", suffix=".tmp"
)
try:
with os.fdopen(fd, "w") as f:
json.dump(payload, f, indent=2)
os.replace(tmp_path, path)
except Exception:
Path(tmp_path).unlink(missing_ok=True)
raise
@app.command()
def daemon(
rescan_interval: float = typer.Option(
60.0,
"--rescan-interval",
"-i",
help=(
"Seconds between fallback rescans. This is NOT a battery poll "
"interval -- battery updates are event-driven and written as "
"soon as a device reports a change. New/removed devices "
"(e.g. plugging a keyboard in to charge) are normally noticed "
"within milliseconds via a udev hotplug watcher; this interval "
"is only a safety net in case a udev event is ever missed."
),
),
out: Path = typer.Option(
DEFAULT_OUTPUT,
"--out",
"-o",
help="Where to write the JSON status file.",
),
lock_file: Path = typer.Option(
DEFAULT_LOCK,
"--lock-file",
help="Path used to ensure only one daemon runs at a time.",
),
verbose: bool = typer.Option(
False,
"--verbose",
"-v",
help="Log every listener start and battery-change write to stderr.",
),
):
lock_fd = _acquire_singleton_lock(lock_file)
if lock_fd is None:
try:
holder_pid = lock_file.read_text().strip()
except OSError:
holder_pid = "unknown"
typer.echo(
f"error: a battery daemon is already running (pid {holder_pid}, lock: {lock_file})",
err=True,
)
raise typer.Exit(code=1)
if verbose:
logging.basicConfig(
level=logging.INFO, format="%(asctime)s %(name)s: %(message)s"
)
write_lock = threading.Lock()
last_payload = None
pending_write_timer: list[threading.Timer | None] = [None]
pending_write_lock = threading.Lock()
WRITE_SETTLE_SECONDS = 0.3
write_seq_lock = threading.Lock()
write_seq = 0
last_written_seq = 0
def write_if_changed():
nonlocal last_payload, write_seq, last_written_seq
with write_seq_lock:
write_seq += 1
my_seq = write_seq
results = _snapshot(registry)
with write_lock:
with write_seq_lock:
if my_seq <= last_written_seq:
return
last_written_seq = my_seq
payload = [asdict(r) for r in results]
if payload != last_payload:
_write_json_atomic(out, results)
last_payload = payload
logger.info(
"battery status changed, wrote %d device(s)", len(results)
)
def write_if_changed_coalesced():
with pending_write_lock:
existing = pending_write_timer[0]
if existing is not None:
existing.cancel()
timer = threading.Timer(WRITE_SETTLE_SECONDS, write_if_changed)
timer.daemon = True
pending_write_timer[0] = timer
timer.start()
def on_change(_device_or_receiver, alert=None, reason=None):
write_if_changed_coalesced()
registry = _ListenerRegistry(on_change)
def rescan_and_write():
try:
registry.rescan()
except Exception:
logger.exception("rescan failed")
write_if_changed_coalesced()
try:
registry.rescan()
except RuntimeError as e:
typer.echo(f"error: {e}", err=True)
raise typer.Exit(code=1) from None
write_if_changed()
hotplug_observer = _start_hotplug_watcher(rescan_and_write)
running = True
def _stop(signum, frame):
nonlocal running
running = False
signal.signal(signal.SIGINT, _stop)
signal.signal(signal.SIGTERM, _stop)
typer.echo(
f"Listening for battery events, writing to {out} (lock: {lock_file})"
)
try:
while running:
remaining = rescan_interval
while running and remaining > 0:
step = min(0.2, remaining)
time.sleep(step)
remaining -= step
if running:
rescan_and_write()
finally:
if hotplug_observer is not None:
with contextlib.suppress(Exception):
hotplug_observer.stop()
with contextlib.suppress(Exception):
_clean_json(out)
registry.stop()
typer.echo("Stopped.")
if __name__ == "__main__":
app()
+3 -4
View File
@@ -10,12 +10,11 @@ dependencies = [
"typer",
"pillow",
"jinja2",
"materialyoucolor"
"materialyoucolor",
"solaar",
"pytest"
]
[project.optional-dependencies]
dev = ["pytest"]
[project.scripts]
zshell-cli = "zshell:main"