123 Commits

Author SHA1 Message Date
zach ad23da4eda initial commit, primitive loading of QML code outside of shell directory 2026-05-25 13:32:06 +02:00
zach 06ebc4ffbf remove irrelevant settings options plus bugfixes 2026-05-25 11:51:16 +02:00
zach f2f9fa1302 fix opening links on non-uwsm and disable file watcher when shell is installed 2026-05-25 11:01:01 +02:00
zach b4020438f9 Merge pull request 'Tune transparency scale impact on luminance scaling' (#98) from fix/transparency-luminance-scaling into main
Reviewed-on: #98
Reviewed-by: AramJonghu <2+aramjonghu@noreply.git.zach-dev.cc>
2026-05-24 22:37:03 +02:00
zach 184ab20d11 Merge branch 'main' into fix/transparency-luminance-scaling
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 26s
Python / test (pull_request) Successful in 42s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
2026-05-24 22:36:52 +02:00
zach 5097e30a77 fix anchors used in Layouts 2026-05-24 19:33:06 +02:00
zach ef71ae8afd button to install colorscheme and wallpaper to greeter in settings 2026-05-24 19:28:02 +02:00
AramJonghu 6533533936 Merge branch 'main' into fix/transparency-luminance-scaling
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 51s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m47s
2026-05-24 18:53:39 +02:00
zach 9688072e93 record.py 2026-05-24 18:48:03 +02:00
AramJonghu 9c36f0de5b pycache in cache removal 2026-05-24 18:28:32 +02:00
zach 9ca46967d9 Merge pull request 'hotfix(cli): replace raw subprocess tracebacks with styled error messages and fix restart race condition' (#96) from hotfix-restart-race-condition into main
Reviewed-on: #96
Reviewed-by: zach <zach@brohn.se>
2026-05-24 18:23:43 +02:00
zach 16e84ca998 fixed region selection for recording, plus cache file cleanup
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 18s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m34s
2026-05-24 18:21:37 +02:00
AramJonghu c30128cf95 check every 50ms -> 250ms for restart
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 30s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m44s
2026-05-24 18:03:32 +02:00
zach ba9926af18 Increased floor and decreased ceiling of offset for brightening darker colors in dark mode/darkening brighter colors in light mode
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 28s
Python / test (pull_request) Successful in 43s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
2026-05-24 11:40:44 +02:00
AramJonghu 78fcf33b3a refactor(cli): clean shell start/restart, drop redundant ipc check
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 24s
Python / test (pull_request) Successful in 48s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m44s
2026-05-24 03:09:19 +02:00
AramJonghu 5e9b373405 tests did not match changed code logic
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m46s
2026-05-23 20:54:35 +02:00
AramJonghu b49165e7ea minor typer adjustments to use typer in error/exception throws
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 14s
Python / lint-format (pull_request) Successful in 34s
Python / test (pull_request) Failing after 53s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m48s
2026-05-23 20:48:51 +02:00
AramJonghu d0cda51639 wait for instance to fully terminate before restart 2026-05-23 20:31:48 +02:00
zach ad57764636 Merge pull request '#73 dynamic color scheme presets from .txt palettes + shell restart command' (#92) from 73-colorscheme-options into main
Reviewed-on: #92
2026-05-23 20:15:30 +02:00
zach 96afbdb30b Settings UI for color scheme presets
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 53s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m52s
2026-05-23 20:14:12 +02:00
AramJonghu 5df46160f6 documentation in README.md added
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 23s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m50s
2026-05-23 17:53:16 +02:00
AramJonghu d118c02e75 README: document scheme CLI and shell subcommands 2026-05-23 17:52:06 +02:00
AramJonghu 21ed178bbc scheme: add --json flag to list-presets, --accent flag, drop :accent from preset spec
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Python / lint-format (pull_request) Successful in 25s
Python / test (pull_request) Successful in 44s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m48s
- list-presets --json outputs structured JSON with variants,
  modes, accents, and default_accent for accent-aware schemes
- --accent flag replaces :accent shorthand in preset string
- Validate --accent against variant's available accents
- resolve_preset returns tuple[str, str] (scheme + variant only)
- Update tests for new signature
2026-05-23 17:42:58 +02:00
AramJonghu 2934d863ca --accent removed. Accidental inclusion
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Python / lint-format (pull_request) Successful in 22s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m46s
2026-05-23 00:51:14 +02:00
AramJonghu 7c29921a6b removal of plans package/directory (we have project board now)
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 22s
Python / test (pull_request) Successful in 49s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m45s
2026-05-23 00:42:25 +02:00
AramJonghu 0309fde3aa format check and lint resolved
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Successful in 19s
Python / test (pull_request) Successful in 46s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m50s
2026-05-22 23:26:33 +02:00
AramJonghu f147969f37 added workflow running python tests
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Python / lint-format (pull_request) Failing after 24s
Python / test (pull_request) Successful in 50s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m48s
2026-05-22 22:56:03 +02:00
AramJonghu 90a1954658 cache 2026-05-22 22:53:36 +02:00
AramJonghu 2b550763e7 ruff unused import 2026-05-22 22:46:02 +02:00
AramJonghu 3037cfab53 cache 2026-05-22 22:38:56 +02:00
AramJonghu 5f92b6f6de shell: add restart command, fix call None arg, add tests 2026-05-22 22:38:35 +02:00
AramJonghu 67ae693d0c Color preset schemes now exposed for use.
- Catppuccin txt colors extracted from dankmaterialyou and created txt
  files for each.
- Preset is now an option and are exposed.
- Tests test presets, might add workflow to run tests.
2026-05-22 22:32:31 +02:00
zach fd620e7487 prep for replay 2026-05-22 20:42:51 +02:00
zach 0ec426e0f0 Record module added to sidebar, file list and buttons. Region recording is broken 2026-05-22 12:51:06 +02:00
zach ec5e6d3995 add typer command 2026-05-22 11:06:17 +02:00
zach 41a129bb90 init commit 2026-05-22 11:04:54 +02:00
Inorishio 8c48ddbbe7 Merge branch 'main' of git.zach-dev.cc:zach/z-bar-qt 2026-05-21 23:58:49 +02:00
Inorishio 625d766719 added escape to lock + greeter 2026-05-21 23:58:24 +02:00
zach 88526b9e98 show notif icon true by default 2026-05-21 23:43:27 +02:00
zach a0d56b965c toggle to show notif icon on lockscreen 2026-05-21 23:29:49 +02:00
zach 2342edcf66 apply button, wheel doesn't zoom 2026-05-21 23:12:01 +02:00
zach 9e75b593f4 scale slider for crop tool 2026-05-21 19:14:29 +02:00
zach 4663c7d683 better wallpaper preview positioning 2026-05-21 17:55:31 +02:00
zach 80683800eb crop region now restores correctly 2026-05-21 16:52:12 +02:00
zach 57836f974c remove residue test files 2026-05-21 15:52:43 +02:00
zach 8dbb88e136 crop region now correct 2026-05-21 15:51:42 +02:00
zach 06c402c050 better crop region handling, but coordinate math is wrong 2026-05-20 23:16:55 +02:00
zach e425a1701b small fix for wallpaper grid 2026-05-20 14:34:11 +02:00
zach 41666d0150 Merge pull request 'hyprland lua support' (#91) from hypr-plugin into main
Reviewed-on: #91
2026-05-20 14:08:30 +02:00
zach 853b683962 Changed base deform numbers, less bouncy
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Lint & Format (Python) / lint-format (pull_request) Successful in 24s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m39s
2026-05-20 07:45:18 +02:00
zach b1bfcb3ed0 Merge branch 'main' into hypr-plugin
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 11s
Lint & Format (Python) / lint-format (pull_request) Successful in 19s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m39s
2026-05-20 07:35:26 +02:00
zach 68662120ba Merge pull request 'Lint/formatter workflows merged per language. Resolved errors and warnings for each workflow' (#90) from 89-lint-format-fixes into main
Reviewed-on: #90
2026-05-20 07:33:56 +02:00
zach b8af60008d fixed applying hyprland options and rules, as well as fetching
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 24s
Lint (Rust) / lint (pull_request) Failing after 1m37s
2026-05-20 07:30:38 +02:00
zach b8524ff621 experimental hyprland lua support 2026-05-20 07:17:38 +02:00
zach ffde4063a0 experimental hyprland lua support 2026-05-20 07:13:05 +02:00
zach 96bf5f3365 cleanup for hyprland lua configs 2026-05-20 06:50:14 +02:00
zach 053efb4aaf reintroduce cargo caching
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 20s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m39s
2026-05-20 05:23:56 +02:00
AramJonghu c88aef2164 removal of cache (for now)
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 20s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m33s
2026-05-20 01:03:51 +02:00
AramJonghu 01b54ec5e1 use of newest node version available + attempt cache v3 vs v4
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 25s
Lint & Format (Python) / lint-format (pull_request) Successful in 25s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m31s
2026-05-20 00:59:41 +02:00
AramJonghu 7276ee28dc minor updates to rust workflow
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 19s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m33s
2026-05-20 00:46:59 +02:00
AramJonghu a14ebe2016 wrong cache url
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 17s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m30s
2026-05-20 00:26:50 +02:00
AramJonghu d3f6765819 changed github specific caching to forgejo's solution
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Lint & Format (Python) / lint-format (pull_request) Successful in 18s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m31s
2026-05-20 00:19:46 +02:00
AramJonghu a3d0ee18cb added cargo to be cached to avoid recompiling every time. Also added echo messages for more info
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 13s
Lint & Format (Python) / lint-format (pull_request) Successful in 19s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m35s
2026-05-20 00:11:20 +02:00
AramJonghu c9d6b95ca5 main.rs formatted
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 20s
Lint & Format (Python) / lint-format (pull_request) Successful in 26s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m36s
2026-05-20 00:06:30 +02:00
AramJonghu 794a26a3fe eslint workflow now prints a succes or fail message eslint is run succesfully or if it ran but failed
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 18s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m46s
2026-05-20 00:05:25 +02:00
AramJonghu ca3a288eab minor changes to workflows to prevent preemptive exits/failures
Lint & Format (JS/TS) / lint-format (pull_request) Successful in 12s
Lint & Format (Python) / lint-format (pull_request) Successful in 21s
Lint & Format (Rust) / lint-format (pull_request) Successful in 1m34s
2026-05-20 00:02:14 +02:00
AramJonghu 902863e5ba adjusted workflows -> merge of lint/formatter per lang
Lint & Format (JS/TS) / lint-format (pull_request) Failing after 10s
Lint & Format (Python) / lint-format (pull_request) Failing after 19s
Lint & Format (Rust) / lint-format (pull_request) Failing after 33s
2026-05-19 23:57:11 +02:00
AramJonghu dd49198cf7 prettier and eslint ignore valid syntax for qml specific syntax. Clippy lint resolved in .rs. unused py files commented to be ignored by ruff 2026-05-19 23:53:09 +02:00
AramJonghu ceca949535 Merge pull request 'fix blobs dirty tracking' (#87) from blob-testing into main
Reviewed-on: #87
Reviewed-by: AramJonghu <2+aramjonghu@noreply.git.zach-dev.cc>
2026-05-19 23:18:57 +02:00
zach 24d5584b98 wallpaper now uses Image (hopefully temporarily)
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 10s
Lint (Python) / lint (pull_request) Failing after 19s
Lint (Rust) / lint (pull_request) Failing after 1m36s
2026-05-19 22:12:37 +02:00
zach 62ec1b9f33 cleanup unneeded logging
Format (JS/TS) / format (pull_request) Failing after 6s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 19s
Lint (Rust) / lint (pull_request) Failing after 1m34s
2026-05-19 16:15:39 +02:00
zach 9c6a1ce1a4 hide notification content on lockscreen, toggleable
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 18s
Lint (Rust) / lint (pull_request) Failing after 1m32s
2026-05-19 16:10:00 +02:00
zach b6ad180b6a select part of wallpaper to show
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 12s
Lint (Python) / lint (pull_request) Failing after 20s
Lint (Rust) / lint (pull_request) Failing after 1m40s
2026-05-19 15:38:59 +02:00
zach b20767c702 hopefully increase drawing performance
Format (JS/TS) / format (pull_request) Failing after 6s
Lint (JS/TS) / lint (pull_request) Failing after 15s
Lint (Python) / lint (pull_request) Failing after 18s
Lint (Rust) / lint (pull_request) Failing after 1m32s
2026-05-19 10:04:04 +02:00
zach 362b7bb8c2 add the drawing popout background
Format (JS/TS) / format (pull_request) Failing after 6s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 19s
Lint (Rust) / lint (pull_request) Failing after 1m36s
2026-05-19 08:43:52 +02:00
zach 405825518a move search index file to correct place
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 10s
Lint (Python) / lint (pull_request) Failing after 19s
Lint (Rust) / lint (pull_request) Failing after 1m30s
2026-05-19 08:30:31 +02:00
zach db7a822caf fix some backgrounds, now attaching to wrappers rather than panel itself most of the time
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 10s
Lint (Python) / lint (pull_request) Failing after 19s
Lint (Rust) / lint (pull_request) Failing after 1m35s
2026-05-19 08:24:05 +02:00
zach 3bd9444e2f fix settings background desync
Format (JS/TS) / format (pull_request) Failing after 6s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 25s
Lint (Rust) / lint (pull_request) Failing after 1m33s
2026-05-19 07:09:39 +02:00
zach d0e696c681 update blobs
Format (JS/TS) / format (pull_request) Failing after 8s
Lint (JS/TS) / lint (pull_request) Failing after 10s
Lint (Python) / lint (pull_request) Failing after 29s
Lint (Rust) / lint (pull_request) Failing after 1m34s
2026-05-19 04:52:28 +02:00
zach 550630feaa Revert "update blobs"
This reverts commit 8c22855dd8.
2026-05-19 04:24:24 +02:00
zach 8c22855dd8 update blobs 2026-05-19 04:20:35 +02:00
zach 015ee61885 revert blobs 2026-05-19 04:06:36 +02:00
zach 8fba953f52 update blobs 2026-05-19 04:04:32 +02:00
zach 3d2fc0a3b1 update blobs 2026-05-19 04:02:46 +02:00
zach c060be79e8 update blobs 2026-05-19 03:56:56 +02:00
zach cb1df5078b revert blobs 2026-05-19 03:55:48 +02:00
zach 5eb32fc30c update blobs 2026-05-19 03:53:47 +02:00
zach 0a84c822d5 update blobs 2026-05-19 03:50:24 +02:00
zach f89236f51e update blobs 2026-05-19 03:43:08 +02:00
zach 4b1316e887 log exclusion zones sizing, debugging inconsistent border exclusions 2026-05-18 18:25:23 +02:00
zach 63f4694322 rounding scale applies to more rounded elements, slight rounding changes to tray menu items 2026-05-17 00:52:53 +02:00
zach 51a8f1d5e1 remove residue file 2026-05-17 00:09:08 +02:00
zach d57010a501 Merge pull request 'prettierrc line update to 80 from 100, and singleQuote -> false from true' (#86) from forgejo-workflows into main
Reviewed-on: #86
2026-05-16 01:46:27 +02:00
AramJonghu d506d5ad27 not fmt, clippy linter
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 13s
Lint (Python) / lint (pull_request) Failing after 20s
Lint (Rust) / lint (pull_request) Failing after 1m40s
2026-05-16 01:42:30 +02:00
AramJonghu f6119072f7 rustfmt added
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 24s
Lint (Rust) / lint (pull_request) Failing after 35s
2026-05-16 01:40:08 +02:00
AramJonghu 00063309cd cargo not installed yet in yml
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 10s
Lint (Python) / lint (pull_request) Failing after 26s
Lint (Rust) / lint (pull_request) Failing after 36s
2026-05-16 01:38:57 +02:00
AramJonghu c30891de83 rustfmt w/o project
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 15s
Lint (Python) / lint (pull_request) Failing after 17s
Lint (Rust) / lint (pull_request) Failing after 38s
2026-05-16 01:37:38 +02:00
AramJonghu 55e9c0f267 rustfmt w/o project
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 21s
Lint (Rust) / lint (pull_request) Failing after 35s
2026-05-16 01:35:54 +02:00
AramJonghu 3424df53e2 Merge branch 'main' into forgejo-workflows
Format (JS/TS) / format (pull_request) Failing after 12s
Lint (JS/TS) / lint (pull_request) Failing after 15s
Lint (Python) / lint (pull_request) Failing after 31s
Lint (Rust) / lint (pull_request) Successful in 48s
2026-05-16 01:33:03 +02:00
AramJonghu 583c50f994 prettierrc line update to 80 from 100, and singleQuote -> false from true
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 12s
Lint (Python) / lint (pull_request) Failing after 20s
Lint (Rust) / lint (pull_request) Successful in 59s
2026-05-16 01:32:25 +02:00
zach ca53152630 Merge pull request 'testing ci using external forgejo runner' (#84) from forgejo-workflows into main
Reviewed-on: #84
2026-05-16 01:26:00 +02:00
zach 0cd7df243b Merge pull request 'Screenshot tool' (#83) from screenshot-tool into main
Reviewed-on: #83
2026-05-16 01:24:48 +02:00
AramJonghu 6a8ad4dbf2 removal qml linter, not worth
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 11s
Lint (Python) / lint (pull_request) Failing after 27s
Lint (Rust) / lint (pull_request) Successful in 29s
2026-05-16 01:15:42 +02:00
AramJonghu f57577fefd aur to arch
Format (JS/TS) / format (pull_request) Failing after 6s
Lint (JS/TS) / lint (pull_request) Failing after 35s
Lint (Python) / lint (pull_request) Failing after 45s
Lint (Rust) / lint (pull_request) Successful in 1m13s
Lint (QML) / lint (pull_request) Has been cancelled
2026-05-16 01:11:17 +02:00
AramJonghu 3a05cd339d qml to arch
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 18s
Lint (Python) / lint (pull_request) Failing after 18s
Lint (Rust) / lint (pull_request) Successful in 35s
Lint (QML) / lint (pull_request) Has been cancelled
2026-05-16 01:03:26 +02:00
AramJonghu c67a498f8d qml path issue fixed
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 15s
Lint (Python) / lint (pull_request) Failing after 25s
Lint (Rust) / lint (pull_request) Successful in 53s
Lint (QML) / lint (pull_request) Failing after 2m2s
2026-05-16 00:53:49 +02:00
AramJonghu e7e772ebc6 qml needs node
Format (JS/TS) / format (pull_request) Failing after 9s
Lint (JS/TS) / lint (pull_request) Failing after 20s
Lint (Python) / lint (pull_request) Failing after 27s
Lint (Rust) / lint (pull_request) Successful in 55s
Lint (QML) / lint (pull_request) Successful in 2m3s
2026-05-16 00:50:26 +02:00
AramJonghu fb2c9c6a21 qml goes to debian:sid for simplicity
Format (JS/TS) / format (pull_request) Failing after 10s
Lint (JS/TS) / lint (pull_request) Failing after 17s
Lint (Python) / lint (pull_request) Failing after 25s
Lint (QML) / lint (pull_request) Failing after 37s
Lint (Rust) / lint (pull_request) Successful in 42s
2026-05-16 00:43:03 +02:00
AramJonghu 383671344f qml goes to Ubuntu for simplicity
Format (JS/TS) / format (pull_request) Failing after 9s
Lint (JS/TS) / lint (pull_request) Failing after 18s
Lint (Python) / lint (pull_request) Failing after 22s
Lint (Rust) / lint (pull_request) Successful in 34s
Lint (QML) / lint (pull_request) Has been cancelled
2026-05-16 00:41:11 +02:00
AramJonghu beb1d96750 added eslint config
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 9s
Lint (Python) / lint (pull_request) Failing after 15s
Lint (QML) / lint (pull_request) Failing after 1m18s
Lint (Rust) / lint (pull_request) Successful in 30s
2026-05-16 00:37:20 +02:00
AramJonghu 6f8af9028b added eslint config
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Failing after 8s
Lint (Python) / lint (pull_request) Failing after 14s
Lint (QML) / lint (pull_request) Failing after 1m19s
Lint (Rust) / lint (pull_request) Has been cancelled
2026-05-16 00:35:18 +02:00
AramJonghu e874c19ee2 now should report issue when incorrect
Format (JS/TS) / format (pull_request) Failing after 7s
Lint (JS/TS) / lint (pull_request) Successful in 5s
Lint (Python) / lint (pull_request) Failing after 14s
Lint (Rust) / lint (pull_request) Has been cancelled
Lint (QML) / lint (pull_request) Has been cancelled
2026-05-16 00:33:37 +02:00
AramJonghu af04e5d227 case sensitivity
Format (JS/TS) / format (pull_request) Successful in 7s
Lint (JS/TS) / lint (pull_request) Successful in 5s
Lint (Python) / lint (pull_request) Successful in 14s
Lint (QML) / lint (pull_request) Successful in 1m20s
Lint (Rust) / lint (pull_request) Has been cancelled
2026-05-16 00:31:35 +02:00
AramJonghu 4ab19a8e37 separate all workflows, better overview
Format (JS/TS) / format (pull_request) Successful in 6s
Lint (JS/TS) / lint (pull_request) Successful in 5s
Lint (Python) / lint (pull_request) Successful in 14s
Lint (QML) / lint (pull_request) Successful in 1m19s
Lint (Rust) / lint (pull_request) Has been cancelled
2026-05-16 00:29:31 +02:00
AramJonghu 783d05f815 test different repo apline
CI (Lint + Format Checks) / lint (pull_request) Successful in 1m48s
2026-05-16 00:25:32 +02:00
AramJonghu 17fb9c0fef apline-edge for qmllint
CI (Lint + Format Checks) / lint (pull_request) Successful in 41s
2026-05-16 00:24:06 +02:00
AramJonghu 39cbfa2c93 minor adjustments and added .prettierrc.json
CI (Lint + Format Checks) / lint (pull_request) Successful in 1m41s
2026-05-16 00:16:17 +02:00
AramJonghu 17fef78672 fix python venv in ci
CI (Lint + Format Checks) / lint (pull_request) Failing after 38s
2026-05-16 00:06:20 +02:00
AramJonghu c120dcae41 added node as this is required
CI (Lint + Format Checks) / lint (pull_request) Failing after 41s
2026-05-16 00:04:50 +02:00
AramJonghu 64e65ca9df test
CI (Lint + Format Checks) / lint (pull_request) Failing after 4s
2026-05-16 00:01:14 +02:00
AramJonghu 22a7993c07 test
CI (Lint + Format Checks) / lint (pull_request) Failing after 2s
2026-05-15 23:59:39 +02:00
AramJonghu 24526ca2d1 runs on Linux, not alpine?
CI (Lint + Format Checks) / lint (pull_request) Has been cancelled
2026-05-15 23:56:30 +02:00
AramJonghu c5ee27bf62 testing ci using external forgejo runner
CI (Lint + Format Checks) / lint (pull_request) Failing after 7s
2026-05-15 23:52:46 +02:00
189 changed files with 12439 additions and 4058 deletions
+42
View File
@@ -0,0 +1,42 @@
name: Lint & Format (JS/TS)
on:
pull_request:
jobs:
lint-format:
runs-on: alpine
container: node:26-alpine
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install tools
run: |
apk add --no-cache \
git
- name: Prettier
continue-on-error: true
run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
npx --yes prettier --check "**/*.{js,jsx,ts,tsx,mjs,cjs}" --ignore-path .prettierignore
else
echo "No JS/TS files found"
fi
- name: ESLint
run: |
if [ -n "$(find . \( -iname "*.js" -o -iname "*.jsx" -o -iname "*.ts" -o -iname "*.tsx" -o -iname "*.mjs" -o -iname "*.cjs" \) -print -quit)" ]; then
if [ -f package.json ]; then
npm install --no-audit --no-fund
fi
if [ -f eslint.config.js ] || [ -f eslint.config.mjs ] || [ -f eslint.config.cjs ] || [ -f .eslintrc ] || [ -f .eslintrc.js ] || [ -f .eslintrc.cjs ] || [ -f .eslintrc.json ] || [ -f .eslintrc.yaml ] || [ -f .eslintrc.yml ]; then
npx --yes eslint . && echo "ESLint passed" || echo "ESLint failed"
else
echo "No eslint config found"
fi
else
echo "No JS/TS files found"
fi
+65
View File
@@ -0,0 +1,65 @@
name: Python
on:
pull_request:
jobs:
lint-format:
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: Format check
continue-on-error: true
run: |
. .venv/bin/activate
ruff format --check .
- name: Lint
run: |
. .venv/bin/activate
ruff check .
test:
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 \
py3-pillow \
build-base
python3 -m venv .venv
. .venv/bin/activate
pip install --no-cache-dir \
typer \
pillow \
materialyoucolor \
jinja2 \
pytest
- name: Test
run: |
. .venv/bin/activate
cd cli
python -m pytest tests/ -v
+72
View File
@@ -0,0 +1,72 @@
name: Lint & Format (Rust)
on:
pull_request:
jobs:
lint-format:
runs-on: alpine
container: node:26-alpine
env:
CARGO_HOME: ${{ github.workspace }}/.cargo
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Cache cargo packages
uses: actions/cache@v4
env:
cache-name: cache-cargo-packages
with:
path: |
.cargo/registry
.cargo/git
target
key: rust-${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: |
rust-${{ runner.os }}-build-${{ env.cache-name }}-
rust-${{ runner.os }}-build-
rust-
- name: Install tools
run: |
apk add --no-cache \
git \
cargo \
rust \
rustfmt \
rust-clippy
- name: Format check
continue-on-error: true
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
for manifest in $(find . -name "Cargo.toml"); do
cargo fmt --manifest-path "$manifest" --check && \
echo "$manifest: formatting OK" || \
echo "$manifest: needs formatting"
done
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
- name: Clippy
run: |
if [ -n "$(find . -name "Cargo.toml" -print -quit)" ]; then
status=0
for manifest in $(find . -name "Cargo.toml"); do
cargo clippy --manifest-path "$manifest" --all-targets --all-features -- -D warnings && \
echo "$manifest: Clippy passed" || \
{ echo "$manifest: Clippy failed"; status=1; }
done
exit $status
elif [ -n "$(find . -name "*.rs" -print -quit)" ]; then
echo "Rust files found but no Cargo.toml"
exit 1
else
echo "No Rust project found"
fi
+3
View File
@@ -0,0 +1,3 @@
.venv/
scripts/fzf.js
scripts/fuzzysort.js
+13
View File
@@ -0,0 +1,13 @@
{
"semi": true,
"singleQuote": false,
"jsxSingleQuote": false,
"tabWidth": 4,
"printWidth": 80,
"trailingComma": "es5",
"bracketSpacing": true,
"bracketSameLine": false,
"arrowParens": "always",
"endOfLine": "lf",
"proseWrap": "preserve"
}
+8 -1
View File
@@ -31,6 +31,13 @@ if("shell" IN_LIST ENABLE_MODULES)
foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths) foreach(dir assets scripts Components Config Modules Daemons Drawers Effects Helpers Paths)
install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}") install(DIRECTORY ${dir} DESTINATION "${INSTALL_QSCONFDIR}")
endforeach() endforeach()
install(FILES shell.qml DESTINATION "${INSTALL_QSCONFDIR}")
# Disable watching for changes
file(READ shell.qml SHELL_QML)
string(REPLACE "settings.watchFiles: true" "settings.watchFiles: false" SHELL_QML "${SHELL_QML}")
file(WRITE "${CMAKE_BINARY_DIR}/qml/shell.qml" "${SHELL_QML}")
install(FILES "${CMAKE_BINARY_DIR}/qml/shell.qml" DESTINATION "${INSTALL_QSCONFDIR}")
# Greeter
install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}") install(DIRECTORY Greeter/ DESTINATION "${INSTALL_GREETERCONFDIR}")
endif() endif()
+16 -2
View File
@@ -8,20 +8,34 @@ Item {
id: root id: root
property alias active: splitButton.active property alias active: splitButton.active
property alias buttonAlias: splitButton
property bool enabled: true property bool enabled: true
property alias expanded: splitButton.expanded property alias expanded: splitButton.expanded
property int expandedZ: 100 property int expandedZ: 100
required property string label required property string label
property alias menuItems: splitButton.menuItems property alias menuItems: splitButton.menuItems
property bool shouldBeActive: true
property alias type: splitButton.type property alias type: splitButton.type
signal selected(item: MenuItem) signal selected(item: MenuItem)
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
clip: false clip: false
implicitHeight: row.implicitHeight + Appearance.padding.smaller * 2
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
z: root.expanded ? expandedZ : -1 z: root.expanded ? expandedZ : -1
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
RowLayout { RowLayout {
id: row id: row
+1
View File
@@ -15,6 +15,7 @@ Text {
color: DynamicColors.palette.m3onSurface color: DynamicColors.palette.m3onSurface
font.family: Appearance.font.family.sans font.family: Appearance.font.family.sans
font.pointSize: Appearance.font.size.normal font.pointSize: Appearance.font.size.normal
linkColor: DynamicColors.palette.m3onPrimaryFixedVariant
renderType: Text.NativeRendering renderType: Text.NativeRendering
textFormat: Text.PlainText textFormat: Text.PlainText
+1 -2
View File
@@ -41,12 +41,11 @@ CustomRect {
color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour color: type === IconButton.Text ? "transparent" : disabled ? disabledColour : internalChecked ? activeColour : inactiveColour
implicitHeight: label.implicitHeight + padding * 2 implicitHeight: label.implicitHeight + padding * 2
implicitWidth: implicitHeight implicitWidth: implicitHeight
radius: internalChecked ? 6 : implicitHeight / 2 * Math.min(1, 1) radius: internalChecked ? 6 : (implicitHeight / 2 * Math.min(1, 1)) * Appearance.rounding.scale
Behavior on radius { Behavior on radius {
Anim { Anim {
id: radiusAnim id: radiusAnim
} }
} }
+4
View File
@@ -7,4 +7,8 @@ JsonObject {
property real alignX: 0.5 property real alignX: 0.5
property real alignY: 0.5 property real alignY: 0.5
property real zoom: 1.0 property real zoom: 1.0
property real sourceClipX: 0
property real sourceClipY: 0
property real sourceClipW: 0
property real sourceClipH: 0
} }
+1
View File
@@ -58,6 +58,7 @@ JsonObject {
property Popouts popouts: Popouts { property Popouts popouts: Popouts {
} }
property int rounding: 8 property int rounding: 8
property int smoothing: 32
component Popouts: JsonObject { component Popouts: JsonObject {
property bool activeWindow: true property bool activeWindow: true
+8
View File
@@ -1,5 +1,13 @@
import Quickshell.Io import Quickshell.Io
JsonObject { JsonObject {
property Presets presets: Presets {
}
property string schemeType: "vibrant" property string schemeType: "vibrant"
component Presets: JsonObject {
property string accent: ""
property string name: ""
property string variant: ""
}
} }
+25 -2
View File
@@ -22,6 +22,7 @@ Singleton {
property alias notifs: adapter.notifs property alias notifs: adapter.notifs
property alias osd: adapter.osd property alias osd: adapter.osd
property alias overview: adapter.overview property alias overview: adapter.overview
property alias plugins: adapter.plugins
property bool recentlySaved: false property bool recentlySaved: false
property alias screenshot: adapter.screenshot property alias screenshot: adapter.screenshot
property alias services: adapter.services property alias services: adapter.services
@@ -83,6 +84,10 @@ Singleton {
wallFadeDuration: background.wallFadeDuration, wallFadeDuration: background.wallFadeDuration,
enabled: background.enabled, enabled: background.enabled,
alignX: background.alignX, alignX: background.alignX,
sourceClipX: background.sourceClipX,
sourceClipY: background.sourceClipY,
sourceClipW: background.sourceClipW,
sourceClipH: background.sourceClipH,
alignY: background.alignY, alignY: background.alignY,
zoom: background.zoom zoom: background.zoom
}; };
@@ -94,6 +99,7 @@ Singleton {
hideWhenNotif: barConfig.hideWhenNotif, hideWhenNotif: barConfig.hideWhenNotif,
rounding: barConfig.rounding, rounding: barConfig.rounding,
border: barConfig.border, border: barConfig.border,
smoothing: barConfig.smoothing,
height: barConfig.height, height: barConfig.height,
popouts: { popouts: {
tray: barConfig.popouts.tray, tray: barConfig.popouts.tray,
@@ -110,7 +116,12 @@ Singleton {
function serializeColors(): var { function serializeColors(): var {
return { return {
schemeType: colors.schemeType schemeType: colors.schemeType,
presets: {
name: colors.presets.name,
variant: colors.presets.variant,
accent: colors.presets.accent
}
}; };
} }
@@ -130,7 +141,8 @@ Singleton {
launcher: serializeLauncher(), launcher: serializeLauncher(),
colors: serializeColors(), colors: serializeColors(),
dock: serializeDock(), dock: serializeDock(),
screenshot: serializeScreenshot() screenshot: serializeScreenshot(),
plugins: serializePlugins()
}; };
} }
@@ -236,6 +248,8 @@ Singleton {
return { return {
recolorLogo: lock.recolorLogo, recolorLogo: lock.recolorLogo,
enableFprint: lock.enableFprint, enableFprint: lock.enableFprint,
showNotifContent: lock.showNotifContent,
showNotifIcon: lock.showNotifIcon,
maxFprintTries: lock.maxFprintTries, maxFprintTries: lock.maxFprintTries,
blurAmount: lock.blurAmount, blurAmount: lock.blurAmount,
sizes: { sizes: {
@@ -277,6 +291,13 @@ Singleton {
}; };
} }
function serializePlugins(): var {
return {
enabled: plugins.enabled,
entries: plugins.entries
};
}
function serializeScreenshot(): var { function serializeScreenshot(): var {
return { return {
enable_pp: screenshot.enable_pp, enable_pp: screenshot.enable_pp,
@@ -446,6 +467,8 @@ Singleton {
} }
property Overview overview: Overview { property Overview overview: Overview {
} }
property PluginConfig plugins: PluginConfig {
}
property Screenshot screenshot: Screenshot { property Screenshot screenshot: Screenshot {
} }
property Services services: Services { property Services services: Services {
+38 -6
View File
@@ -3,6 +3,7 @@ pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Hyprland
import QtQuick import QtQuick
import ZShell import ZShell
import qs.Helpers import qs.Helpers
@@ -29,9 +30,10 @@ Singleton {
readonly property alias wallLuminance: analyser.luminance readonly property alias wallLuminance: analyser.luminance
function alterColor(c: color, a: real, layer: int): color { function alterColor(c: color, a: real, layer: int): color {
const luminance = getLuminance(c); const initLuminance = getLuminance(c);
const luminance = Math.max(initLuminance, 0.001);
const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (1 - transparency.base) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5)); const offset = (!light || layer == 1 ? 1 : -layer / 2) * (light ? 0.2 : 0.3) * (0.2 + 0.3 * (1 - transparency.base)) * (1 + wallLuminance * (light ? (layer == 1 ? 3 : 1) : 2.5));
const scale = (luminance + offset) / luminance; const scale = (luminance + offset) / luminance;
const r = Math.max(0, Math.min(1, c.r * scale)); const r = Math.max(0, Math.min(1, c.r * scale));
const g = Math.max(0, Math.min(1, c.g * scale)); const g = Math.max(0, Math.min(1, c.g * scale));
@@ -79,10 +81,32 @@ Singleton {
} }
function reloadHyprRules(): void { function reloadHyprRules(): void {
const barStr = "keyword layerrule %1 %2, match:namespace ZShell-Bar"; const blur = transparency.enabled ? 1 : 0;
const authStr = "keyword layerrule %1 %2, match:namespace ZShell-Auth"; const alpha = transparency.base - 0.03;
Hypr.extras.batchMessage([barStr.arg("blur").arg(transparency.enabled ? 1 : 0), barStr.arg("ignore_alpha").arg(transparency.base - 0.03)]);
Hypr.extras.batchMessage([authStr.arg("blur").arg(transparency.enabled ? 1 : 0), authStr.arg("ignore_alpha").arg(transparency.base - 0.03)]); const rules = `
hl.layer_rule({
match = { namespace = "ZShell-Bar" },
blur = ${blur}
})
hl.layer_rule({
match = { namespace = "ZShell-Bar" },
ignore_alpha = ${alpha}
})
hl.layer_rule({
match = { namespace = "ZShell-Auth" },
blur = ${blur}
})
hl.layer_rule({
match = { namespace = "ZShell-Auth" },
ignore_alpha = ${alpha}
})
`;
Hypr.extras.message(`eval ${rules}`);
} }
function setMode(mode: string): void { function setMode(mode: string): void {
@@ -93,6 +117,14 @@ Singleton {
Component.onCompleted: debounceTimer.triggered() Component.onCompleted: debounceTimer.triggered()
Connections {
function onUsingLuaChanged(): void {
root.reloadHyprRules();
}
target: Hyprland
}
Connections { Connections {
function onConfigReloaded(): void { function onConfigReloaded(): void {
root.reloadHyprRules(); root.reloadHyprRules();
+2
View File
@@ -5,6 +5,8 @@ JsonObject {
property bool enableFprint: true property bool enableFprint: true
property int maxFprintTries: 3 property int maxFprintTries: 3
property bool recolorLogo: false property bool recolorLogo: false
property bool showNotifContent: false
property bool showNotifIcon: true
property Sizes sizes: Sizes { property Sizes sizes: Sizes {
} }
+11
View File
@@ -0,0 +1,11 @@
import Quickshell.Io
JsonObject {
property bool enabled: false
property list<var> entries: [
{
id: "Plugin",
enabled: false
},
]
}
+23 -174
View File
@@ -3,184 +3,33 @@ import QtQuick
Canvas { Canvas {
id: root id: root
property rect dirtyRect: Qt.rect(0, 0, 0, 0)
property bool frameQueued: false
property bool fullRepaintPending: true
property point lastPoint: Qt.point(0, 0)
property real minPointDistance: 2.0
property color penColor: "white" property color penColor: "white"
property real penWidth: 4 property real penWidth: 4
property var pendingSegments: [] property var points: []
property bool strokeActive: false
property var strokes: []
function appendPoint(x, y) { function clear(): void {
if (!strokeActive || strokes.length === 0) var ctx = getContext('2d');
root.points = [];
ctx.reset();
root.requestPaint();
}
renderStrategy: Canvas.Cooperative
onPaint: {
if (points.length < 2)
return; return;
const dx = x - lastPoint.x; var ctx = root.getContext('2d');
const dy = y - lastPoint.y; ctx.save();
ctx.lineWidth = root.penWidth;
if ((dx * dx + dy * dy) < (minPointDistance * minPointDistance)) ctx.strokeStyle = root.penColor;
return;
const x1 = lastPoint.x;
const y1 = lastPoint.y;
const x2 = x;
const y2 = y;
strokes[strokes.length - 1].push(Qt.point(x2, y2));
pendingSegments.push({
dot: false,
x1: x1,
y1: y1,
x2: x2,
y2: y2
});
lastPoint = Qt.point(x2, y2);
queueDirty(segmentDirtyRect(x1, y1, x2, y2));
}
function beginStroke(x, y) {
const p = Qt.point(x, y);
strokes.push([p]);
lastPoint = p;
strokeActive = true;
pendingSegments.push({
dot: true,
x: x,
y: y
});
queueDirty(pointDirtyRect(x, y));
}
function clear() {
strokes = [];
pendingSegments = [];
dirtyRect = Qt.rect(0, 0, 0, 0);
fullRepaintPending = true;
markDirty(Qt.rect(0, 0, width, height));
}
function drawDot(ctx, x, y) {
ctx.beginPath();
ctx.arc(x, y, penWidth / 2, 0, Math.PI * 2);
ctx.fill();
}
function drawSegment(ctx, x1, y1, x2, y2) {
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
function endStroke() {
strokeActive = false;
}
function pointDirtyRect(x, y) {
const pad = penWidth + 2;
return Qt.rect(x - pad, y - pad, pad * 2, pad * 2);
}
function queueDirty(r) {
dirtyRect = unionRects(dirtyRect, r);
if (frameQueued)
return;
frameQueued = true;
requestAnimationFrame(function () {
frameQueued = false;
if (dirtyRect.width > 0 && dirtyRect.height > 0) {
markDirty(dirtyRect);
dirtyRect = Qt.rect(0, 0, 0, 0);
}
});
}
function replayAll(ctx) {
ctx.clearRect(0, 0, width, height);
for (const stroke of strokes) {
if (!stroke || stroke.length === 0)
continue;
if (stroke.length === 1) {
const p = stroke[0];
drawDot(ctx, p.x, p.y);
continue;
}
ctx.beginPath();
ctx.moveTo(stroke[0].x, stroke[0].y);
for (let i = 1; i < stroke.length; ++i)
ctx.lineTo(stroke[i].x, stroke[i].y);
ctx.stroke();
}
}
function requestFullRepaint() {
fullRepaintPending = true;
markDirty(Qt.rect(0, 0, width, height));
}
function segmentDirtyRect(x1, y1, x2, y2) {
const pad = penWidth + 2;
const left = Math.min(x1, x2) - pad;
const top = Math.min(y1, y2) - pad;
const right = Math.max(x1, x2) + pad;
const bottom = Math.max(y1, y2) + pad;
return Qt.rect(left, top, right - left, bottom - top);
}
function unionRects(a, b) {
if (a.width <= 0 || a.height <= 0)
return b;
if (b.width <= 0 || b.height <= 0)
return a;
const left = Math.min(a.x, b.x);
const top = Math.min(a.y, b.y);
const right = Math.max(a.x + a.width, b.x + b.width);
const bottom = Math.max(a.y + a.height, b.y + b.height);
return Qt.rect(left, top, right - left, bottom - top);
}
anchors.fill: parent
contextType: "2d"
renderStrategy: Canvas.Threaded
renderTarget: Canvas.Image
onHeightChanged: requestFullRepaint()
onPaint: region => {
const ctx = getContext("2d");
ctx.lineCap = "round"; ctx.lineCap = "round";
ctx.lineJoin = "round"; ctx.beginPath();
ctx.lineWidth = penWidth; ctx.moveTo(points[0].x, points[0].y);
ctx.strokeStyle = penColor; for (var i = 1; i < points.length; i++)
ctx.fillStyle = penColor; ctx.lineTo(points[i].x, points[i].y);
ctx.stroke();
if (fullRepaintPending) { points = points.slice(points.length - 2);
fullRepaintPending = false; ctx.restore();
replayAll(ctx);
pendingSegments = [];
return;
}
for (const seg of pendingSegments) {
if (seg.dot)
drawDot(ctx, seg.x, seg.y);
else
drawSegment(ctx, seg.x1, seg.y1, seg.x2, seg.y2);
}
pendingSegments = [];
} }
onWidthChanged: requestFullRepaint()
} }
+7 -5
View File
@@ -30,8 +30,10 @@ CustomMouseArea {
const x = event.x; const x = event.x;
const y = event.y; const y = event.y;
if (event.buttons & Qt.LeftButton) if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.drawing.appendPoint(x, y); root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
}
if (root.inLeftPanel(root.popout, x, y)) { if (root.inLeftPanel(root.popout, x, y)) {
root.z = -2; root.z = -2;
@@ -44,7 +46,8 @@ CustomMouseArea {
if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) { if (root.visibilities.isDrawing && (event.buttons & Qt.LeftButton)) {
root.panels.drawing.expanded = false; root.panels.drawing.expanded = false;
root.drawing.beginStroke(x, y); root.drawing.points.push(Qt.point(x, y));
root.drawing.requestPaint();
return; return;
} }
@@ -52,7 +55,6 @@ CustomMouseArea {
root.drawing.clear(); root.drawing.clear();
} }
onReleased: { onReleased: {
if (root.visibilities.isDrawing) root.drawing.points = [];
root.drawing.endStroke();
} }
} }
+15
View File
@@ -12,22 +12,37 @@ Scope {
required property ShellScreen screen required property ShellScreen screen
ExclusionZone { ExclusionZone {
id: top
anchors.top: true anchors.top: true
exclusiveZone: root.bar.exclusiveZone exclusiveZone: root.bar.exclusiveZone
} }
ExclusionZone { ExclusionZone {
id: left
anchors.left: true anchors.left: true
} }
ExclusionZone { ExclusionZone {
id: right
anchors.right: true anchors.right: true
} }
ExclusionZone { ExclusionZone {
id: bottom
anchors.bottom: true anchors.bottom: true
} }
Timer {
interval: 5000
running: true
onTriggered: console.log("top height:", top.exclusiveZone, "left width:", left.exclusiveZone, "right width:", right.exclusiveZone, "bottom height:", bottom.exclusiveZone)
}
component ExclusionZone: CustomWindow { component ExclusionZone: CustomWindow {
exclusiveZone: Config.barConfig.border exclusiveZone: Config.barConfig.border
implicitHeight: 1 implicitHeight: 1
+18 -18
View File
@@ -78,7 +78,7 @@ CustomMouseArea {
const dragY = y - dragStart.y; const dragY = y - dragStart.y;
if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) { if (root.visibilities.isDrawing && !root.inLeftPanel(root.panels.drawing, x, y)) {
root.input.z = 2; // root.input.z = 2;
root.panels.drawing.expanded = false; root.panels.drawing.expanded = false;
} }
@@ -96,25 +96,25 @@ CustomMouseArea {
if (dragY < -10) if (dragY < -10)
visibilities.dock = true; visibilities.dock = true;
if (panels.sidebar.width === 0) { if (panels.sidebar.width === 0) {
const showOsd = inRightPanel(panels.osdWrapper, x, y); const showOsd = inRightPanel(panels.osdWrapper, x, y);
if (showOsd) { if (showOsd) {
osdShortcutActive = false; osdShortcutActive = false;
root.panels.osd.hovered = true; root.panels.osd.hovered = true;
}
} else {
const outOfSidebar = x < width - panels.sidebar.width;
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
visibilities.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
} }
} else {
const outOfSidebar = x < width - panels.sidebar.width;
const showOsd = outOfSidebar && inRightPanel(panels.osdWrapper, x, y);
if (!osdShortcutActive) {
visibilities.osd = showOsd;
root.panels.osd.hovered = showOsd;
} else if (showOsd) {
osdShortcutActive = false;
root.panels.osd.hovered = true;
}
}
if (Config.dock.enable && !Config.dock.hoverToReveal && !visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y)) if (Config.dock.enable && !Config.dock.hoverToReveal && !visibilities.dock && !visibilities.launcher && inBottomPanel(panels.dock, x, y))
visibilities.dock = true; visibilities.dock = true;
+17 -6
View File
@@ -34,6 +34,7 @@ Item {
readonly property alias resourcesWrapper: resourcesWrapper readonly property alias resourcesWrapper: resourcesWrapper
required property ShellScreen screen required property ShellScreen screen
readonly property alias settings: settings readonly property alias settings: settings
readonly property alias settingsWrapper: settingsWrapper
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
readonly property alias toasts: toasts readonly property alias toasts: toasts
readonly property alias utilities: utilities readonly property alias utilities: utilities
@@ -176,15 +177,25 @@ Item {
visibilities: root.visibilities visibilities: root.visibilities
} }
Settings.Wrapper { Item {
id: settings id: settingsWrapper
anchors.horizontalCenter: parent.horizontalCenter anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top anchors.top: parent.top
// anchors.centerIn: parent clip: true
panels: root implicitHeight: settings.implicitHeight * (1 - settings.offsetScale)
screen: root.screen implicitWidth: settings.implicitWidth
visibilities: root.visibilities
Settings.Wrapper {
id: settings
anchors.horizontalCenter: parent.horizontalCenter
anchors.top: parent.top
// anchors.centerIn: parent
panels: root
screen: root.screen
visibilities: root.visibilities
}
} }
Dock.Wrapper { Dock.Wrapper {
+31 -18
View File
@@ -158,6 +158,7 @@ Variants {
id: blobGroup id: blobGroup
color: DynamicColors.palette.m3surface color: DynamicColors.palette.m3surface
smoothing: Config.barConfig.smoothing
Behavior on color { Behavior on color {
CAnim { CAnim {
@@ -179,28 +180,34 @@ Variants {
PanelBg { PanelBg {
id: dashBg id: dashBg
deformAmount: 0.08 * Config.appearance.deform.scale property real extraHeight: 0.2
implicitHeight: panels.dashboard.height
deformAmount: 0.06
implicitHeight: panels.dashboard.height * (1 + extraHeight)
implicitWidth: panels.dashboard.width implicitWidth: panels.dashboard.width
panel: panels.dashboard panel: panels.dashboardWrapper
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
x: panels.dashboardWrapper.x + panels.dashboard.x + Config.barConfig.border x: panels.dashboardWrapper.x + panels.dashboard.x + Config.barConfig.border
y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight y: panels.dashboardWrapper.y + panels.dashboard.y + bar.implicitHeight - panels.dashboard.height * extraHeight
} }
PanelBg { PanelBg {
id: launcherBg id: launcherBg
deformAmount: 0.08 * Config.appearance.deform.scale property real extraHeight: 0.2
deformAmount: 0.06
implicitHeight: panels.launcher.height * (1 + extraHeight)
panel: panels.launcher panel: panels.launcher
radius: Appearance.rounding.smallest + 5 radius: Appearance.rounding.smallest + 5
y: panels.launcher.y + bar.implicitHeight
} }
PanelBg { PanelBg {
id: sidebarBg id: sidebarBg
bottomLeftRadius: 0 bottomLeftRadius: 0
deformAmount: 0.08 * Config.appearance.deform.scale deformAmount: 0.04
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [utilsBg]
implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2 implicitHeight: panel.height * (1 / rawDeformMatrix.m22) + 2
panel: panels.sidebar panel: panels.sidebar
@@ -209,10 +216,10 @@ Variants {
PanelBg { PanelBg {
id: osdBg id: osdBg
deformAmount: 0.1 * Config.appearance.deform.scale deformAmount: 0.1
implicitHeight: panels.osd.height implicitHeight: panels.osd.height
implicitWidth: panels.osd.width implicitWidth: panels.osd.width
panel: panels.osd panel: panels.osdWrapper
radius: 20 radius: 20
x: panels.osdWrapper.x + panels.osd.x + Config.barConfig.border x: panels.osdWrapper.x + panels.osd.x + Config.barConfig.border
y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight y: panels.osdWrapper.y + panels.osd.y + bar.implicitHeight
@@ -227,7 +234,7 @@ Variants {
PanelBg { PanelBg {
id: utilsBg id: utilsBg
deformAmount: panels.sidebar.visible ? (0.1 * Config.appearance.deform.scale) : (0.1 * Config.appearance.deform.scale) deformAmount: panels.sidebar.visible ? (0.1) : (0.1)
exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg] exclude: panels.sidebar.offsetScale > 0.08 ? [] : [sidebarBg]
panel: panels.utilities panel: panels.utilities
topLeftRadius: 0 topLeftRadius: 0
@@ -238,11 +245,11 @@ Variants {
property real extraHeight: panels.popouts.isDetached ? 0 : 0.2 property real extraHeight: panels.popouts.isDetached ? 0 : 0.2
deformAmount: panels.popouts.isDetached ? 0.05 * Config.appearance.deform.scale : panels.popouts.hasCurrent ? 0.15 * Config.appearance.deform.scale : 0.1 * Config.appearance.deform.scale deformAmount: panels.popouts.isDetached ? 0.05 : panels.popouts.hasCurrent ? 0.15 : 0.1
implicitHeight: panels.popouts.height * (1 + extraHeight) implicitHeight: panels.popouts.height * (1 + extraHeight)
implicitWidth: panels.popouts.width implicitWidth: panels.popouts.width
panel: panels.popouts panel: panels.popoutsWrapper
radius: (panels.popouts.currentName.startsWith("audio") || panels.popouts.currentName.startsWith("updates")) ? Appearance.rounding.normal : Appearance.rounding.smallest radius: (panels.popouts.currentName.startsWith("audio") || panels.popouts.currentName.startsWith("updates")) ? Appearance.rounding.normal : 20 * Appearance.rounding.scale
x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border x: panels.popoutsWrapper.x + panels.popouts.x + Config.barConfig.border
y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight y: panels.popoutsWrapper.y + panels.popouts.y + bar.implicitHeight - panels.popouts.height * extraHeight
@@ -255,10 +262,10 @@ Variants {
PanelBg { PanelBg {
id: resourcesBg id: resourcesBg
deformAmount: 0.08 * Config.appearance.deform.scale deformAmount: 0.05
implicitHeight: panels.resources.height implicitHeight: panels.resources.height
implicitWidth: panels.resources.width implicitWidth: panels.resources.width
panel: panels.resources panel: panels.resourcesWrapper
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border x: panels.resourcesWrapper.x + panels.resources.x + Config.barConfig.border
y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight y: panels.resourcesWrapper.y + panels.resources.y + bar.implicitHeight
@@ -267,17 +274,23 @@ Variants {
PanelBg { PanelBg {
id: settingsBg id: settingsBg
deformAmount: 0.08 * Config.appearance.deform.scale property real extraHeight: 0.2
deformAmount: 0.03
implicitHeight: panels.settings.height * (1 + extraHeight)
implicitWidth: panels.settings.width
panel: panels.settings panel: panels.settings
radius: Appearance.rounding.large radius: Appearance.rounding.large
topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller topLeftRadius: Appearance.rounding.large + Appearance.padding.smaller
topRightRadius: Appearance.rounding.large + Appearance.padding.smaller topRightRadius: Appearance.rounding.large + Appearance.padding.smaller
x: panels.settingsWrapper.x + panels.settings.x + Config.barConfig.border
y: panels.settingsWrapper.y + panels.settings.y + bar.implicitHeight - panels.settings.height * extraHeight
} }
PanelBg { PanelBg {
id: dockBg id: dockBg
deformAmount: 0.08 * Config.appearance.deform.scale deformAmount: 0.08
panel: panels.dock panel: panels.dock
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
} }
@@ -285,7 +298,7 @@ Variants {
PanelBg { PanelBg {
id: drawingBg id: drawingBg
deformAmount: 0.08 * Config.appearance.deform.scale deformAmount: 0.08
panel: panels.drawing panel: panels.drawing
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
} }
@@ -380,7 +393,7 @@ Variants {
property real deformAmount: 0.15 property real deformAmount: 0.15
required property Item panel required property Item panel
deformScale: deformAmount / 10000 deformScale: (deformAmount * Config.appearance.deform.scale) / 10000
group: blobGroup group: blobGroup
implicitHeight: panel.height implicitHeight: panel.height
implicitWidth: panel.width implicitWidth: panel.width
+18
View File
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import ZShell.Models
Singleton {
id: root
property alias plugins: plugins.entries
FileSystemModel {
id: plugins
nameFilters: ["*.qml"]
path: Quickshell.env("HOME") + "/.config/zshell"
recursive: false
}
}
+17
View File
@@ -0,0 +1,17 @@
import Quickshell
import QtQuick
import ZShell.Models
import qs.Config
Repeater {
model: FetchPlugins.plugins
LazyLoader {
required property FileSystemEntry modelData
activeAsync: Config.plugins.entries.some(p => {
return p.id === modelData.baseName && p.enabled;
})
source: modelData.path
}
}
+20 -20
View File
@@ -16,27 +16,14 @@ Scope {
property bool launching: false property bool launching: false
property string promptMessage: "" property string promptMessage: ""
readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null readonly property var selectedSession: sessionIndex >= 0 ? sessions[sessionIndex] : null
readonly property var selectedUser: Users.selectedUser
property int sessionIndex: sessions.length > 0 ? 0 : -1 property int sessionIndex: sessions.length > 0 ? 0 : -1
property var sessions: [] property var sessions: []
readonly property string userFace: selectedUser ? selectedUser.face : ""
readonly property string username: Users.selectedUsername
// User handling - now uses the Users singleton // User handling - now uses the Users singleton
readonly property var users: Users.users readonly property var users: Users.users
readonly property var selectedUser: Users.selectedUser
readonly property string username: Users.selectedUsername
readonly property string userFace: selectedUser ? selectedUser.face : ""
// User selection functions (delegate to Users singleton)
function selectUser(username: string): bool {
return Users.selectUser(username);
}
function selectNextUser(): void {
Users.selectNext();
}
function selectPreviousUser(): void {
Users.selectPrevious();
}
signal flashMsg signal flashMsg
@@ -58,11 +45,11 @@ Scope {
event.accepted = true; event.accepted = true;
return; return;
} } else if (event.key === Qt.Key_Escape) {
buffer = "";
if (event.text && !/[\r\n]/.test(event.text)) { } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
// No illegal characters (you are insane if you use unicode in your password)
buffer += event.text; buffer += event.text;
event.accepted = true;
} }
} }
@@ -81,6 +68,19 @@ Scope {
Greetd.launch(selectedSession.command, [], true); Greetd.launch(selectedSession.command, [], true);
} }
function selectNextUser(): void {
Users.selectNext();
}
function selectPreviousUser(): void {
Users.selectPrevious();
}
// User selection functions (delegate to Users singleton)
function selectUser(username: string): bool {
return Users.selectUser(username);
}
function submit(): void { function submit(): void {
errorMessage = ""; errorMessage = "";
-1
View File
@@ -346,7 +346,6 @@ Singleton {
stdout: StdioCollector { stdout: StdioCollector {
onStreamFinished: { onStreamFinished: {
console.log("this is running");
if (root.gpuType === "GENERIC") { if (root.gpuType === "GENERIC") {
const percs = text.trim().split("\n"); const percs = text.trim().split("\n");
const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0); const sum = percs.reduce((acc, d) => acc + parseInt(d, 10), 0);
+20 -44
View File
@@ -7,23 +7,30 @@ import QtQuick
Singleton { Singleton {
id: root id: root
// The list of users that can log in graphically readonly property string defaultUserFile: "/etc/zshell-greeter/default-user"
// Each user object has: username, uid, home, shell, gecos (full name), face (avatar path) property int selectedIndex: 0
readonly property var selectedUser: selectedIndex >= 0 && selectedIndex < users.length ? users[selectedIndex] : null
readonly property string selectedUsername: selectedUser ? selectedUser.username : ""
property var users: [] property var users: []
// The currently selected user index function saveDefaultUser(): void {
property int selectedIndex: 0 if (selectedUser) {
defaultUserStorage.setText(selectedUser.username);
}
}
// The currently selected user object (or null if none) function selectNext(): void {
readonly property var selectedUser: selectedIndex >= 0 && selectedIndex < users.length ? users[selectedIndex] : null if (users.length === 0)
return;
selectedIndex = (selectedIndex + 1) % users.length;
}
// Convenience property for the selected username function selectPrevious(): void {
readonly property string selectedUsername: selectedUser ? selectedUser.username : "" if (users.length === 0)
return;
selectedIndex = (selectedIndex - 1 + users.length) % users.length;
}
// Path to store the default user preference
readonly property string defaultUserFile: "/etc/zshell-greeter/default-user"
// Select a user by username
function selectUser(username: string): bool { function selectUser(username: string): bool {
for (let i = 0; i < users.length; i++) { for (let i = 0; i < users.length; i++) {
if (users[i].username === username) { if (users[i].username === username) {
@@ -34,28 +41,6 @@ Singleton {
return false; return false;
} }
// Select the next user in the list (wraps around)
function selectNext(): void {
if (users.length === 0)
return;
selectedIndex = (selectedIndex + 1) % users.length;
}
// Select the previous user in the list (wraps around)
function selectPrevious(): void {
if (users.length === 0)
return;
selectedIndex = (selectedIndex - 1 + users.length) % users.length;
}
// Save the current user as the default for next login
function saveDefaultUser(): void {
if (selectedUser) {
defaultUserStorage.setText(selectedUser.username);
}
}
// Process to fetch the list of graphical users
Process { Process {
id: userLister id: userLister
@@ -67,13 +52,10 @@ Singleton {
try { try {
root.users = JSON.parse(text); root.users = JSON.parse(text);
// If we have users and no selection yet, try to select the default user
if (root.users.length > 0) { if (root.users.length > 0) {
// Try to load the default user
if (defaultUserStorage.loaded) { if (defaultUserStorage.loaded) {
const defaultUsername = defaultUserStorage.text().trim(); const defaultUsername = defaultUserStorage.text().trim();
if (defaultUsername && !root.selectUser(defaultUsername)) { if (defaultUsername && !root.selectUser(defaultUsername)) {
// Default user not found, select first user
root.selectedIndex = 0; root.selectedIndex = 0;
} }
} else { } else {
@@ -87,15 +69,14 @@ Singleton {
} }
} }
// FileView for persisting the default user
FileView { FileView {
id: defaultUserStorage id: defaultUserStorage
path: root.defaultUserFile path: root.defaultUserFile
preload: true preload: true
onLoadFailed: {}
onLoaded: { onLoaded: {
// If users are already loaded, try to select the default user
if (root.users.length > 0) { if (root.users.length > 0) {
const defaultUsername = text().trim(); const defaultUsername = text().trim();
if (defaultUsername) { if (defaultUsername) {
@@ -103,10 +84,5 @@ Singleton {
} }
} }
} }
onLoadFailed: {
// File doesn't exist yet, that's fine - we'll create it on first save
console.log("No default user file found, will use first user");
}
} }
} }
+63
View File
@@ -0,0 +1,63 @@
// FetchPresets.qml
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
property var parsedPresets: ({})
readonly property var presets: parsedPresets
property bool ready: false
function accents(presetName, variantName) {
const variant = parsedPresets[presetName]?.variants?.[variantName];
return variant?.accents ?? [];
}
function defaultAccent(presetName, variantName) {
const variant = parsedPresets[presetName]?.variants?.[variantName];
return variant?.default_accent ?? "";
}
function modes(presetName, variantName) {
const variant = parsedPresets[presetName]?.variants?.[variantName];
return variant?.modes ?? [];
}
function presetNames() {
return Object.keys(parsedPresets);
}
function variantNames(presetName) {
const preset = parsedPresets[presetName];
if (!preset || !preset.variants)
return [];
return Object.keys(preset.variants);
}
Process {
command: ["zshell-cli", "scheme", "list-presets", "--json"]
running: true
stdout: StdioCollector {
onStreamFinished: {
try {
const parsed = JSON.parse(text);
root.parsedPresets = parsed.presets ?? {};
root.ready = true;
} catch (e) {
console.error("Failed to parse presets JSON:", e);
}
}
}
}
}
+5 -5
View File
@@ -13,11 +13,11 @@ Singleton {
function setHyprConf(): void { function setHyprConf(): void {
Hypr.extras.applyOptions({ Hypr.extras.applyOptions({
"animations:enabled": 0, "animations.enabled": 0,
"decoration:shadow:enabled": 0, "decoration.shadow.enabled": 0,
"decoration:blur:enabled": 0, "decoration.blur.enabled": 0,
"general:border_size": 0, "general.border_size": 0,
"decoration:rounding": 0 "decoration.rounding": 0
}); });
} }
+1 -1
View File
@@ -12,7 +12,7 @@ Singleton {
readonly property int darkEnd: Config.general.color.scheduleDarkEnd readonly property int darkEnd: Config.general.color.scheduleDarkEnd
readonly property int darkStart: Config.general.color.scheduleDarkStart readonly property int darkStart: Config.general.color.scheduleDarkStart
readonly property bool enabled: Config.general.color.scheduleDark readonly property bool enabled: Config.general.color.scheduleDark && Config.general.color.schemeGeneration
function applyDarkMode() { function applyDarkMode() {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--mode", "dark"]);
+82 -41
View File
@@ -1,41 +1,82 @@
// pragma Singleton pragma Singleton
//
// import Quickshell import Quickshell
// import QtQuick import Quickshell.Io
// import QtQuick
// Singleton {
// id: root Singleton {
// id: root
// function start(extraArgs = []): void {
// needsStart = true; readonly property alias elapsed: props.elapsed
// startArgs = extraArgs; property bool needsPause
// checkProc.running = true; property bool needsStart
// } property bool needsStop
// readonly property alias paused: props.paused
// PersistentProperties { readonly property alias running: props.running
// id: props property list<string> startArgs
//
// property real elapsed: 0 function start(extraArgs = []): void {
// property bool paused: false needsStart = true;
// property bool running: false startArgs = extraArgs;
// checkProc.running = true;
// reloadableId: "recorder" }
// }
// function stop(): void {
// Process { needsStop = true;
// id: checkProc checkProc.running = true;
// }
// command: ["pidof", "gpu-screen-recorder"]
// running: true function togglePause(): void {
// needsPause = true;
// onExited: code => { checkProc.running = true;
// props.running = code === 0; }
//
// if (code === 0) { PersistentProperties {
// if (root.needsStop) { id: props
// Quickshell.execDetached(["zshell-cli"]);
// } property real elapsed: 0
// } property bool paused: false
// } property bool running: false
// }
// } reloadableId: "recorder"
}
Process {
id: checkProc
command: ["pidof", "gpu-screen-recorder"]
running: true
onExited: code => {
props.running = code === 0;
if (code === 0) {
if (root.needsStop) {
Quickshell.execDetached(["zshell-cli", "record", "record"]);
props.running = false;
props.paused = false;
} else if (root.needsPause) {
Quickshell.execDetached(["zshell-cli", "record", "record", "-p"]);
props.paused = !props.paused;
}
} else if (root.needsStart) {
Quickshell.execDetached(["zshell-cli", "record", "record", ...root.startArgs]);
props.running = true;
props.paused = false;
props.elapsed = 0;
}
root.needsStart = false;
root.needsStop = false;
root.needsPause = false;
}
}
Connections {
function onSecondsChanged(): void {
props.elapsed++;
}
target: Time // qmllint disable incompatible-type
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
import Quickshell
import "../scripts/fzf.js" as Fzf import "../scripts/fzf.js" as Fzf
import "../scripts/fuzzysort.js" as Fuzzy import "../scripts/fuzzysort.js" as Fuzzy
import QtQuick import QtQuick
import Quickshell
Singleton { Singleton {
property var extraOpts: ({}) property var extraOpts: ({})
+14
View File
@@ -6,7 +6,21 @@ import Quickshell.Services.UPower
Singleton { Singleton {
id: root id: root
readonly property real batteryPercent: UPower.displayDevice.percentage
readonly property list<UPowerDevice> devices: UPower.devices.values readonly property list<UPowerDevice> devices: UPower.devices.values
readonly property UPowerDevice displayDevice: UPower.displayDevice readonly property UPowerDevice displayDevice: UPower.displayDevice
readonly property bool onBattery: UPower.onBattery readonly property bool onBattery: UPower.onBattery
// property bool toastShown
//
// Connections {
// target: UPower
//
// function onPercentageChanged(): {
// if (root.batteryPercent >= 0.2 && toastShown)
// return;
//
// root.toastShown = true;
// Toaster.toast(qsTr("Battery "))
// }
// }
} }
+51
View File
@@ -1,7 +1,9 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import QtQuick
import ZShell.Models import ZShell.Models
import qs.Config import qs.Config
import qs.Modules import qs.Modules
@@ -12,10 +14,17 @@ Searcher {
id: root id: root
property string actualCurrent: WallpaperPath.currentWallpaperPath property string actualCurrent: WallpaperPath.currentWallpaperPath
property alias crops: adapter.monitorCrops
readonly property string current: showPreview ? previewPath : actualCurrent readonly property string current: showPreview ? previewPath : actualCurrent
property alias monitorCrops: monitorCrops
property string previewPath property string previewPath
property bool recentlyChanged
property bool showPreview: false property bool showPreview: false
function getCrop(screen: string): var {
return root.crops[screen];
}
function preview(path: string): void { function preview(path: string): void {
previewPath = path; previewPath = path;
if (Config.general.color.schemeGeneration) if (Config.general.color.schemeGeneration)
@@ -23,9 +32,35 @@ Searcher {
showPreview = true; showPreview = true;
} }
function setCrop(screen: string, rect: rect, scaledRect: rect, zoom: real): void {
let updated = Object.assign({}, root.crops);
if (zoom <= 0)
zoom = 1.0;
else if (zoom > 5.0)
zoom = 5.0;
updated[screen] = {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
scaledX: scaledRect.x,
scaledY: scaledRect.y,
scaledWidth: scaledRect.width,
scaledHeight: scaledRect.height,
zoom: zoom
};
root.crops = updated;
monitorCrops.writeAdapter();
monitorCrops.reload();
}
function setWallpaper(path: string): void { function setWallpaper(path: string): void {
actualCurrent = path; actualCurrent = path;
WallpaperPath.currentWallpaperPath = path; WallpaperPath.currentWallpaperPath = path;
Quickshell.screens.forEach(n => setCrop(n.name, Qt.rect(0, 0, 0, 0), Qt.rect(0, 0, 0, 0), 1.0));
Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]); Quickshell.execDetached(["zshell-cli", "wallpaper", "lockscreen", "--input-image", `${root.actualCurrent}`, "--output-path", `${Paths.state}/lockscreen_bg.png`, "--blur-amount", `${Config.lock.blurAmount}`]);
if (Config.general.color.schemeGeneration) if (Config.general.color.schemeGeneration)
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]); Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--image-path", `${root.actualCurrent}`, "--scheme", `${Config.colors.schemeType}`, "--mode", `${Config.general.color.mode}`]);
@@ -52,6 +87,22 @@ Searcher {
target: "wallpaper" target: "wallpaper"
} }
FileView {
id: monitorCrops
path: `${Paths.state}/wallpaper-crops.json`
watchChanges: true
onAdapterUpdated: writeAdapter()
onFileChanged: reload()
JsonAdapter {
id: adapter
property var monitorCrops: ({})
}
}
FileSystemModel { FileSystemModel {
id: wallpapers id: wallpapers
-31
View File
@@ -1,31 +0,0 @@
import Quickshell
import QtQuick
import qs.Config
import qs.Helpers
import qs.Components
CustomRect {
id: root
required property PersistentProperties visibilities
anchors.bottom: parent.bottom
anchors.bottomMargin: 6
anchors.top: parent.top
anchors.topMargin: 6
color: DynamicColors.tPalette.m3surfaceContainer
implicitWidth: 40
radius: Appearance.rounding.full
StateLayer {
onClicked: {
root.visibilities.dashboard = !root.visibilities.dashboard;
}
}
MaterialIcon {
anchors.centerIn: parent
color: DynamicColors.palette.m3onSurface
text: "widgets"
}
}
+25 -77
View File
@@ -12,90 +12,29 @@ Item {
required property Canvas drawing required property Canvas drawing
property bool expanded: true property bool expanded: true
property real offsetScale: shouldBeActive ? 0 : 1
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.isDrawing readonly property bool shouldBeActive: visibilities.isDrawing
required property var visibilities required property var visibilities
anchors.leftMargin: (-implicitWidth - 5) * offsetScale
implicitHeight: content.implicitHeight implicitHeight: content.implicitHeight
implicitWidth: 0 implicitWidth: root.expanded ? content.implicitWidth : icon.implicitWidth
visible: width > 0 opacity: 1 - offsetScale
visible: offsetScale < 1
states: [ Behavior on implicitWidth {
State { Anim {
name: "hidden" duration: Appearance.anim.durations.expressiveDefaultSpatial
when: !root.shouldBeActive easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
PropertyChanges {
root.implicitWidth: 0
}
PropertyChanges {
icon.opacity: 0
}
PropertyChanges {
content.opacity: 0
}
},
State {
name: "collapsed"
when: root.shouldBeActive && !root.expanded
PropertyChanges {
root.implicitWidth: icon.implicitWidth
}
PropertyChanges {
icon.opacity: 1
}
PropertyChanges {
content.opacity: 0
}
},
State {
name: "visible"
when: root.shouldBeActive && root.expanded
PropertyChanges {
root.implicitWidth: content.implicitWidth
}
PropertyChanges {
icon.opacity: 0
}
PropertyChanges {
content.opacity: 1
}
} }
] }
transitions: [ Behavior on offsetScale {
Transition { Anim {
from: "*" duration: Appearance.anim.durations.expressiveDefaultSpatial
to: "*" easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
ParallelAnimation {
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitWidth"
target: root
}
Anim {
duration: Appearance.anim.durations.small
property: "opacity"
target: icon
}
Anim {
duration: Appearance.anim.durations.small
property: "opacity"
target: content
}
}
} }
] }
onVisibleChanged: { onVisibleChanged: {
if (!visible) if (!visible)
@@ -109,8 +48,12 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
height: content.contentItem.height height: content.contentItem.height
opacity: 1 opacity: root.expanded ? 0 : 1
Behavior on opacity {
Anim {
}
}
sourceComponent: MaterialIcon { sourceComponent: MaterialIcon {
font.pointSize: Appearance.font.size.larger font.pointSize: Appearance.font.size.larger
text: "arrow_forward_ios" text: "arrow_forward_ios"
@@ -122,7 +65,12 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
opacity: root.expanded ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: Content { sourceComponent: Content {
drawing: root.drawing drawing: root.drawing
visibilities: root.visibilities visibilities: root.visibilities
-1
View File
@@ -13,7 +13,6 @@ Searcher {
function launch(entry: DesktopEntry): void { function launch(entry: DesktopEntry): void {
appDb.incrementFrequency(entry.id); appDb.incrementFrequency(entry.id);
console.log(root.command);
if (entry.runInTerminal) if (entry.runInTerminal)
Quickshell.execDetached({ Quickshell.execDetached({
+3
View File
@@ -58,6 +58,7 @@ CustomRect {
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: Config.notifs.sizes.image height: Config.notifs.sizes.image
source: Qt.resolvedUrl(root.image) source: Qt.resolvedUrl(root.image)
visible: Config.lock.showNotifIcon
width: Config.notifs.sizes.image width: Config.notifs.sizes.image
} }
} }
@@ -284,6 +285,8 @@ CustomRect {
Layout.fillWidth: true Layout.fillWidth: true
color: root.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface color: root.urgency === "critical" ? DynamicColors.palette.m3onSecondaryContainer : DynamicColors.palette.m3onSurface
text: { text: {
if (!Config.lock.showNotifContent)
return "Unlock to view";
const summary = modelData.summary.replace(/\n/g, " "); const summary = modelData.summary.replace(/\n/g, " ");
const body = modelData.body.replace(/\n/g, " "); const body = modelData.body.replace(/\n/g, " ");
const color = root.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline; const color = root.urgency === "critical" ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3outline;
+2
View File
@@ -30,6 +30,8 @@ Scope {
} else { } else {
buffer = buffer.slice(0, -1); buffer = buffer.slice(0, -1);
} }
} else if (event.key === Qt.Key_Escape) {
buffer = "";
} else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) { } else if (" abcdefghijklmnopqrstuvwxyz1234567890`~!@#$%^&*()-_=+[{]}\\|;:'\",<.>/?".includes(event.text.toLowerCase())) {
// No illegal characters (you are insane if you use unicode in your password) // No illegal characters (you are insane if you use unicode in your password)
buffer += event.text; buffer += event.text;
+4 -1
View File
@@ -136,7 +136,10 @@ CustomRect {
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
onLinkActivated: link => { onLinkActivated: link => {
Quickshell.execDetached(["app2unit", "-O", "--", link]); if (Config.launcher.uwsm)
Quickshell.execDetached(["app2unit", "-O", "--", link]);
else
Quickshell.execDetached(["xdg-open", link]);
root.visibilities.sidebar = false; root.visibilities.sidebar = false;
} }
} }
@@ -0,0 +1,290 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Components
import qs.Config
import qs.Helpers
CustomRect {
id: root
required property var props
required property PersistentProperties visibilities
Layout.fillWidth: true
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.implicitHeight + layout.anchors.margins * 2
radius: Appearance.rounding.smallest
ColumnLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.large
spacing: Appearance.spacing.normal
RowLayout {
spacing: Appearance.spacing.normal
z: 1
CustomRect {
color: Recorder.running ? DynamicColors.palette.m3secondary : DynamicColors.palette.m3secondaryContainer
implicitHeight: {
const h = icon.implicitHeight + Appearance.padding.smaller * 2;
return h - (h % 2);
}
implicitWidth: implicitHeight
radius: Appearance.rounding.full
MaterialIcon {
id: icon
anchors.centerIn: parent
anchors.horizontalCenterOffset: -0.5
anchors.verticalCenterOffset: 1.5
color: Recorder.running ? DynamicColors.palette.m3onSecondary : DynamicColors.palette.m3onSecondaryContainer
font.pointSize: Appearance.font.size.large
text: "screen_record"
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
CustomText {
Layout.fillWidth: true
elide: Text.ElideRight
font.pointSize: Appearance.font.size.normal
text: qsTr("Screen Recorder")
}
CustomText {
Layout.fillWidth: true
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
font.pointSize: Appearance.font.size.small
text: Recorder.paused ? qsTr("Recording paused") : Recorder.running ? qsTr("Recording running") : qsTr("Recording off")
}
}
CustomSplitButton {
active: menuItems.find(m => root.props.recordingMode === m.icon + m.text) ?? menuItems[0]
disabled: Recorder.running
menuItems: [
MenuItem {
activeText: qsTr("Fullscreen")
icon: "fullscreen"
text: qsTr("Record fullscreen")
onClicked: Recorder.start()
},
MenuItem {
activeText: qsTr("Region")
icon: "screenshot_region"
text: qsTr("Record region")
onClicked: Recorder.start(["-r"])
},
MenuItem {
activeText: qsTr("Fullscreen")
icon: "select_to_speak"
text: qsTr("Record fullscreen with sound")
onClicked: Recorder.start(["-s"])
},
MenuItem {
activeText: qsTr("Region")
icon: "volume_up"
text: qsTr("Record region with sound")
onClicked: Recorder.start(["-s", "-r"])
}
]
menu.onItemSelected: item => root.props.recordingMode = item.icon + item.text
}
}
Loader {
id: listOrControls
property bool running: Recorder.running
Layout.fillWidth: true
Layout.preferredHeight: implicitHeight
asynchronous: true
sourceComponent: running ? recordingControls : recordingList
Behavior on Layout.preferredHeight {
id: locHeightAnim
enabled: false
Anim {
}
}
Behavior on running {
SequentialAnimation {
ParallelAnimation {
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardAccel
property: "scale"
target: listOrControls
to: 0.7
}
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardAccel
property: "opacity"
target: listOrControls
to: 0
}
}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: true
}
PropertyAction {
}
PropertyAction {
property: "enabled"
target: locHeightAnim
value: false
}
ParallelAnimation {
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardDecel
property: "scale"
target: listOrControls
to: 1
}
Anim {
duration: Appearance.anim.durations.small
easing: Appearance.anim.curves.standardDecel
property: "opacity"
target: listOrControls
to: 1
}
}
}
}
}
}
Component {
id: recordingList
RecordingList {
props: root.props
visibilities: root.visibilities
}
}
Component {
id: recordingControls
RowLayout {
spacing: Appearance.spacing.normal
CustomRect {
color: Recorder.paused ? DynamicColors.palette.m3tertiary : DynamicColors.palette.m3error
implicitHeight: recText.implicitHeight + Appearance.padding.smaller * 2
implicitWidth: recText.implicitWidth + Appearance.padding.normal * 2
radius: Appearance.rounding.full
Behavior on implicitWidth {
Anim {
}
}
SequentialAnimation on opacity {
alwaysRunToEnd: true
loops: Animation.Infinite
running: !Recorder.paused
Anim {
duration: Appearance.anim.durations.large
easing: Appearance.anim.curves.emphasizedAccel
from: 1
to: 0
}
Anim {
duration: Appearance.anim.durations.extraLarge
easing: Appearance.anim.curves.emphasizedDecel
from: 0
to: 1
}
}
CustomText {
id: recText
anchors.centerIn: parent
animate: true
color: Recorder.paused ? DynamicColors.palette.m3onTertiary : DynamicColors.palette.m3onError
font.family: Appearance.font.family.mono
text: Recorder.paused ? "PAUSED" : "REC"
}
}
CustomText {
font.pointSize: Appearance.font.size.normal
text: {
const elapsed = Recorder.elapsed;
const hours = Math.floor(elapsed / 3600);
const mins = Math.floor((elapsed % 3600) / 60);
const secs = Math.floor(elapsed % 60).toString().padStart(2, "0");
let time;
if (hours > 0)
time = `${hours}:${mins.toString().padStart(2, "0")}:${secs}`;
else
time = `${mins}:${secs}`;
return qsTr("Recording for %1").arg(time);
}
}
Item {
Layout.fillWidth: true
}
IconButton {
checked: Recorder.paused
font.pointSize: Appearance.font.size.large
icon: Recorder.paused ? "play_arrow" : "pause"
label.animate: true
toggle: true
type: IconButton.Tonal
onClicked: {
Recorder.togglePause();
internalChecked = Recorder.paused;
}
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "stop"
inactiveColour: DynamicColors.palette.m3error
inactiveOnColour: DynamicColors.palette.m3onError
onClicked: Recorder.stop()
}
}
}
}
@@ -0,0 +1,226 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import ZShell.Models
import qs.Components
import qs.Helpers
import qs.Paths
import qs.Config
ColumnLayout {
id: root
required property var props
required property PersistentProperties visibilities
spacing: 0
WrapperMouseArea {
Layout.fillWidth: true
cursorShape: Qt.PointingHandCursor
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignVCenter
font.pointSize: Appearance.font.size.large
text: "list"
}
CustomText {
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
font.pointSize: Appearance.font.size.normal
text: qsTr("Recordings")
}
IconButton {
icon: root.props.recordingListExpanded ? "unfold_less" : "unfold_more"
label.animate: true
type: IconButton.Text
onClicked: root.props.recordingListExpanded = !root.props.recordingListExpanded
}
}
}
CustomListView {
id: list
Layout.fillWidth: true
Layout.rightMargin: -Appearance.spacing.small
clip: true
implicitHeight: (Appearance.font.size.larger + Appearance.padding.small) * (root.props.recordingListExpanded ? 10 : 3)
CustomScrollBar.vertical: CustomScrollBar {
flickable: list
}
add: Transition {
Anim {
from: 0
property: "opacity"
to: 1
}
Anim {
from: 0.5
property: "scale"
to: 1
}
}
delegate: RowLayout {
id: recording
property string baseName
required property FileSystemEntry modelData
anchors.left: list.contentItem.left
anchors.right: list.contentItem.right
anchors.rightMargin: Appearance.spacing.small
spacing: Appearance.spacing.small / 2
Component.onCompleted: baseName = modelData.baseName
CustomText {
Layout.fillWidth: true
Layout.rightMargin: Appearance.spacing.small / 2
color: DynamicColors.palette.m3onSurfaceVariant
elide: Text.ElideRight
text: {
const time = recording.baseName;
const matches = time.match(/^recording_(\d{4})(\d{2})(\d{2})_(\d{2})-(\d{2})-(\d{2})/);
if (!matches)
return time;
const date = new Date(...matches.slice(1));
date.setMonth(date.getMonth() - 1);
return qsTr("Recording at %1").arg(Qt.formatDateTime(date, Qt.locale()));
}
}
IconButton {
icon: "play_arrow"
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.playback, recording.modelData.path]);
}
}
IconButton {
icon: "folder"
type: IconButton.Text
onClicked: {
root.visibilities.sidebar = false;
Quickshell.execDetached(["app2unit", "--", ...Config.general.apps.explorer, recording.modelData.path]);
}
}
}
displaced: Transition {
Anim {
properties: "opacity,scale"
to: 1
}
Anim {
property: "y"
}
}
Behavior on implicitHeight {
Anim {
}
}
model: FileSystemModel {
nameFilters: ["recording_*.mp4"]
path: Paths.recsdir
sortReverse: true
}
remove: Transition {
Anim {
property: "opacity"
to: 0
}
Anim {
property: "scale"
to: 0.5
}
}
Loader {
active: opacity > 0
anchors.centerIn: parent
asynchronous: true
opacity: list.count === 0 ? 1 : 0
Behavior on opacity {
Anim {
}
}
sourceComponent: ColumnLayout {
spacing: Appearance.spacing.small
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredHeight: root.props.recordingListExpanded ? implicitHeight : 0
color: DynamicColors.palette.m3outline
font.pointSize: Appearance.font.size.extraLarge
opacity: root.props.recordingListExpanded ? 1 : 0
scale: root.props.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredHeight {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
}
RowLayout {
spacing: Appearance.spacing.smaller
MaterialIcon {
Layout.alignment: Qt.AlignHCenter
Layout.preferredWidth: !root.props.recordingListExpanded ? implicitWidth : 0
color: DynamicColors.palette.m3outline
opacity: !root.props.recordingListExpanded ? 1 : 0
scale: !root.props.recordingListExpanded ? 1 : 0
text: "scan_delete"
Behavior on Layout.preferredWidth {
Anim {
}
}
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
}
CustomText {
color: DynamicColors.palette.m3outline
text: qsTr("No recordings found")
}
}
}
}
}
}
@@ -1,13 +1,14 @@
import qs.Modules.Notifications.Sidebar.Utils.Cards import Quickshell
import qs.Config
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules.Notifications.Sidebar.Utils.Cards
import qs.Config
Item { Item {
id: root id: root
required property Item popouts required property Item popouts
required property var props required property PersistentProperties props
required property var visibilities required property var visibilities
implicitHeight: layout.implicitHeight implicitHeight: layout.implicitHeight
@@ -22,6 +23,12 @@ Item {
IdleInhibit { IdleInhibit {
} }
Record {
props: root.props
visibilities: root.visibilities
z: 1
}
Toggles { Toggles {
popouts: root.popouts popouts: root.popouts
visibilities: root.visibilities visibilities: root.visibilities
+8 -6
View File
@@ -100,12 +100,14 @@ Item {
icon: `brightness_${(Math.round(value * 6) + 1)}` icon: `brightness_${(Math.round(value * 6) + 1)}`
value: root.brightness value: root.brightness
onMoved: { onPressedChanged: {
if (Config.osd.allMonBrightness) { if (!pressed) {
root.monitor?.setBrightness(value); if (Config.osd.allMonBrightness) {
} else { for (const mon of Brightness.monitors) {
for (const mon of Brightness.monitors) { mon.setBrightness(value);
mon.setBrightness(value); }
} else {
root.monitor?.setBrightness(value);
} }
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ CustomRect {
color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer color: visibilities.resources ? DynamicColors.palette.m3primary : DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2 implicitWidth: rowLayout.implicitWidth + Appearance.padding.normal * 2
radius: height / 2 radius: Appearance.rounding.full
StateLayer { StateLayer {
onClicked: root.visibilities.resources = !root.visibilities.resources onClicked: root.visibilities.resources = !root.visibilities.resources
+6
View File
@@ -116,6 +116,12 @@ Item {
key: "updates" key: "updates"
name: "Updates" name: "Updates"
} }
ListElement {
icon: "extension"
key: "plugins"
name: "Extensions"
}
} }
CustomClippingRect { CustomClippingRect {
@@ -1,3 +1,4 @@
import QtQuick
import qs.Modules.Settings.Controls import qs.Modules.Settings.Controls
import qs.Config import qs.Config
@@ -80,6 +81,7 @@ SettingsPage {
name: "Sans family" name: "Sans family"
object: Config.appearance.font.family object: Config.appearance.font.family
setting: "sans" setting: "sans"
stringList: Qt.fontFamilies()
} }
Separator { Separator {
@@ -89,6 +91,7 @@ SettingsPage {
name: "Monospace family" name: "Monospace family"
object: Config.appearance.font.family object: Config.appearance.font.family
setting: "mono" setting: "mono"
stringList: Qt.fontFamilies()
} }
} }
+8 -8
View File
@@ -1,3 +1,4 @@
import Quickshell
import QtQuick.Layouts import QtQuick.Layouts
import qs.Modules.Settings.Controls import qs.Modules.Settings.Controls
import qs.Config import qs.Config
@@ -5,6 +6,8 @@ import qs.Config
SettingsPage { SettingsPage {
id: root id: root
required property ShellScreen screen
SettingsSection { SettingsSection {
sectionId: "Wallpaper" sectionId: "Wallpaper"
@@ -29,20 +32,17 @@ SettingsPage {
step: 50 step: 50
} }
// Separator { Separator {
// } }
//
// WallpaperCropper { WallpaperCropper {
// Layout.fillWidth: true }
// Layout.preferredHeight: 300
// }
} }
SettingsSection { SettingsSection {
sectionId: "Wallpapers" sectionId: "Wallpapers"
WallpaperGrid { WallpaperGrid {
Layout.fillWidth: true
} }
} }
} }
+16 -6
View File
@@ -19,8 +19,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Height"
min: 1 min: 1
name: "Height"
object: Config.barConfig object: Config.barConfig
setting: "height" setting: "height"
} }
@@ -29,8 +29,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Rounding"
min: 0 min: 0
name: "Rounding"
object: Config.barConfig object: Config.barConfig
setting: "rounding" setting: "rounding"
} }
@@ -39,11 +39,21 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Border"
min: 0 min: 0
name: "Border"
object: Config.barConfig object: Config.barConfig
setting: "border" setting: "border"
} }
Separator {
}
SettingSpinBox {
min: 0
name: "Smoothing"
object: Config.barConfig
setting: "smoothing"
}
} }
SettingsSection { SettingsSection {
@@ -145,8 +155,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Dock height"
min: 1 min: 1
name: "Dock height"
object: Config.dock object: Config.dock
setting: "height" setting: "height"
} }
@@ -173,8 +183,8 @@ SettingsPage {
} }
SettingStringList { SettingStringList {
name: "Pinned apps"
addLabel: qsTr("Add pinned app") addLabel: qsTr("Add pinned app")
name: "Pinned apps"
object: Config.dock object: Config.dock
setting: "pinnedApps" setting: "pinnedApps"
} }
@@ -183,8 +193,8 @@ SettingsPage {
} }
SettingStringList { SettingStringList {
name: "Ignored app regexes"
addLabel: qsTr("Add ignored regex") addLabel: qsTr("Add ignored regex")
name: "Ignored app regexes"
object: Config.dock object: Config.dock
setting: "ignoredAppRegexes" setting: "ignoredAppRegexes"
} }
+111 -111
View File
@@ -19,8 +19,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Media update interval"
min: 0 min: 0
name: "Media update interval"
object: Config.dashboard object: Config.dashboard
setting: "mediaUpdateInterval" setting: "mediaUpdateInterval"
step: 50 step: 50
@@ -30,8 +30,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Resource update interval"
min: 0 min: 0
name: "Resource update interval"
object: Config.dashboard object: Config.dashboard
setting: "resourceUpdateInterval" setting: "resourceUpdateInterval"
step: 50 step: 50
@@ -41,8 +41,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Drag threshold"
min: 0 min: 0
name: "Drag threshold"
object: Config.dashboard object: Config.dashboard
setting: "dragThreshold" setting: "dragThreshold"
} }
@@ -107,112 +107,112 @@ SettingsPage {
} }
} }
SettingsSection { // SettingsSection {
sectionId: "Layout Sizes" // sectionId: "Layout Sizes"
//
SettingsHeader { // SettingsHeader {
name: "Layout Sizes" // name: "Layout Sizes"
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Tab indicator height" // name: "Tab indicator height"
value: String(Config.dashboard.sizes.tabIndicatorHeight) // value: String(Config.dashboard.sizes.tabIndicatorHeight)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Tab indicator spacing" // name: "Tab indicator spacing"
value: String(Config.dashboard.sizes.tabIndicatorSpacing) // value: String(Config.dashboard.sizes.tabIndicatorSpacing)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Info width" // name: "Info width"
value: String(Config.dashboard.sizes.infoWidth) // value: String(Config.dashboard.sizes.infoWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Info icon size" // name: "Info icon size"
value: String(Config.dashboard.sizes.infoIconSize) // value: String(Config.dashboard.sizes.infoIconSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Date time width" // name: "Date time width"
value: String(Config.dashboard.sizes.dateTimeWidth) // value: String(Config.dashboard.sizes.dateTimeWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media width" // name: "Media width"
value: String(Config.dashboard.sizes.mediaWidth) // value: String(Config.dashboard.sizes.mediaWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media progress sweep" // name: "Media progress sweep"
value: String(Config.dashboard.sizes.mediaProgressSweep) // value: String(Config.dashboard.sizes.mediaProgressSweep)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media progress thickness" // name: "Media progress thickness"
value: String(Config.dashboard.sizes.mediaProgressThickness) // value: String(Config.dashboard.sizes.mediaProgressThickness)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Resource progress thickness" // name: "Resource progress thickness"
value: String(Config.dashboard.sizes.resourceProgessThickness) // value: String(Config.dashboard.sizes.resourceProgessThickness)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Weather width" // name: "Weather width"
value: String(Config.dashboard.sizes.weatherWidth) // value: String(Config.dashboard.sizes.weatherWidth)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media cover art size" // name: "Media cover art size"
value: String(Config.dashboard.sizes.mediaCoverArtSize) // value: String(Config.dashboard.sizes.mediaCoverArtSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Media visualiser size" // name: "Media visualiser size"
value: String(Config.dashboard.sizes.mediaVisualiserSize) // value: String(Config.dashboard.sizes.mediaVisualiserSize)
} // }
//
Separator { // Separator {
} // }
//
SettingReadOnly { // SettingReadOnly {
name: "Resource size" // name: "Resource size"
value: String(Config.dashboard.sizes.resourceSize) // value: String(Config.dashboard.sizes.resourceSize)
} // }
} // }
} }
+52
View File
@@ -1,4 +1,6 @@
import Quickshell import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Modules.Settings.Controls import qs.Modules.Settings.Controls
import qs.Config import qs.Config
import qs.Components import qs.Components
@@ -67,6 +69,7 @@ SettingsPage {
CustomSplitButtonRow { CustomSplitButtonRow {
active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1] active: Config.general.color.mode === "light" ? menuItems[0] : menuItems[1]
buttonAlias.disabled: !Config.general.color.schemeGeneration
label: qsTr("Scheme mode") label: qsTr("Scheme mode")
menuItems: [ menuItems: [
@@ -100,6 +103,7 @@ SettingsPage {
id: schemeType id: schemeType
active: root.schemeTypeItem(menuItems, Config.colors.schemeType) active: root.schemeTypeItem(menuItems, Config.colors.schemeType)
buttonAlias.disabled: !Config.general.color.schemeGeneration
label: qsTr("Scheme type") label: qsTr("Scheme type")
z: 2 z: 2
@@ -169,21 +173,69 @@ SettingsPage {
} }
Separator { Separator {
shouldBeActive: Config.general.color.schemeGeneration ? 0 : 1
}
SchemesListView {
name: "Color scheme presets"
object: Config.colors.presets
setting: "name"
shouldBeActive: Config.general.color.schemeGeneration ? 0 : 1
stringList: FetchPresets.presetNames()
}
Separator {
shouldBeActive: Config.colors.presets.name !== "" && !Config.general.color.schemeGeneration
}
SchemesListView {
name: "Preset variant"
object: Config.colors.presets
setting: "variant"
shouldBeActive: Config.colors.presets.name !== "" && !Config.general.color.schemeGeneration
stringList: FetchPresets.variantNames(Config.colors.presets.name)
onOptionSet: item => {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${Config.colors.presets.name.toLowerCase()}:${item}`]);
}
}
Separator {
shouldBeActive: Config.colors.presets.variant !== "" && FetchPresets.accents(Config.colors.presets.name, Config.colors.presets.variant).length > 0 && !Config.general.color.schemeGeneration
}
SchemesListView {
name: "Preset accent"
object: Config.colors.presets
setting: "accent"
shouldBeActive: Config.colors.presets.variant !== "" && FetchPresets.accents(Config.colors.presets.name, Config.colors.presets.variant).length > 0 && !Config.general.color.schemeGeneration
stringList: FetchPresets.accents(Config.colors.presets.name, Config.colors.presets.variant)
onOptionSet: item => {
Quickshell.execDetached(["zshell-cli", "scheme", "generate", "--preset", `${Config.colors.presets.name.toLowerCase()}:${Config.colors.presets.variant}`, "--accent", `${item}`]);
}
}
Separator {
shouldBeActive: Config.general.color.schemeGeneration ? 1 : 0
} }
SettingSwitch { SettingSwitch {
name: "Smart color scheme" name: "Smart color scheme"
object: Config.general.color object: Config.general.color
setting: "smart" setting: "smart"
shouldBeActive: Config.general.color.schemeGeneration ? 1 : 0
} }
Separator { Separator {
shouldBeActive: Config.general.color.schemeGeneration ? 1 : 0
} }
SettingSpinner { SettingSpinner {
name: "Schedule dark mode" name: "Schedule dark mode"
object: Config.general.color object: Config.general.color
settings: ["scheduleDarkStart", "scheduleDarkEnd", "scheduleDark"] settings: ["scheduleDarkStart", "scheduleDarkEnd", "scheduleDark"]
shouldBeActive: Config.general.color.schemeGeneration ? 1 : 0
} }
Separator { Separator {
+35 -5
View File
@@ -31,8 +31,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Max fingerprint tries"
min: 1 min: 1
name: "Max fingerprint tries"
object: Config.lock object: Config.lock
setting: "maxFprintTries" setting: "maxFprintTries"
step: 1 step: 1
@@ -41,9 +41,27 @@ SettingsPage {
Separator { Separator {
} }
SettingSwitch {
name: "Show notification details"
object: Config.lock
setting: "showNotifContent"
}
Separator {
}
SettingSwitch {
name: "Show notification icon"
object: Config.lock
setting: "showNotifIcon"
}
Separator {
}
SettingSpinBox { SettingSpinBox {
name: "Blur amount"
min: 0 min: 0
name: "Blur amount"
object: Config.lock object: Config.lock
setting: "blurAmount" setting: "blurAmount"
step: 1 step: 1
@@ -53,9 +71,9 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Height multiplier"
max: 2 max: 2
min: 0.1 min: 0.1
name: "Height multiplier"
object: Config.lock.sizes object: Config.lock.sizes
setting: "heightMult" setting: "heightMult"
step: 0.05 step: 0.05
@@ -65,9 +83,9 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Aspect ratio"
max: 4 max: 4
min: 0.5 min: 0.5
name: "Aspect ratio"
object: Config.lock.sizes object: Config.lock.sizes
setting: "ratio" setting: "ratio"
step: 0.05 step: 0.05
@@ -77,14 +95,26 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Center width"
min: 100 min: 100
name: "Center width"
object: Config.lock.sizes object: Config.lock.sizes
setting: "centerWidth" setting: "centerWidth"
step: 10 step: 10
} }
} }
SettingsSection {
sectionId: "Greeter"
SettingsHeader {
name: "Greeter"
}
SettingsIconButton {
name: "Install wallpaper and color scheme to greeter"
}
}
SettingsSection { SettingsSection {
sectionId: "Idle" sectionId: "Idle"
@@ -9,6 +9,8 @@ import qs.Modules.Settings.Controls
ColumnLayout { ColumnLayout {
id: root id: root
property bool shouldBeActive: true
function addTimeoutEntry() { function addTimeoutEntry() {
let list = [...Config.general.idle.timeouts]; let list = [...Config.general.idle.timeouts];
@@ -40,8 +42,26 @@ ColumnLayout {
Config.save(); Config.save();
} }
Layout.fillWidth: true anchors.left: parent.left
anchors.right: parent.right
height: shouldBeActive ? implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
spacing: Appearance.spacing.smaller spacing: Appearance.spacing.smaller
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Settings { Settings {
name: "Idle Monitors" name: "Idle Monitors"
@@ -52,6 +72,8 @@ ColumnLayout {
SettingList { SettingList {
Layout.fillWidth: true Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
onAddActiveActionRequested: { onAddActiveActionRequested: {
root.updateTimeoutEntry(index, "activeAction", ""); root.updateTimeoutEntry(index, "activeAction", "");
+18
View File
@@ -0,0 +1,18 @@
import qs.Modules.Settings.Controls
import qs.Config
SettingsPage {
SettingsSection {
sectionId: "Plugins"
SettingsHeader {
name: "Plugins"
}
SettingBarEntryList {
name: "Enable or disable plugins"
object: Config.plugins
setting: "entries"
}
}
}
+97 -97
View File
@@ -19,8 +19,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Max toasts"
min: 1 min: 1
name: "Max toasts"
object: Config.utilities object: Config.utilities
setting: "maxToasts" setting: "maxToasts"
} }
@@ -29,8 +29,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Panel width"
min: 1 min: 1
name: "Panel width"
object: Config.utilities.sizes object: Config.utilities.sizes
setting: "width" setting: "width"
} }
@@ -39,8 +39,8 @@ SettingsPage {
} }
SettingSpinBox { SettingSpinBox {
name: "Toast width"
min: 1 min: 1
name: "Toast width"
object: Config.utilities.sizes object: Config.utilities.sizes
setting: "toastWidth" setting: "toastWidth"
} }
@@ -77,100 +77,100 @@ SettingsPage {
setting: "gameModeChanged" setting: "gameModeChanged"
} }
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Do not disturb changed" // name: "Do not disturb changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "dndChanged" // setting: "dndChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Audio output changed" // name: "Audio output changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "audioOutputChanged" // setting: "audioOutputChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Audio input changed" // name: "Audio input changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "audioInputChanged" // setting: "audioInputChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Caps lock changed" // name: "Caps lock changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "capsLockChanged" // setting: "capsLockChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Num lock changed" // name: "Num lock changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "numLockChanged" // setting: "numLockChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Keyboard layout changed" // name: "Keyboard layout changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "kbLayoutChanged" // setting: "kbLayoutChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "VPN changed" // name: "VPN changed"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "vpnChanged" // setting: "vpnChanged"
} // }
//
Separator { // Separator {
} // }
//
SettingSwitch { // SettingSwitch {
name: "Now playing" // name: "Now playing"
object: Config.utilities.toasts // object: Config.utilities.toasts
setting: "nowPlaying" // setting: "nowPlaying"
} // }
} }
SettingsSection { // SettingsSection {
sectionId: "VPN" // sectionId: "VPN"
//
SettingsHeader { // SettingsHeader {
name: "VPN" // name: "VPN"
} // }
//
SettingSwitch { // SettingSwitch {
name: "Enable VPN integration" // name: "Enable VPN integration"
object: Config.utilities.vpn // object: Config.utilities.vpn
setting: "enabled" // setting: "enabled"
} // }
//
Separator { // Separator {
} // }
//
SettingStringList { // SettingStringList {
name: "Provider" // name: "Provider"
addLabel: qsTr("Add VPN provider") // addLabel: qsTr("Add VPN provider")
object: Config.utilities.vpn // object: Config.utilities.vpn
setting: "provider" // setting: "provider"
} // }
} // }
} }
+13 -2
View File
@@ -1,3 +1,5 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Widgets import Quickshell.Widgets
import QtQuick import QtQuick
@@ -20,7 +22,6 @@ Item {
required property PersistentProperties visibilities required property PersistentProperties visibilities
function scrollToSetting(section: string, settingName: string) { function scrollToSetting(section: string, settingName: string) {
// Wait for the StackView transition to complete, then scroll
root.pendingSection = section; root.pendingSection = section;
root.pendingSetting = settingName; root.pendingSetting = settingName;
scrollTimer.restart(); scrollTimer.restart();
@@ -78,6 +79,8 @@ Item {
stack.push(screenshot); stack.push(screenshot);
else if (currentCategory === "updates") else if (currentCategory === "updates")
stack.push(updates); stack.push(updates);
else if (currentCategory === "plugins")
stack.push(plugins);
} }
target: root target: root
@@ -133,7 +136,7 @@ Item {
anchors.right: parent.right anchors.right: parent.right
anchors.top: searchBar.bottom anchors.top: searchBar.bottom
anchors.topMargin: Appearance.spacing.smaller anchors.topMargin: Appearance.spacing.smaller
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainerLowest
radius: Appearance.rounding.normal radius: Appearance.rounding.normal
StackView { StackView {
@@ -157,6 +160,7 @@ Item {
id: background id: background
Cat.Background { Cat.Background {
screen: root.screen
} }
} }
@@ -243,4 +247,11 @@ Item {
Cat.SystemUpdates { Cat.SystemUpdates {
} }
} }
Component {
id: plugins
Cat.Plugins {
}
}
} }
@@ -0,0 +1,161 @@
pragma ComponentBehavior: Bound
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Config
import qs.Components
Item {
id: root
required property string name
required property var object
property alias row: row
required property string setting
property bool shouldBeActive: true
required property list<var> stringList
signal optionSet(option: string)
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? row.height : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
RowLayout {
id: row
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger
text: root.name
}
CustomClippingRect {
Layout.preferredHeight: 42 * 6 + Appearance.padding.normal * 2 + Appearance.spacing.small * 5
Layout.preferredWidth: 500
color: DynamicColors.tPalette.m3surfaceContainer
radius: (21 + Appearance.padding.normal) * Appearance.rounding.scale
CustomRect {
id: searchBox
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: 42
radius: Appearance.rounding.full
MaterialIcon {
id: searchIcon
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: Appearance.padding.large
anchors.top: parent.top
font.pointSize: Appearance.font.size.large
text: "search"
verticalAlignment: Text.AlignVCenter
}
CustomTextField {
id: textSearch
anchors.left: searchIcon.right
anchors.leftMargin: Appearance.spacing.small
anchors.right: parent.right
anchors.rightMargin: Appearance.spacing.normal
anchors.verticalCenter: parent.verticalCenter
placeholderText: "Search..."
}
}
CustomClippingRect {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.margins: Appearance.padding.normal
anchors.right: parent.right
anchors.top: searchBox.bottom
bottomLeftRadius: 21
bottomRightRadius: 21
CustomListView {
anchors.fill: parent
clip: true
spacing: Appearance.spacing.small
delegate: CustomRect {
id: delegate
required property string modelData
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 42
radius: Appearance.rounding.smallest
CustomText {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.normal
text: modelData
verticalAlignment: Text.AlignVCenter
}
MaterialIcon {
anchors.fill: parent
anchors.rightMargin: Appearance.padding.normal
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
horizontalAlignment: Text.AlignRight
text: "check_circle"
verticalAlignment: Text.AlignVCenter
visible: root.object[root.setting] === delegate.modelData
}
StateLayer {
onClicked: {
root.object[root.setting] = delegate.modelData;
root.optionSet(delegate.modelData);
Config.save();
}
}
}
model: ScriptModel {
values: {
const values = root.stringList;
const search = textSearch.text;
var regex = new RegExp(search, "i");
return values.filter(n => regex.test(n));
}
}
}
}
}
}
}
+21 -2
View File
@@ -6,7 +6,26 @@ import qs.Config
CustomRect { CustomRect {
id: root id: root
Layout.fillWidth: true property bool shouldBeActive: true
Layout.preferredHeight: 1
anchors.left: parent.left
anchors.right: parent.right
color: DynamicColors.tPalette.m3outlineVariant color: DynamicColors.tPalette.m3outlineVariant
implicitHeight: shouldBeActive ? 1 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
} }
@@ -13,6 +13,7 @@ ColumnLayout {
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
function addAction() { function addAction() {
const list = [...root.object[root.setting]]; const list = [...root.object[root.setting]];
@@ -44,8 +45,26 @@ ColumnLayout {
Config.save(); Config.save();
} }
Layout.fillWidth: true anchors.left: parent.left
anchors.right: parent.right
height: shouldBeActive ? implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
spacing: Appearance.spacing.smaller spacing: Appearance.spacing.smaller
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -108,6 +127,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -188,6 +210,8 @@ ColumnLayout {
StringListEditor { StringListEditor {
Layout.fillWidth: true Layout.fillWidth: true
addLabel: qsTr("Add command argument") addLabel: qsTr("Add command argument")
anchors.left: undefined
anchors.right: undefined
values: [...(modelData.command ?? [])] values: [...(modelData.command ?? [])]
onListEdited: function (values) { onListEdited: function (values) {
@@ -196,6 +220,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -214,6 +241,9 @@ ColumnLayout {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
+139 -108
View File
@@ -6,13 +6,14 @@ import qs.Components
import qs.Config import qs.Config
import qs.Helpers import qs.Helpers
ColumnLayout { CustomRect {
id: root id: root
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
function addAlias() { function addAlias() {
const list = [...root.object[root.setting]]; const list = [...root.object[root.setting]];
@@ -40,8 +41,25 @@ ColumnLayout {
Config.save(); Config.save();
} }
Layout.fillWidth: true anchors.left: parent.left
spacing: Appearance.spacing.smaller anchors.right: parent.right
height: shouldBeActive ? layout.implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -58,115 +76,128 @@ ColumnLayout {
} }
} }
CustomText { ColumnLayout {
Layout.fillWidth: true id: layout
font.pointSize: Appearance.font.size.larger
text: root.name
}
Repeater { anchors.left: parent.left
model: [...root.object[root.setting]] anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
Item { spacing: Appearance.spacing.smaller
required property int index
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: -(Appearance.spacing.smaller / 2)
color: DynamicColors.tPalette.m3outlineVariant
implicitHeight: 1
visible: index !== 0
}
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
text: qsTr("From")
}
CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: 33
color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.full
CustomTextField {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
text: modelData.from ?? ""
onEditingFinished: root.updateAlias(index, "from", text)
}
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "delete"
type: IconButton.Tonal
onClicked: root.removeAlias(index)
}
}
RowLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
text: qsTr("To")
}
CustomRect {
Layout.fillWidth: true
Layout.preferredHeight: 33
color: DynamicColors.tPalette.m3surface
radius: Appearance.rounding.small
CustomTextField {
anchors.fill: parent
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
text: modelData.to ?? ""
onEditingFinished: root.updateAlias(index, "to", text)
}
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
IconButton {
font.pointSize: Appearance.font.size.large
icon: "add"
onClicked: root.addAlias()
}
CustomText { CustomText {
Layout.fillWidth: true Layout.fillWidth: true
text: qsTr("Add alias") font.pointSize: Appearance.font.size.larger
text: root.name
}
Repeater {
model: [...root.object[root.setting]]
Item {
required property int index
required property var modelData
Layout.fillWidth: true
Layout.preferredHeight: layout.implicitHeight + Appearance.padding.smaller * 2
CustomRect {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
anchors.topMargin: -(Appearance.spacing.smaller / 2)
color: DynamicColors.tPalette.m3outlineVariant
implicitHeight: 1
visible: index !== 0
}
ColumnLayout {
id: layout
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.small
RowLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
text: qsTr("From")
}
CustomRect {
Layout.preferredHeight: 33
Layout.preferredWidth: Math.max(Math.min(fromTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.full
CustomTextField {
id: fromTextField
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
text: modelData.from ?? ""
onEditingFinished: root.updateAlias(index, "from", text)
}
}
IconButton {
font.pointSize: Appearance.font.size.large
icon: "delete"
type: IconButton.Tonal
onClicked: root.removeAlias(index)
}
}
RowLayout {
Layout.fillWidth: true
CustomText {
Layout.fillWidth: true
text: qsTr("To")
}
CustomRect {
Layout.preferredHeight: 33
Layout.preferredWidth: Math.max(Math.min(toTextField.contentWidth + Appearance.padding.large * 2, 550), 50)
color: DynamicColors.tPalette.m3surfaceContainerHigh
radius: Appearance.rounding.full
CustomTextField {
id: toTextField
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
implicitWidth: Math.min(contentWidth + Appearance.padding.normal * 2, 550)
text: modelData.to ?? ""
onEditingFinished: root.updateAlias(index, "to", text)
}
}
}
}
}
}
RowLayout {
Layout.fillWidth: true
IconButton {
font.pointSize: Appearance.font.size.large
icon: "add"
onClicked: root.addAlias()
}
CustomText {
Layout.fillWidth: true
text: qsTr("Add alias")
}
} }
} }
} }
@@ -25,6 +25,7 @@ Item {
required property var object required property var object
property var pendingCommitEntries: [] property var pendingCommitEntries: []
required property string setting required property string setting
property bool shouldBeActive: true
property int uidCounter: 0 property int uidCounter: 0
property var visualEntries: [] property var visualEntries: []
@@ -146,8 +147,25 @@ Item {
Config.save(); Config.save();
} }
Layout.fillWidth: true anchors.left: parent.left
implicitHeight: layout.implicitHeight anchors.right: parent.right
implicitHeight: shouldBeActive ? layout.implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Component.onCompleted: root.rebuildVisualEntries() Component.onCompleted: root.rebuildVisualEntries()
@@ -11,6 +11,7 @@ Item {
required property string name required property string name
required property var object required property var object
required property list<string> settings required property list<string> settings
property bool shouldBeActive: true
function commitChoice(choice: int, setting: string): void { function commitChoice(choice: int, setting: string): void {
root.object[setting] = choice; root.object[setting] = choice;
@@ -32,8 +33,25 @@ Item {
return Qt.formatTime(d, "h AP"); return Qt.formatTime(d, "h AP");
} }
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+20 -2
View File
@@ -11,6 +11,7 @@ Item {
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
function formattedValue(): string { function formattedValue(): string {
const value = root.object[root.setting]; const value = root.object[root.setting];
@@ -21,8 +22,25 @@ Item {
return String(value); return String(value);
} }
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+26 -2
View File
@@ -8,13 +8,31 @@ Item {
required property int index required property int index
required property var modelData required property var modelData
property bool shouldBeActive: true
signal addActiveActionRequested signal addActiveActionRequested
signal deleteRequested(int index) signal deleteRequested(int index)
signal fieldEdited(string key, var value) signal fieldEdited(string key, var value)
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
CustomRect { CustomRect {
anchors.left: parent.left anchors.left: parent.left
@@ -176,6 +194,9 @@ Item {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
RowLayout { RowLayout {
@@ -207,6 +228,9 @@ Item {
} }
Separator { Separator {
Layout.fillWidth: true
anchors.left: undefined
anchors.right: undefined
} }
Item { Item {
+28 -5
View File
@@ -11,10 +11,32 @@ Item {
required property string name required property string name
required property var object required property var object
property alias row: row
required property string setting required property string setting
property bool shouldBeActive: true
required property list<var> stringList
Layout.fillWidth: true signal optionSet
Layout.preferredHeight: row.height
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? row.height : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
RowLayout { RowLayout {
id: row id: row
@@ -38,7 +60,7 @@ Item {
Layout.preferredHeight: 42 * 6 + Appearance.padding.normal * 2 + Appearance.spacing.small * 5 Layout.preferredHeight: 42 * 6 + Appearance.padding.normal * 2 + Appearance.spacing.small * 5
Layout.preferredWidth: 500 Layout.preferredWidth: 500
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
radius: 21 + Appearance.padding.normal radius: (21 + Appearance.padding.normal) * Appearance.rounding.scale
CustomRect { CustomRect {
id: searchBox id: searchBox
@@ -120,17 +142,18 @@ Item {
StateLayer { StateLayer {
onClicked: { onClicked: {
root.object[root.setting] = fontDelegate.modelData; root.object[root.setting] = fontDelegate.modelData;
root.optionSet();
Config.save(); Config.save();
} }
} }
} }
model: ScriptModel { model: ScriptModel {
values: { values: {
const fonts = Qt.fontFamilies(); const values = root.stringList;
const search = fontSearch.text; const search = fontSearch.text;
var regex = new RegExp(search, "i"); var regex = new RegExp(search, "i");
return fonts.filter(n => regex.test(n)); return values.filter(n => regex.test(n));
} }
} }
} }
+20 -2
View File
@@ -9,10 +9,28 @@ Item {
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
required property string name required property string name
property bool shouldBeActive: true
required property string value required property string value
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+20 -2
View File
@@ -13,10 +13,28 @@ Item {
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
property real step: 1 property real step: 1
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+20 -2
View File
@@ -11,6 +11,7 @@ Item {
required property string name required property string name
required property var object required property var object
required property list<string> settings required property list<string> settings
property bool shouldBeActive: true
function commitChoice(choice: int, setting: string): void { function commitChoice(choice: int, setting: string): void {
root.object[setting] = choice; root.object[setting] = choice;
@@ -32,8 +33,25 @@ Item {
return Qt.formatTime(d, "h AP"); return Qt.formatTime(d, "h AP");
} }
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -12,9 +12,27 @@ Item {
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: layout.implicitHeight anchors.right: parent.right
implicitHeight: shouldBeActive ? layout.implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
@@ -46,6 +64,9 @@ Item {
StringListEditor { StringListEditor {
Layout.fillWidth: true Layout.fillWidth: true
addLabel: root.addLabel addLabel: root.addLabel
anchors.left: undefined
anchors.right: undefined
anchors.verticalCenter: undefined
values: [...(root.object[root.setting] ?? [])] values: [...(root.object[root.setting] ?? [])]
onListEdited: function (values) { onListEdited: function (values) {
+20 -2
View File
@@ -11,9 +11,27 @@ Item {
required property string name required property string name
required property var object required property var object
required property string setting required property string setting
property bool shouldBeActive: true
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: row.implicitHeight + Appearance.padding.smaller * 2 anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
+6 -2
View File
@@ -7,9 +7,13 @@ CustomRect {
id: root id: root
required property string name required property string name
property bool shouldBeActive: true
Layout.preferredHeight: 60 implicitHeight: 60
Layout.preferredWidth: 200 implicitWidth: 200
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
CustomText { CustomText {
anchors.fill: parent anchors.fill: parent
@@ -0,0 +1,83 @@
import Quickshell
import QtQuick
import QtQuick.Layouts
import qs.Paths
import qs.Components
import qs.Config
import qs.Helpers
Item {
id: root
property alias button: iButton
readonly property bool highlighted: SettingsHighlight.highlightedSetting === name
required property string name
property bool shouldBeActive: true
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: shouldBeActive ? row.implicitHeight + Appearance.padding.smaller * 2 : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Rectangle {
anchors.fill: parent
anchors.margins: -Appearance.padding.smaller
color: DynamicColors.palette.m3primaryContainer
opacity: root.highlighted ? 0.5 : 0
radius: Appearance.rounding.small
Behavior on opacity {
Anim {
duration: Appearance.anim.durations.normal
}
}
}
RowLayout {
id: row
anchors.left: parent.left
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
CustomText {
id: text
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.fillWidth: true
font.pointSize: Appearance.font.size.larger
text: root.name
}
IconButton {
id: iButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
icon: "download"
onClicked: {
const lockBg = `${Paths.state}/lockscreen_bg.png`;
const scheme = `${Paths.state}/scheme.json`;
const face = `${Paths.home}/.face`;
const destination = "/etc/zshell-greeter/images";
Quickshell.execDetached(["pkexec", "sh", "-c", `mkdir -p ${destination}; cp ${lockBg} ${destination}; cp ${scheme} /etc/zshell-greeter; cp ${face} ${destination}`]);
}
}
}
}
+7 -1
View File
@@ -60,12 +60,18 @@ CustomClippingRect {
} }
} }
ColumnLayout { Column {
id: clayout id: clayout
anchors.left: parent.left anchors.left: parent.left
anchors.right: parent.right anchors.right: parent.right
spacing: Appearance.spacing.small spacing: Appearance.spacing.small
// move: Transition {
// Anim {
// properties: "y"
// }
// }
} }
} }
} }
+25 -4
View File
@@ -10,18 +10,39 @@ CustomRect {
property real contentPadding: Appearance.padding.large property real contentPadding: Appearance.padding.large
property string sectionId: "" property string sectionId: ""
Layout.fillWidth: true anchors.left: parent.left
Layout.preferredHeight: layout.implicitHeight + contentPadding * 2 anchors.right: parent.right
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: layout.height + contentPadding * 2
radius: Appearance.rounding.normal - Appearance.padding.smaller radius: Appearance.rounding.normal - Appearance.padding.smaller
ColumnLayout { Behavior on implicitHeight {
Anim {
}
}
Behavior on y {
Anim {
}
}
Column {
id: layout id: layout
anchors.left: parent.left anchors.left: parent.left
anchors.margins: root.contentPadding anchors.margins: root.contentPadding
anchors.right: parent.right anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter anchors.top: parent.top
// anchors.verticalCenter: parent.verticalCenter
spacing: Appearance.spacing.normal spacing: Appearance.spacing.normal
Behavior on height {
Anim {
}
}
move: Transition {
Anim {
properties: "y"
}
}
} }
} }
@@ -12,12 +12,29 @@ CustomRect {
property alias expanded: menu.expanded property alias expanded: menu.expanded
property alias label: label property alias label: label
property alias menu: menu property alias menu: menu
property bool shouldBeActive: true
property alias text: label.text property alias text: label.text
color: enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2) color: enabled ? DynamicColors.palette.m3primary : DynamicColors.layer(DynamicColors.palette.m3surfaceContainerHigh, 2)
opacity: shouldBeActive ? 1 : 0
radius: Appearance.rounding.full radius: Appearance.rounding.full
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
z: expanded ? 100 : 0 z: expanded ? 100 : 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
CustomText { CustomText {
id: label id: label
+20 -1
View File
@@ -9,6 +9,7 @@ ColumnLayout {
id: root id: root
property string addLabel: qsTr("Add entry") property string addLabel: qsTr("Add entry")
property bool shouldBeActive: true
property var values: [] property var values: []
signal listEdited(var values) signal listEdited(var values)
@@ -31,8 +32,26 @@ ColumnLayout {
root.listEdited(list); root.listEdited(list);
} }
Layout.fillWidth: true anchors.left: parent.left
anchors.right: parent.right
height: shouldBeActive ? implicitHeight : 0
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
spacing: Appearance.spacing.smaller spacing: Appearance.spacing.smaller
visible: opacity > 0
Behavior on opacity {
Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
Repeater { Repeater {
model: [...root.values] model: [...root.values]
+247 -70
View File
@@ -1,102 +1,279 @@
pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls
import Quickshell import Quickshell
import Quickshell.Hyprland
import ZShell.Internal
import qs.Config import qs.Config
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
ColumnLayout { Item {
id: root id: wrapper
spacing: 15 property bool changesMade: false
width: Math.min(parent ? parent.width : 600, 600) property bool shouldBeActive: true
Rectangle { signal requestCrop
id: previewContainer
Layout.fillHeight: true anchors.left: parent.left
Layout.preferredWidth: height * (Quickshell.screens.length > 0 ? (Quickshell.screens[0].height / Math.max(1, Quickshell.screens[0].width)) : 16 / 9) anchors.right: parent.right
clip: true implicitHeight: shouldBeActive ? 400 : 0
color: DynamicColors.palette.m3surfaceContainer opacity: shouldBeActive ? 1 : 0
radius: Config.appearance.rounding.scale * 10 scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
Image { Behavior on opacity {
id: img Anim {
}
}
Behavior on scale {
Anim {
}
}
Behavior on y {
Anim {
}
}
anchors.fill: parent IconButton {
asynchronous: true anchors.margins: Appearance.padding.normal
fillMode: Image.PreserveAspectFit anchors.right: parent.right
source: Wallpapers.current anchors.top: parent.top
icon: "check"
opacity: wrapper.changesMade ? 1 : 0
scale: wrapper.changesMade ? 1 : 0
z: 2
Rectangle { Behavior on opacity {
id: cropRect Anim {
}
}
Behavior on scale {
Anim {
}
}
property real cropHeight: (imageAspect > screenAspect ? paintedHeight : paintedWidth / screenAspect) / Config.background.zoom onClicked: {
property real cropWidth: (imageAspect > screenAspect ? paintedHeight * screenAspect : paintedWidth) / Config.background.zoom wrapper.requestCrop();
property real imageAspect: Math.max(1, paintedWidth) / Math.max(1, paintedHeight) wrapper.changesMade = false;
property real paintedHeight: img.paintedHeight > 0 ? img.paintedHeight : img.height }
property real paintedWidth: img.paintedWidth > 0 ? img.paintedWidth : img.width }
property real paintedX: (img.width - paintedWidth) / 2
property real paintedY: (img.height - paintedHeight) / 2
property real screenAspect: Quickshell.screens.length > 0 ? (Quickshell.screens[0].width / Math.max(1, Quickshell.screens[0].height)) : 16 / 9
border.color: DynamicColors.palette.m3primary RowLayout {
border.width: 2 id: root
color: Qt.alpha(DynamicColors.palette.m3primaryContainer, 0.3)
height: cropHeight
width: cropWidth
x: paintedX + (paintedWidth - width) * Config.background.alignX
y: paintedY + (paintedHeight - height) * Config.background.alignY
DragHandler { anchors.fill: parent
target: null spacing: Appearance.spacing.normal
onActiveTranslationChanged: { Repeater {
if (active) { model: ScriptModel {
let newX = cropRect.x - cropRect.paintedX + translation.x; values: [...Quickshell.screens].sort((a, b) => {
let newY = cropRect.y - cropRect.paintedY + translation.y; return a.x - b.x;
})
}
let rangeX = cropRect.paintedWidth - cropRect.width; Item {
let rangeY = cropRect.paintedHeight - cropRect.height; id: delegate
if (rangeX > 0) { required property ShellScreen modelData
let valX = newX / rangeX;
Config.background.alignX = Math.max(0.0, Math.min(1.0, valX));
Config.save();
}
if (rangeY > 0) { function applyCrop(): void {
let valY = newY / rangeY; const croprect = cropRect.mapToItem(scaledImg, 0, 0, cropRect.width, cropRect.height);
Config.background.alignY = Math.max(0.0, Math.min(1.0, valY)); const upscaledRect = Qt.rect((croprect.x - cropRect.imageX) / scaledImg.paintedWidth, (croprect.y - cropRect.imageY) / scaledImg.paintedHeight, croprect.width / scaledImg.paintedWidth, croprect.height / scaledImg.paintedHeight);
Config.save(); Wallpapers.setCrop(delegate.modelData.name, upscaledRect, croprect, cropRect.zoom);
} }
function zoomClipRect(zoom: real): void {
let oldCenterX = cropRect.x + cropRect.width * 0.5;
let oldCenterY = cropRect.y + cropRect.height * 0.5;
cropRect.zoom = zoom;
cropRect.x = oldCenterX - cropRect.width * 0.5;
cropRect.y = oldCenterY - cropRect.height * 0.5;
cropRect.clampToBounds();
}
Layout.fillHeight: true
Layout.fillWidth: true
Connections {
function onRequestCrop(): void {
delegate.applyCrop();
}
target: wrapper
}
RowLayout {
id: sliderLayout
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: 30
spacing: Appearance.spacing.large
CustomText {
text: qsTr("Crop scale")
}
CustomSlider {
id: zoomSlider
Layout.fillWidth: true
Layout.preferredHeight: 10
from: 1.0
to: 5.0
value: cropRect.zoom
onMoved: {
delegate.zoomClipRect(value);
wrapper.changesMade = true;
} }
} }
} }
PinchHandler { CachingImage {
maximumScale: 5.0 id: scaledImg
minimumScale: 1.0
target: null
onActiveScaleChanged: { property var displayData
if (active) { property real monitorScale: 1.0
let newZoom = Config.background.zoom * (1 / (1 + (activeScale - 1) * 0.1));
Config.background.zoom = Math.max(1.0, Math.min(newZoom, 5.0)); anchors.bottom: sliderLayout.top
anchors.bottomMargin: Appearance.spacing.normal
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
asynchronous: true
fillMode: Image.PreserveAspectFit
// retainWhileLoading: true
source: Wallpapers.current
sourceSize.height: parent.height
sourceSize.width: parent.width
onPaintedWidthChanged: {
if (paintedWidth > 0) {
scaledImg.displayData = Wallpapers.getCrop(delegate.modelData.name);
cropRect.zoom = Wallpapers.getCrop(delegate.modelData.name).zoom;
cropRect.restoreFromData();
}
}
onSourceChanged: cropRect.clampToBounds()
onStatusChanged: if (scaledImg.status == Image.Ready)
cropRect.clampToBounds()
CustomText {
id: monitorId
anchors.centerIn: parent
color: Qt.alpha(DynamicColors.palette.m3surface, 0.85)
font.pointSize: Appearance.font.size.large * 4
style: Text.Outline
styleColor: DynamicColors.palette.m3onSurface
text: delegate.modelData.name
}
CustomRect {
id: cropRect
property real aspectRatio: delegate.modelData.width / delegate.modelData.height
readonly property real baseHeight: baseWidth / aspectRatio
readonly property real baseWidth: {
let fittedHeight = scaledImg.paintedHeight;
let fittedWidth = fittedHeight * aspectRatio;
if (fittedWidth > scaledImg.paintedWidth) {
fittedWidth = scaledImg.paintedWidth;
fittedHeight = fittedWidth / aspectRatio;
}
return fittedWidth;
}
readonly property real imageX: (scaledImg.width - scaledImg.paintedWidth) / 2
readonly property real imageY: (scaledImg.height - scaledImg.paintedHeight) / 2
property real imgAspectRatio: scaledImg.paintedWidth / scaledImg.paintedHeight
property real zoom: scaledImg.displayData.zoom
function centerInImage() {
x = imageX + (scaledImg.paintedWidth - width) / 2;
y = imageY + (scaledImg.paintedHeight - height) / 2;
}
function clampToBounds() {
x = Math.max(imageX, Math.min(x, imageX + scaledImg.paintedWidth - width));
y = Math.max(imageY, Math.min(y, imageY + scaledImg.paintedHeight - height));
}
function restoreFromData() {
let data = scaledImg.displayData;
if (data && data.scaledX !== 0 || data.scaledY !== 0 || data.scaledWidth !== 0 || data.scaledHeight !== 0) {
x = data.scaledX;
y = data.scaledY;
clampToBounds();
} else {
zoom = 1.0;
centerInImage();
}
}
border.color: DynamicColors.palette.m3primary
border.width: 2
height: baseHeight / zoom
opacity: 1
width: baseWidth / zoom
Behavior on opacity {
Anim {
}
}
Component.onCompleted: clampToBounds()
onHeightChanged: clampToBounds()
onWidthChanged: clampToBounds()
}
MouseArea {
id: mouse
function updateCrop(mouseX, mouseY) {
let nx = mouseX - cropRect.width * 0.5;
let ny = mouseY - cropRect.height * 0.5;
nx = Math.max(cropRect.imageX, Math.min(nx, cropRect.imageX + scaledImg.paintedWidth - cropRect.width));
ny = Math.max(cropRect.imageY, Math.min(ny, cropRect.imageY + scaledImg.paintedHeight - cropRect.height));
cropRect.x = nx;
cropRect.y = ny;
}
anchors.fill: parent
hoverEnabled: true
preventStealing: true
onPositionChanged: mouse => {
if (pressed) {
updateCrop(mouse.x, mouse.y);
wrapper.changesMade = true;
}
}
onPressed: mouse => {
updateCrop(mouse.x, mouse.y);
wrapper.changesMade = true;
}
onReleased: {
wrapper.changesMade = true;
} }
} }
} }
} }
} }
} }
SettingSpinBox {
max: 5.0
min: 1.0
name: "Zoom"
object: Config.background
setting: "zoom"
step: 0.1
}
} }
+54 -42
View File
@@ -12,37 +12,30 @@ GridView {
readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth)) readonly property int columnsCount: Math.max(1, Math.floor(width / minCellWidth))
readonly property int minCellWidth: 200 + Appearance.spacing.normal readonly property int minCellWidth: 200 + Appearance.spacing.normal
property bool shouldBeActive: true
Layout.preferredHeight: contentHeight anchors.left: parent.left
anchors.right: parent.right
cellHeight: 140 + Appearance.spacing.normal cellHeight: 140 + Appearance.spacing.normal
cellWidth: width / columnsCount cellWidth: width / columnsCount
clip: true clip: true
implicitHeight: shouldBeActive ? contentHeight : 0
interactive: false interactive: false
model: Wallpapers.list model: Wallpapers.list
opacity: shouldBeActive ? 1 : 0
scale: shouldBeActive ? 1 : 0.8
visible: opacity > 0
delegate: Item { delegate: Item {
required property int index required property int index
readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent readonly property bool isCurrent: modelData && modelData.path === Wallpapers.actualCurrent
readonly property real itemMargin: Appearance.spacing.normal / 2 readonly property real itemMargin: Appearance.spacing.normal
readonly property real itemRadius: Appearance.rounding.normal readonly property real itemRadius: Appearance.rounding.small
required property var modelData required property var modelData
height: root.cellHeight height: root.cellHeight
width: root.cellWidth width: root.cellWidth
StateLayer {
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
anchors.bottomMargin: itemMargin
anchors.fill: parent
anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin
radius: itemRadius
}
CustomClippingRect { CustomClippingRect {
id: image id: image
@@ -53,8 +46,6 @@ GridView {
anchors.topMargin: itemMargin anchors.topMargin: itemMargin
antialiasing: true antialiasing: true
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
layer.enabled: true
layer.smooth: true
radius: itemRadius radius: itemRadius
CachingImage { CachingImage {
@@ -100,6 +91,33 @@ GridView {
} }
} }
Rectangle {
anchors.fill: parent
antialiasing: true
border.color: DynamicColors.palette.m3primary
border.width: isCurrent ? 2 : 0
color: "transparent"
radius: itemRadius + 2
smooth: true
Behavior on border.width {
NumberAnimation {
duration: 150
easing.type: Easing.OutQuad
}
}
MaterialIcon {
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
text: "check_circle"
visible: isCurrent
}
}
Timer { Timer {
id: fallbackTimer id: fallbackTimer
@@ -112,35 +130,29 @@ GridView {
} }
} }
Rectangle { StateLayer {
function onClicked(): void {
Wallpapers.setWallpaper(modelData.path);
}
anchors.bottomMargin: itemMargin anchors.bottomMargin: itemMargin
anchors.fill: parent anchors.fill: parent
anchors.leftMargin: itemMargin anchors.leftMargin: itemMargin
anchors.rightMargin: itemMargin anchors.rightMargin: itemMargin
anchors.topMargin: itemMargin anchors.topMargin: itemMargin
antialiasing: true radius: itemRadius
border.color: DynamicColors.palette.m3primary }
border.width: isCurrent ? 2 : 0 }
color: "transparent" Behavior on opacity {
radius: itemRadius - border.width Anim {
smooth: true }
}
Behavior on border.width { Behavior on scale {
NumberAnimation { Anim {
duration: 150 }
easing.type: Easing.OutQuad }
} Behavior on y {
} Anim {
MaterialIcon {
anchors.margins: Appearance.padding.small
anchors.right: parent.right
anchors.top: parent.top
color: DynamicColors.palette.m3primary
font.pointSize: Appearance.font.size.large
text: "check_circle"
visible: isCurrent
}
} }
} }
} }
File diff suppressed because it is too large Load Diff
+2 -3
View File
@@ -6,7 +6,7 @@ import QtQuick.Controls
import qs.Components import qs.Components
import qs.Config import qs.Config
import "../../scripts/fuzzysort.js" as Fuzzy import "../../scripts/fuzzysort.js" as Fuzzy
import "./SettingsIndex.mjs" as SettingsIndex import "../../scripts/SettingsIndex.mjs" as SettingsIndex
Item { Item {
id: root id: root
@@ -53,11 +53,10 @@ Item {
Shortcut { Shortcut {
sequence: "/" sequence: "/"
onActivated: searchField.forceActiveFocus() onActivated: searchField.forceActiveFocus()
} }
Component.onCompleted: console.log(root.height)
ListModel { ListModel {
id: resultsModel id: resultsModel
} }
+10 -31
View File
@@ -7,45 +7,24 @@ import qs.Helpers
Item { Item {
id: root id: root
property real offsetScale: shouldBeActive ? 0 : 1
required property var panels required property var panels
required property ShellScreen screen required property ShellScreen screen
readonly property bool shouldBeActive: visibilities.settings
required property PersistentProperties visibilities required property PersistentProperties visibilities
implicitHeight: 0 anchors.topMargin: (-implicitHeight - 5) * offsetScale
implicitHeight: content.implicitHeight
implicitWidth: content.implicitWidth implicitWidth: content.implicitWidth
visible: height > 0 opacity: 1 - offsetScale
visible: offsetScale < 1
states: State { Behavior on offsetScale {
name: "visible" Anim {
when: root.visibilities.settings duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
PropertyChanges {
root.implicitHeight: content.implicitHeight
} }
} }
transitions: [
Transition {
from: ""
to: "visible"
Anim {
duration: MaterialEasing.expressiveEffectsTime
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
},
Transition {
from: "visible"
to: ""
Anim {
easing.bezierCurve: MaterialEasing.expressiveEffects
property: "implicitHeight"
target: root
}
}
]
CustomClippingRect { CustomClippingRect {
anchors.fill: parent anchors.fill: parent
-1
View File
@@ -19,7 +19,6 @@ Scope {
if (!root.launcherInterrupted && !root.hasFullscreen) { if (!root.launcherInterrupted && !root.hasFullscreen) {
const visibilities = Visibilities.getForActive(); const visibilities = Visibilities.getForActive();
visibilities.launcher = !visibilities.launcher; visibilities.launcher = !visibilities.launcher;
console.log(root.launcherInterrupted);
} }
root.launcherInterrupted = false; root.launcherInterrupted = false;
} }
+7 -5
View File
@@ -82,12 +82,14 @@ StackView {
CustomRect { CustomRect {
id: item id: item
required property int index
required property QsMenuEntry modelData required property QsMenuEntry modelData
color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent" color: modelData.isSeparator ? DynamicColors.palette.m3outlineVariant : "transparent"
implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight implicitHeight: modelData.isSeparator ? 1 : children.implicitHeight
implicitWidth: root.biggestWidth implicitWidth: root.biggestWidth
radius: Appearance.rounding.smallest / 2 radius: Appearance.rounding.full
visible: index !== (menuOpener.children.values.length - 1) ? true : (modelData.isSeparator ? false : true)
Loader { Loader {
id: children id: children
@@ -201,18 +203,18 @@ StackView {
asynchronous: true asynchronous: true
sourceComponent: Item { sourceComponent: Item {
implicitHeight: back.implicitHeight + 2 / 2 implicitHeight: 30
implicitWidth: back.implicitWidth implicitWidth: back.implicitWidth
Item { Item {
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
implicitHeight: back.implicitHeight implicitHeight: 30
implicitWidth: back.implicitWidth + 10 implicitWidth: root.biggestWidth
CustomRect { CustomRect {
anchors.fill: parent anchors.fill: parent
color: DynamicColors.palette.m3secondaryContainer color: DynamicColors.palette.m3secondaryContainer
radius: Appearance.rounding.smallest / 2 radius: Appearance.rounding.full
StateLayer { StateLayer {
function onClicked(): void { function onClicked(): void {
+1 -1
View File
@@ -31,7 +31,7 @@ Item {
implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight implicitHeight: Math.max(saver.implicitHeight, balance.implicitHeight, perf.implicitHeight) + 5 * 2 + saverLabel.contentHeight
implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth implicitWidth: saver.implicitHeight + balance.implicitHeight + perf.implicitHeight + 8 * 2 + saverLabel.contentWidth
// color: "transparent" // color: "transparent"
radius: 6 radius: (20 - Appearance.padding.small) * Appearance.rounding.scale
CustomRect { CustomRect {
id: indicator id: indicator
+1
View File
@@ -1,5 +1,6 @@
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick import QtQuick
import QtQuick.VectorImage
import Quickshell import Quickshell
import Quickshell.Services.SystemTray import Quickshell.Services.SystemTray
import qs.Modules import qs.Modules
+1 -1
View File
@@ -14,7 +14,7 @@ CustomRect {
color: DynamicColors.tPalette.m3surfaceContainer color: DynamicColors.tPalette.m3surfaceContainer
implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2 implicitHeight: Config.barConfig.height + Appearance.padding.smallest * 2
implicitWidth: contentRow.implicitWidth + Appearance.spacing.small * 2 implicitWidth: contentRow.implicitWidth + Appearance.spacing.small * 2
radius: height / 2 radius: Appearance.rounding.full
RowLayout { RowLayout {
id: contentRow id: contentRow
+80
View File
@@ -0,0 +1,80 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.Components
import qs.Helpers
import qs.Config
Item {
id: root
property Image current: one
property string source: Wallpapers.current
anchors.fill: parent
Component.onCompleted: {
if (source)
Qt.callLater(() => one.update());
}
onSourceChanged: {
if (!source) {
current = null;
} else if (current === one) {
two.update();
} else {
one.update();
}
}
Img {
id: one
}
Img {
id: two
}
component Img: Image {
id: img
function update(): void {
if (source === root.source) {
root.current = this;
} else {
source = root.source;
}
}
anchors.fill: parent
asynchronous: true
fillMode: Image.PreserveAspectCrop
opacity: 0
retainWhileLoading: true
scale: Wallpapers.showPreview ? 1 : 0.8
sourceClipRect: Qt.rect(Config.background.sourceClipX, Config.background.sourceClipY, Config.background.sourceClipW, Config.background.sourceClipH)
states: State {
name: "visible"
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
}
}
transitions: Transition {
Anim {
duration: Config.background.wallFadeDuration
properties: "opacity,scale"
target: img
}
}
onStatusChanged: {
if (status === Image.Ready) {
root.current = this;
}
}
}
}
+54 -55
View File
@@ -1,5 +1,7 @@
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Hyprland
import QtQuick import QtQuick
import qs.Components import qs.Components
import qs.Helpers import qs.Helpers
@@ -8,79 +10,76 @@ import qs.Config
Item { Item {
id: root id: root
property Image current: one required property ShellScreen screen
property string source: Wallpapers.current property string source: Wallpapers.current
function refreshData(): void {
Hyprland.refreshMonitors();
const scale = Hyprland.monitorFor(root.screen).scale;
if (scale > 0 && img.resScale !== scale) {
img.resScale = scale;
img.sourceSize.width = root.screen.width * scale;
}
const displayData = Wallpapers.getCrop(root.screen.name);
const displayRect = Qt.rect(img.sourceSize.width * displayData.x, img.implicitHeight * displayData.y, img.sourceSize.width * displayData.width, img.implicitHeight * displayData.height);
img.anchors.fill = null;
img.zoom = displayData.zoom;
img.x = -(displayRect.x * displayData.zoom / img.resScale);
img.y = -(displayRect.y * displayData.zoom / img.resScale);
}
anchors.fill: parent anchors.fill: parent
Component.onCompleted: { Image {
if (source)
Qt.callLater(() => one.update());
}
onSourceChanged: {
if (!source) {
current = null;
} else if (current === one) {
two.update();
} else {
one.update();
}
}
Img {
id: one
}
Img {
id: two
}
component Img: CachingImage {
id: img id: img
property real imageRatio: Math.max(1, sourceSize.width) / Math.max(1, sourceSize.height) property int displayH
property bool isValid: sourceSize.width > 0 && sourceSize.height > 0 && root.width > 0 && root.height > 0 property int displayW
property real windowRatio: root.width / Math.max(1, root.height) property real resScale
property real zoom: 1.0
function update(): void {
if (path === root.source) {
root.current = this;
} else {
path = root.source;
}
}
anchors.fill: undefined
asynchronous: true asynchronous: true
fillMode: Image.PreserveAspectCrop fillMode: Image.PreserveAspectCrop
height: isValid ? (imageRatio > windowRatio ? root.height : root.width / imageRatio) * Config.background.zoom : root.height height: implicitHeight * zoom / resScale
opacity: 0 opacity: 1
scale: Wallpapers.showPreview ? 1 : 0.8 retainWhileLoading: true
width: isValid ? (imageRatio > windowRatio ? root.height * imageRatio : root.width) * Config.background.zoom : root.width source: root.source
x: isValid ? (root.width - width) * Config.background.alignX : 0 sourceSize.width: root.screen.width * resScale
y: isValid ? (root.height - height) * Config.background.alignY : 0 width: implicitWidth * zoom / resScale
states: State { Behavior on height {
name: "visible" Anim {
when: root.current === img
PropertyChanges {
img.opacity: 1
img.scale: 1
} }
} }
transitions: Transition { Behavior on width {
Anim {
}
}
Behavior on x {
Anim {
}
}
Behavior on y {
Anim { Anim {
duration: Config.background.wallFadeDuration
properties: "opacity,scale"
target: img
} }
} }
onStatusChanged: { onStatusChanged: {
if (status === Image.Ready) { if (img.status == Image.Ready) {
root.current = this; root.refreshData();
} }
} }
Connections {
function onAdapterUpdated(): void {
root.refreshData();
}
function onLoaded(): void {
root.refreshData();
}
target: Wallpapers.monitorCrops
}
} }
} }
+1
View File
@@ -30,6 +30,7 @@ Loader {
} }
WallBackground { WallBackground {
screen: root.screen
} }
Loader { Loader {
+2 -1
View File
@@ -79,7 +79,8 @@ Item {
} }
onPressed: { onPressed: {
Hyprland.dispatch(`workspace ${button.modelData.name}`); const ws = button.modelData.name;
Hyprland.dispatch(Hyprland.usingLua ? `hl.dsp.focus({ workspace= "${ws}"})` : `workspace ${ws}`);
} }
} }
} }
+1
View File
@@ -12,6 +12,7 @@ qml_module(ZShell-blobs
qt_add_shaders(ZShell-blobs "blob_shaders" qt_add_shaders(ZShell-blobs "blob_shaders"
BATCHABLE OPTIMIZED NOHLSL NOMSL BATCHABLE OPTIMIZED NOHLSL NOMSL
GLSL "300es,330"
PREFIX "/" PREFIX "/"
FILES FILES
shaders/blob.frag shaders/blob.frag
+7 -1
View File
@@ -2,6 +2,9 @@
#include <cstring> #include <cstring>
static_assert(sizeof(decltype(BlobRectData::excludeMask)) == sizeof(float),
"BlobMaterial packs excludeMask into a float slot via memcpy");
QSGMaterialType* BlobMaterial::type() const { QSGMaterialType* BlobMaterial::type() const {
static QSGMaterialType s_type; static QSGMaterialType s_type;
return &s_type; return &s_type;
@@ -82,8 +85,11 @@ bool BlobMaterialShader::updateUniformData(RenderState& state, QSGMaterial* newM
for (int i = 0; i < count; ++i) { for (int i = 0; i < count; ++i) {
const auto& r = mat->m_rects[i]; const auto& r = mat->m_rects[i];
const int base = 160 + i * 80; const int base = 160 + i * 80;
// Pack excludeMask into props.x via bit-cast (read in shader with floatBitsToInt)
float maskAsFloat;
memcpy(&maskAsFloat, &r.excludeMask, sizeof(float));
const float d0[4] = { r.cx, r.cy, r.hw, r.hh }; const float d0[4] = { r.cx, r.cy, r.hw, r.hh };
const float d1[4] = { 0.0f, r.offsetX, r.offsetY, r.minEig }; const float d1[4] = { maskAsFloat, r.offsetX, r.offsetY, r.minEig };
const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f }; const float d3[4] = { r.screenHalfX, r.screenHalfY, 0.0f, 0.0f };
memcpy(buf->data() + base, d0, 16); memcpy(buf->data() + base, d0, 16);
memcpy(buf->data() + base + 16, d1, 16); memcpy(buf->data() + base + 16, d1, 16);
+3
View File
@@ -14,6 +14,9 @@ struct BlobRectData {
float screenHalfX = 0, screenHalfY = 0; float screenHalfX = 0, screenHalfY = 0;
// Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU // Effective per-corner radii (tr, br, bl, tl), pre-computed on CPU
float radius[4] = { 0, 0, 0, 0 }; float radius[4] = { 0, 0, 0, 0 };
// Bitmask of indices in this rect's m_cachedRects that mutually exclude (or are excluded by) this rect.
// Used by the shader to skip smin between excluded pairs.
int excludeMask = 0;
}; };
class BlobMaterial : public QSGMaterial { class BlobMaterial : public QSGMaterial {
+33 -3
View File
@@ -72,11 +72,17 @@ void BlobShape::geometryChange(const QRectF& newGeometry, const QRectF& oldGeome
// Accumulate sub-pixel drift so slow movements don't desync the shader // Accumulate sub-pixel drift so slow movements don't desync the shader
m_pendingDx += static_cast<float>(newGeometry.x() - oldGeometry.x()); m_pendingDx += static_cast<float>(newGeometry.x() - oldGeometry.x());
m_pendingDy += static_cast<float>(newGeometry.y() - oldGeometry.y()); m_pendingDy += static_cast<float>(newGeometry.y() - oldGeometry.y());
const auto dw = std::abs(newGeometry.width() - oldGeometry.width()); // Accumulate size delta across multiple frames so incremental size
const auto dh = std::abs(newGeometry.height() - oldGeometry.height()); // changes that are each below the threshold still trigger a dirty
if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f || dw > 0.5 || dh > 0.5) { // mark once their accumulated delta exceeds it.
m_pendingDw += static_cast<float>(newGeometry.width() - oldGeometry.width());
m_pendingDh += static_cast<float>(newGeometry.height() - oldGeometry.height());
if (std::abs(m_pendingDx) > 0.5f || std::abs(m_pendingDy) > 0.5f ||
std::abs(m_pendingDw) > 0.5f || std::abs(m_pendingDh) > 0.5f) {
m_pendingDx = 0; m_pendingDx = 0;
m_pendingDy = 0; m_pendingDy = 0;
m_pendingDw = 0;
m_pendingDh = 0;
m_group->markShapeDirty(this); m_group->markShapeDirty(this);
} }
} }
@@ -149,6 +155,10 @@ void BlobShape::updatePolish() {
const QRectF myPadded(static_cast<double>(m_cachedPaddedX), static_cast<double>(m_cachedPaddedY), const QRectF myPadded(static_cast<double>(m_cachedPaddedX), static_cast<double>(m_cachedPaddedY),
static_cast<double>(m_cachedPaddedW), static_cast<double>(m_cachedPaddedH)); static_cast<double>(m_cachedPaddedW), static_cast<double>(m_cachedPaddedH));
// Track shape pointers parallel to m_cachedRects for pairwise exclusion lookups
QVector<BlobShape*> rectShapes;
rectShapes.reserve(m_group->shapes().size());
for (BlobShape* other : m_group->shapes()) { for (BlobShape* other : m_group->shapes()) {
if (other->isInvertedRect()) if (other->isInvertedRect())
continue; continue;
@@ -210,12 +220,29 @@ void BlobShape::updatePolish() {
r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh; r.screenHalfY = std::abs(b) * r.hw + std::abs(d) * r.hh;
m_cachedRects.append(r); m_cachedRects.append(r);
rectShapes.append(other);
} }
} }
if (isInvertedRect()) if (isInvertedRect())
m_cachedMyIndex = -1; m_cachedMyIndex = -1;
// Compute pairwise exclude masks. Bit j in entry i is set iff rect i excludes rect j
// or rect j excludes rect i. The shader uses this to avoid smin between excluded pairs.
const auto cachedCount = m_cachedRects.size();
for (qsizetype i = 0; i < cachedCount; ++i) {
int mask = 0;
BlobShape* si = rectShapes[i];
for (qsizetype j = 0; j < cachedCount; ++j) {
if (j == i)
continue;
BlobShape* sj = rectShapes[j];
if (si->isExcluded(sj) || sj->isExcluded(si))
mask |= (1 << j);
}
m_cachedRects[i].excludeMask = mask;
}
// Cache inverted rect data // Cache inverted rect data
m_cachedHasInverted = false; m_cachedHasInverted = false;
m_cachedInvertedRadius = 0; m_cachedInvertedRadius = 0;
@@ -270,6 +297,7 @@ void BlobShape::updatePolish() {
const auto rectCount = m_cachedRects.size(); const auto rectCount = m_cachedRects.size();
for (qsizetype i = 0; i < rectCount; ++i) { for (qsizetype i = 0; i < rectCount; ++i) {
auto& ri = m_cachedRects[i]; auto& ri = m_cachedRects[i];
const int riExcludeMask = ri.excludeMask;
float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f; float fTr = 1.0f, fBr = 1.0f, fBl = 1.0f, fTl = 1.0f;
const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh; const float cTrX = ri.cx + ri.hw, cTrY = ri.cy - ri.hh;
@@ -280,6 +308,8 @@ void BlobShape::updatePolish() {
for (qsizetype j = 0; j < rectCount; ++j) { for (qsizetype j = 0; j < rectCount; ++j) {
if (j == i) if (j == i)
continue; continue;
if (riExcludeMask & (1 << j))
continue;
const auto& rj = m_cachedRects[j]; const auto& rj = m_cachedRects[j];
fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh))); fTr = std::min(fTr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cTrX, cTrY, rj.cx, rj.cy, rj.hw, rj.hh)));
fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh))); fBr = std::min(fBr, cpuSmoothstep(0.0f, smoothFactor, cpuSdBox(cBrX, cBrY, rj.cx, rj.cy, rj.hw, rj.hh)));
+4 -2
View File
@@ -84,8 +84,10 @@ QRectF m_localPaddedRect;
QVector<BlobRectData> m_cachedRects; QVector<BlobRectData> m_cachedRects;
int m_cachedMyIndex = -2; int m_cachedMyIndex = -2;
float m_pendingDx = 0; float m_pendingDx = 0;
float m_pendingDy = 0; float m_pendingDy = 0;
bool m_cachedHasInverted = false; float m_pendingDw = 0;
float m_pendingDh = 0;
bool m_cachedHasInverted = false;
float m_cachedInvertedRadius = 0; float m_cachedInvertedRadius = 0;
float m_cachedInvertedOuter[4] = {}; float m_cachedInvertedOuter[4] = {};
float m_cachedInvertedInner[4] = {}; float m_cachedInvertedInner[4] = {};
+36 -5
View File
@@ -63,13 +63,17 @@ float smaxSharpA(float a, float b, float k) {
void main() { void main() {
vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH); vec2 pixel = vec2(paddedX, paddedY) + qt_TexCoord0 * vec2(paddedW, paddedH);
float mergedSdf = 1e10; // Phase 1: compute per-rect SDF, track owner. We can't smin yet because
// excluded pairs need to skip the smooth blend, which requires pairwise pass
// below.
float dArr[16];
int owner = -2; int owner = -2;
float minDist = 1e10; float minDist = 1e10;
for (int i = 0; i < rectCount; i++) { for (int i = 0; i < rectCount; i++) {
vec4 rect = rectData[i * 5]; // cx, cy, hw, hh vec4 rect = rectData[i * 5]; // cx, cy, hw, hh
vec4 props = rectData[i * 5 + 1]; // radius, offsetX, offsetY, minEig vec4 props =
rectData[i * 5 + 1]; // excludeMask(int bits), offsetX, offsetY, minEig
vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix vec4 invDm = rectData[i * 5 + 2]; // inverse deform matrix
vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0 vec4 sh = rectData[i * 5 + 3]; // screenHalfX, screenHalfY, 0, 0
vec4 radii = vec4 radii =
@@ -81,8 +85,10 @@ void main() {
// AABB early-out: skip rects far from this pixel // AABB early-out: skip rects far from this pixel
vec2 extent = sh.xy + vec2(smoothFactor * 1.5); vec2 extent = sh.xy + vec2(smoothFactor * 1.5);
if (abs(pixel.x - center.x) > extent.x || if (abs(pixel.x - center.x) > extent.x ||
abs(pixel.y - center.y) > extent.y) abs(pixel.y - center.y) > extent.y) {
dArr[i] = 1e10;
continue; continue;
}
// Apply pre-computed inverse deformation to the evaluation point // Apply pre-computed inverse deformation to the evaluation point
mat2 invDeform = mat2(invDm.xy, invDm.zw); mat2 invDeform = mat2(invDm.xy, invDm.zw);
@@ -138,13 +144,38 @@ void main() {
d *= scale; d *= scale;
} }
mergedSdf = smin(mergedSdf, d, smoothFactor); dArr[i] = d;
if (d < smoothFactor && d < minDist) { if (d < smoothFactor && d < minDist) {
minDist = d; minDist = d;
owner = i; owner = i;
} }
} }
// Phase 2: hard-min baseline over all rects.
float mergedSdf = 1e10;
for (int i = 0; i < rectCount; i++) {
mergedSdf = min(mergedSdf, dArr[i]);
}
// Phase 3: pair-wise smin contributions, skipping excluded pairs. Pair smin
// <= min, so taking the min over all non-excluded pair smins gives the
// smoothly-merged SDF.
for (int i = 0; i < rectCount; i++) {
if (dArr[i] >= 1e9)
continue;
int excludeMask = floatBitsToInt(rectData[i * 5 + 1].x);
for (int j = i + 1; j < rectCount; j++) {
if (dArr[j] >= 1e9)
continue;
if ((excludeMask & (1 << j)) != 0)
continue;
// smin only deviates from min within smoothFactor
if (abs(dArr[i] - dArr[j]) >= smoothFactor)
continue;
mergedSdf = min(mergedSdf, smin(dArr[i], dArr[j], smoothFactor));
}
}
if (hasInverted != 0) { if (hasInverted != 0) {
float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0; float dOuter = sdBox(pixel, invertedOuter.xy, invertedOuter.zw) - 1.0;
float dInner = float dInner =
+309 -142
View File
@@ -1,217 +1,384 @@
#include "hyprextras.hpp" #include "hyprextras.hpp"
#include "hyprdevices.hpp"
#include <qdir.h> #include <qdir.h>
#include <qjsonarray.h> #include <qjsonarray.h>
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qloggingcategory.h>
#include <qmetatype.h>
#include <qregularexpression.h>
#include <qvariant.h> #include <qvariant.h>
Q_LOGGING_CATEGORY(lcHypr, "ZShell.internal.hypr", QtInfoMsg)
namespace ZShell::internal::hypr { namespace ZShell::internal::hypr {
namespace {
static QString luaEscapeString(const QString& s) {
QString out;
out.reserve(s.size() + 2);
out += QLatin1Char('"');
for (const QChar c : s) {
switch (c.unicode()) {
case '\\':
out += QLatin1String(R"(\\)");
break;
case '"':
out += QLatin1String(R"(\")");
break;
case '\n':
out += QLatin1String(R"(\n)");
break;
case '\r':
out += QLatin1String(R"(\r)");
break;
case '\t':
out += QLatin1String(R"(\t)");
break;
default:
out += c;
break;
}
}
out += QLatin1Char('"');
return out;
}
static QString luaValue(const QVariant& v);
static QString luaArray(const QVariantList& list) {
QStringList parts;
parts.reserve(list.size());
for (const auto& item : list) {
parts << luaValue(item);
}
return QLatin1String("{ ") + parts.join(QLatin1String(", ")) + QLatin1String(" }");
}
static QString luaArray(const QStringList& list) {
QStringList parts;
parts.reserve(list.size());
for (const auto& item : list) {
parts << luaEscapeString(item);
}
return QLatin1String("{ ") + parts.join(QLatin1String(", ")) + QLatin1String(" }");
}
static QString luaMapFromHash(const QVariantHash& hash) {
QStringList parts;
parts.reserve(hash.size());
for (auto it = hash.cbegin(); it != hash.cend(); ++it) {
parts << luaEscapeString(it.key()) + QLatin1String(" = ") + luaValue(it.value());
}
return QLatin1String("{ ") + parts.join(QLatin1String(", ")) + QLatin1String(" }");
}
static QString luaMap(const QVariantMap& map) {
QStringList parts;
parts.reserve(map.size());
for (auto it = map.cbegin(); it != map.cend(); ++it) {
parts << luaEscapeString(it.key()) + QLatin1String(" = ") + luaValue(it.value());
}
return QLatin1String("{ ") + parts.join(QLatin1String(", ")) + QLatin1String(" }");
}
static QString luaValue(const QVariant& v) {
if (!v.isValid() || v.isNull()) {
return QLatin1String("nil");
}
switch (v.metaType().id()) {
case QMetaType::Bool:
return v.toBool() ? QLatin1String("true") : QLatin1String("false");
case QMetaType::Int:
case QMetaType::UInt:
case QMetaType::LongLong:
case QMetaType::ULongLong:
case QMetaType::Double:
return v.toString();
case QMetaType::QString:
return luaEscapeString(v.toString());
case QMetaType::QStringList:
return luaArray(v.toStringList());
case QMetaType::QVariantList:
return luaArray(v.toList());
case QMetaType::QVariantMap:
return luaMap(v.toMap());
case QMetaType::QVariantHash:
return luaMapFromHash(v.toHash());
default:
return luaEscapeString(v.toString());
}
}
static QString normalizeOptionPath(QString key) {
key = key.trimmed();
key.replace(QLatin1Char(':'), QLatin1Char('.'));
return key;
}
static QString buildHlConfigCall(const QString& key, const QVariant& value) {
const auto parts = normalizeOptionPath(key).split(QLatin1Char('.'), Qt::SkipEmptyParts);
if (parts.isEmpty()) {
return {};
}
QString out;
out.reserve(32 + key.size() + value.toString().size());
out += QLatin1String("hl.config({ ");
for (int i = 0; i < parts.size(); ++i) {
out += parts.at(i);
out += QLatin1String(" = ");
if (i + 1 < parts.size()) {
out += QLatin1String("{ ");
}
}
out += luaValue(value);
for (int i = 0; i + 1 < parts.size(); ++i) {
out += QLatin1String(" }");
}
out += QLatin1String(" })");
return out;
}
} // namespace
HyprExtras::HyprExtras(QObject* parent) HyprExtras::HyprExtras(QObject* parent)
: QObject(parent) : QObject(parent)
, m_requestSocket("") , m_requestSocket("")
, m_eventSocket("") , m_eventSocket("")
, m_socket(nullptr) , m_socket(nullptr)
, m_socketValid(false) , m_socketValid(false)
, m_devices(new HyprDevices(this)) { , m_devices(new HyprDevices(this)) {
const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); const auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE");
if (his.isEmpty()) { if (his.isEmpty()) {
qWarning() qCWarning(lcHypr) << "$HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket.";
<< "HyprExtras::HyprExtras: $HYPRLAND_INSTANCE_SIGNATURE is unset. Unable to connect to Hyprland socket."; return;
return; }
}
auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his); auto hyprDir = QString("%1/hypr/%2").arg(qEnvironmentVariable("XDG_RUNTIME_DIR"), his);
if (!QDir(hyprDir).exists()) { if (!QDir(hyprDir).exists()) {
hyprDir = "/tmp/hypr/" + his; hyprDir = QStringLiteral("/tmp/hypr/") + his;
if (!QDir(hyprDir).exists()) { if (!QDir(hyprDir).exists()) {
qWarning() << "HyprExtras::HyprExtras: Hyprland socket directory does not exist. Unable to connect to " qCWarning(lcHypr) << "Hyprland socket directory does not exist. Unable to connect to Hyprland socket.";
"Hyprland socket."; return;
return; }
} }
}
m_requestSocket = hyprDir + "/.socket.sock"; m_requestSocket = hyprDir + QStringLiteral("/.socket.sock");
m_eventSocket = hyprDir + "/.socket2.sock"; m_eventSocket = hyprDir + QStringLiteral("/.socket2.sock");
refreshOptions(); refreshOptions();
refreshDevices(); refreshDevices();
m_socket = new QLocalSocket(this); m_socket = new QLocalSocket(this);
QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError); QObject::connect(m_socket, &QLocalSocket::errorOccurred, this, &HyprExtras::socketError);
QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged); QObject::connect(m_socket, &QLocalSocket::stateChanged, this, &HyprExtras::socketStateChanged);
QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent); QObject::connect(m_socket, &QLocalSocket::readyRead, this, &HyprExtras::readEvent);
m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly); m_socket->connectToServer(m_eventSocket, QLocalSocket::ReadOnly);
} }
QVariantHash HyprExtras::options() const { QVariantHash HyprExtras::options() const {
return m_options; return m_options;
} }
HyprDevices* HyprExtras::devices() const { HyprDevices* HyprExtras::devices() const {
return m_devices; return m_devices;
} }
void HyprExtras::message(const QString& message) { void HyprExtras::message(const QString& message) {
if (message.isEmpty()) { if (message.isEmpty()) {
return; return;
} }
makeRequest(message, [](bool success, const QByteArray& res) { makeRequest(message, [](bool success, const QByteArray& res) {
if (!success) { if (!success) {
qWarning() << "HyprExtras::message: request error:" << QString::fromUtf8(res); qCWarning(lcHypr) << "message: request error:" << QString::fromUtf8(res);
} }
}); });
} }
void HyprExtras::batchMessage(const QStringList& messages) { void HyprExtras::batchMessage(const QStringList& messages) {
if (messages.isEmpty()) { if (messages.isEmpty()) {
return; return;
} }
makeRequest("[[BATCH]]" + messages.join(";"), [](bool success, const QByteArray& res) { makeRequest(QStringLiteral("[[BATCH]]") + messages.join(QLatin1Char(';')),
if (!success) { [](bool success, const QByteArray& res) {
qWarning() << "HyprExtras::batchMessage: request error:" << QString::fromUtf8(res); if (!success) {
} qCWarning(lcHypr) << "batchMessage: request error:" << QString::fromUtf8(res);
}); }
});
} }
void HyprExtras::applyOptions(const QVariantHash& options) { void HyprExtras::applyOptions(const QVariantHash& options) {
if (options.isEmpty()) { if (options.isEmpty()) {
return; return;
} }
QString request = "[[BATCH]]"; QStringList calls;
for (auto it = options.constBegin(); it != options.constEnd(); ++it) { calls.reserve(options.size());
request += QString("keyword %1 %2;").arg(it.key(), it.value().toString());
}
makeRequest(request, [this](bool success, const QByteArray& res) { for (auto it = options.constBegin(); it != options.constEnd(); ++it) {
if (success) { const auto call = buildHlConfigCall(it.key(), it.value());
refreshOptions(); if (!call.isEmpty()) {
} else { calls << call;
qWarning() << "HyprExtras::applyOptions: request error" << QString::fromUtf8(res); }
} }
});
if (calls.isEmpty()) {
return;
}
makeRequest(QStringLiteral("eval ") + calls.join(QLatin1String("; ")), [this](bool success, const QByteArray& res) {
if (success) {
refreshOptions();
} else {
qCWarning(lcHypr) << "applyOptions: request error" << QString::fromUtf8(res);
}
});
} }
void HyprExtras::refreshOptions() { void HyprExtras::refreshOptions() {
if (!m_optionsRefresh.isNull()) { if (!m_optionsRefresh.isNull()) {
m_optionsRefresh->close(); m_optionsRefresh->close();
} }
m_optionsRefresh = makeRequestJson("descriptions", [this](bool success, const QJsonDocument& response) { m_optionsRefresh = makeRequestJson(QStringLiteral("descriptions"), [this](bool success, const QJsonDocument& response) {
m_optionsRefresh.reset(); m_optionsRefresh.reset();
if (!success) { if (!success) {
return; return;
} }
const auto options = response.array(); const auto options = response.array();
bool dirty = false; bool dirty = false;
for (const auto& o : std::as_const(options)) { for (const auto& o : std::as_const(options)) {
const auto obj = o.toObject(); const auto obj = o.toObject();
const auto key = obj.value("value").toString(); const auto key = obj.value(QStringLiteral("value")).toString();
const auto value = obj.value("data").toObject().value("current").toVariant(); const auto value = obj.value(QStringLiteral("data")).toObject().value(QStringLiteral("current")).toVariant();
if (m_options.value(key) != value) {
dirty = true;
m_options.insert(key, value);
}
}
if (dirty) { if (m_options.value(key) != value) {
emit optionsChanged(); dirty = true;
} m_options.insert(key, value);
}); }
}
if (dirty) {
emit optionsChanged();
}
});
} }
void HyprExtras::refreshDevices() { void HyprExtras::refreshDevices() {
if (!m_devicesRefresh.isNull()) { if (!m_devicesRefresh.isNull()) {
m_devicesRefresh->close(); m_devicesRefresh->close();
} }
m_devicesRefresh = makeRequestJson("devices", [this](bool success, const QJsonDocument& response) { m_devicesRefresh = makeRequestJson(QStringLiteral("devices"), [this](bool success, const QJsonDocument& response) {
m_devicesRefresh.reset(); m_devicesRefresh.reset();
if (success) { if (success) {
m_devices->updateLastIpcObject(response.object()); m_devices->updateLastIpcObject(response.object());
} }
}); });
} }
void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const { void HyprExtras::socketError(QLocalSocket::LocalSocketError error) const {
if (!m_socketValid) { if (!m_socketValid) {
qWarning() << "HyprExtras::socketError: unable to connect to Hyprland event socket:" << error; qCWarning(lcHypr) << "socketError: unable to connect to Hyprland event socket:" << error;
} else { } else {
qWarning() << "HyprExtras::socketError: Hyprland event socket error:" << error; qCWarning(lcHypr) << "socketError: Hyprland event socket error:" << error;
} }
} }
void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) { void HyprExtras::socketStateChanged(QLocalSocket::LocalSocketState state) {
if (state == QLocalSocket::UnconnectedState && m_socketValid) { if (state == QLocalSocket::UnconnectedState && m_socketValid) {
qWarning() << "HyprExtras::socketStateChanged: Hyprland event socket disconnected."; qCWarning(lcHypr) << "socketStateChanged: Hyprland event socket disconnected.";
} }
m_socketValid = state == QLocalSocket::ConnectedState; m_socketValid = state == QLocalSocket::ConnectedState;
} }
void HyprExtras::readEvent() { void HyprExtras::readEvent() {
while (true) { while (true) {
auto rawEvent = m_socket->readLine(); auto rawEvent = m_socket->readLine();
if (rawEvent.isEmpty()) { if (rawEvent.isEmpty()) {
break; break;
} }
rawEvent.truncate(rawEvent.length() - 1); // Remove trailing \n rawEvent.truncate(rawEvent.length() - 1);
const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>")); const auto event = QByteArrayView(rawEvent.data(), rawEvent.indexOf(">>"));
handleEvent(QString::fromUtf8(event)); handleEvent(QString::fromUtf8(event));
} }
} }
void HyprExtras::handleEvent(const QString& event) { void HyprExtras::handleEvent(const QString& event) {
if (event == "configreloaded") { if (event == QStringLiteral("configreloaded")) {
refreshOptions(); refreshOptions();
} else if (event == "activelayout") { } else if (event == QStringLiteral("activelayout")) {
refreshDevices(); refreshDevices();
} }
} }
HyprExtras::SocketPtr HyprExtras::makeRequestJson( HyprExtras::SocketPtr HyprExtras::makeRequestJson(
const QString& request, const std::function<void(bool, QJsonDocument)>& callback) { const QString& request, const std::function<void(bool, QJsonDocument)>& callback) {
return makeRequest("j/" + request, [callback](bool success, const QByteArray& response) { return makeRequest(QStringLiteral("j/") + request, [callback](bool success, const QByteArray& response) {
callback(success, QJsonDocument::fromJson(response)); callback(success, QJsonDocument::fromJson(response));
}); });
} }
HyprExtras::SocketPtr HyprExtras::makeRequest( HyprExtras::SocketPtr HyprExtras::makeRequest(
const QString& request, const std::function<void(bool, QByteArray)>& callback) { const QString& request, const std::function<void(bool, QByteArray)>& callback) {
if (m_requestSocket.isEmpty()) { if (m_requestSocket.isEmpty()) {
return SocketPtr(); return SocketPtr();
} }
auto socket = SocketPtr::create(this); auto socket = SocketPtr::create(this);
QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() { QObject::connect(socket.data(), &QLocalSocket::connected, this, [=, this]() {
QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() { QObject::connect(socket.data(), &QLocalSocket::readyRead, this, [socket, callback]() {
const auto response = socket->readAll(); const auto response = socket->readAll();
callback(true, std::move(response)); callback(true, std::move(response));
socket->close(); socket->close();
}); });
socket->write(request.toUtf8()); socket->write(request.toUtf8());
socket->flush(); socket->flush();
}); });
QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) { QObject::connect(socket.data(), &QLocalSocket::errorOccurred, this, [=](QLocalSocket::LocalSocketError err) {
qWarning() << "HyprExtras::makeRequest: error making request:" << err << "| request:" << request; qCWarning(lcHypr) << "makeRequest: error making request:" << err << "| request:" << request;
callback(false, {}); callback(false, {});
socket->close(); socket->close();
}); });
socket->connectToServer(m_requestSocket); socket->connectToServer(m_requestSocket);
return socket; return socket;
} }
} // namespace ZShell::internal::hypr } // namespace ZShell::internal::hypr
+34 -30
View File
@@ -1,56 +1,60 @@
#pragma once #pragma once
#include "hyprdevices.hpp"
#include <qlocalsocket.h> #include <qlocalsocket.h>
#include <qobject.h> #include <qobject.h>
#include <qqmlintegration.h> #include <qqmlintegration.h>
#include <qsharedpointer.h>
#include <qvariant.h>
namespace ZShell::internal::hypr { namespace ZShell::internal::hypr {
class HyprExtras : public QObject { class HyprDevices;
Q_OBJECT
QML_ELEMENT
Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged) class HyprExtras : public QObject {
Q_PROPERTY(ZShell::internal::hypr::HyprDevices* devices READ devices CONSTANT) Q_OBJECT
QML_ELEMENT
Q_MOC_INCLUDE("hyprdevices.hpp")
Q_PROPERTY(QVariantHash options READ options NOTIFY optionsChanged)
Q_PROPERTY(ZShell::internal::hypr::HyprDevices* devices READ devices CONSTANT)
public: public:
explicit HyprExtras(QObject* parent = nullptr); explicit HyprExtras(QObject* parent = nullptr);
[[nodiscard]] QVariantHash options() const; [[nodiscard]] QVariantHash options() const;
[[nodiscard]] HyprDevices* devices() const; [[nodiscard]] HyprDevices* devices() const;
Q_INVOKABLE void message(const QString& message); Q_INVOKABLE void message(const QString& message);
Q_INVOKABLE void batchMessage(const QStringList& messages); Q_INVOKABLE void batchMessage(const QStringList& messages);
Q_INVOKABLE void applyOptions(const QVariantHash& options); Q_INVOKABLE void applyOptions(const QVariantHash& options);
Q_INVOKABLE void refreshOptions(); Q_INVOKABLE void refreshOptions();
Q_INVOKABLE void refreshDevices(); Q_INVOKABLE void refreshDevices();
signals: signals:
void optionsChanged(); void optionsChanged();
private: private:
using SocketPtr = QSharedPointer<QLocalSocket>; using SocketPtr = QSharedPointer<QLocalSocket>;
QString m_requestSocket; QString m_requestSocket;
QString m_eventSocket; QString m_eventSocket;
QLocalSocket* m_socket; QLocalSocket* m_socket;
bool m_socketValid; bool m_socketValid;
QVariantHash m_options; QVariantHash m_options;
HyprDevices* const m_devices; HyprDevices* const m_devices;
SocketPtr m_optionsRefresh; SocketPtr m_optionsRefresh;
SocketPtr m_devicesRefresh; SocketPtr m_devicesRefresh;
void socketError(QLocalSocket::LocalSocketError error) const; void socketError(QLocalSocket::LocalSocketError error) const;
void socketStateChanged(QLocalSocket::LocalSocketState state); void socketStateChanged(QLocalSocket::LocalSocketState state);
void readEvent(); void readEvent();
void handleEvent(const QString& event); void handleEvent(const QString& event);
SocketPtr makeRequestJson(const QString& request, const std::function<void(bool, QJsonDocument)>& callback); SocketPtr makeRequestJson(const QString& request, const std::function<void(bool, QJsonDocument)>& callback);
SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback); SocketPtr makeRequest(const QString& request, const std::function<void(bool, QByteArray)>& callback);
}; };
} // namespace ZShell::internal::hypr } // namespace ZShell::internal::hypr
+302 -311
View File
@@ -7,473 +7,464 @@
namespace ZShell::models { namespace ZShell::models {
FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent) FileSystemEntry::FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent)
: QObject(parent) : QObject(parent)
, m_fileInfo(path) , m_fileInfo(path)
, m_path(path) , m_path(path)
, m_relativePath(relativePath) , m_relativePath(relativePath)
, m_isImageInitialised(false) , m_isImageInitialised(false)
, m_mimeTypeInitialised(false) {} , m_mimeTypeInitialised(false) {
}
QString FileSystemEntry::path() const { QString FileSystemEntry::path() const {
return m_path; return m_path;
}; };
QString FileSystemEntry::relativePath() const { QString FileSystemEntry::relativePath() const {
return m_relativePath; return m_relativePath;
}; };
QString FileSystemEntry::name() const { QString FileSystemEntry::name() const {
return m_fileInfo.fileName(); return m_fileInfo.fileName();
}; };
QString FileSystemEntry::baseName() const { QString FileSystemEntry::baseName() const {
return m_fileInfo.baseName(); return m_fileInfo.baseName();
}; };
QString FileSystemEntry::parentDir() const { QString FileSystemEntry::parentDir() const {
return m_fileInfo.absolutePath(); return m_fileInfo.absolutePath();
}; };
QString FileSystemEntry::suffix() const { QString FileSystemEntry::suffix() const {
return m_fileInfo.completeSuffix(); return m_fileInfo.completeSuffix();
}; };
qint64 FileSystemEntry::size() const { qint64 FileSystemEntry::size() const {
return m_fileInfo.size(); return m_fileInfo.size();
}; };
bool FileSystemEntry::isDir() const { bool FileSystemEntry::isDir() const {
return m_fileInfo.isDir(); return m_fileInfo.isDir();
}; };
bool FileSystemEntry::isImage() const { bool FileSystemEntry::isImage() const {
if (!m_isImageInitialised) { if (!m_isImageInitialised) {
QImageReader reader(m_path); QImageReader reader(m_path);
m_isImage = reader.canRead(); m_isImage = reader.canRead();
m_isImageInitialised = true; m_isImageInitialised = true;
} }
return m_isImage; return m_isImage;
} }
QString FileSystemEntry::mimeType() const { QString FileSystemEntry::mimeType() const {
if (!m_mimeTypeInitialised) { if (!m_mimeTypeInitialised) {
const QMimeDatabase db; static const QMimeDatabase s_db;
m_mimeType = db.mimeTypeForFile(m_path).name(); m_mimeType = s_db.mimeTypeForFile(m_path).name();
m_mimeTypeInitialised = true; m_mimeTypeInitialised = true;
} }
return m_mimeType; return m_mimeType;
} }
void FileSystemEntry::updateRelativePath(const QDir& dir) { void FileSystemEntry::updateRelativePath(const QDir& dir) {
const auto relPath = dir.relativeFilePath(m_path); const auto relPath = dir.relativeFilePath(m_path);
if (m_relativePath != relPath) { if (m_relativePath != relPath) {
m_relativePath = relPath; m_relativePath = relPath;
emit relativePathChanged(); emit relativePathChanged();
} }
} }
FileSystemModel::FileSystemModel(QObject* parent) FileSystemModel::FileSystemModel(QObject* parent)
: QAbstractListModel(parent) : QAbstractListModel(parent)
, m_recursive(false) , m_recursive(false)
, m_watchChanges(true) , m_watchChanges(true)
, m_showHidden(false) , m_showHidden(false)
, m_filter(NoFilter) { , m_filter(NoFilter) {
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::watchDirIfRecursive);
connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir); connect(&m_watcher, &QFileSystemWatcher::directoryChanged, this, &FileSystemModel::updateEntriesForDir);
} }
int FileSystemModel::rowCount(const QModelIndex& parent) const { int FileSystemModel::rowCount(const QModelIndex& parent) const {
if (parent != QModelIndex()) { if (parent != QModelIndex()) {
return 0; return 0;
} }
return static_cast<int>(m_entries.size()); return static_cast<int>(m_entries.size());
} }
QVariant FileSystemModel::data(const QModelIndex& index, int role) const { QVariant FileSystemModel::data(const QModelIndex& index, int role) const {
if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) { if (role != Qt::UserRole || !index.isValid() || index.row() >= m_entries.size()) {
return QVariant(); return QVariant();
} }
return QVariant::fromValue(m_entries.at(index.row())); return QVariant::fromValue(m_entries.at(index.row()));
} }
QHash<int, QByteArray> FileSystemModel::roleNames() const { QHash<int, QByteArray> FileSystemModel::roleNames() const {
return { { Qt::UserRole, "modelData" } }; return { { Qt::UserRole, "modelData" } };
} }
QString FileSystemModel::path() const { QString FileSystemModel::path() const {
return m_path; return m_path;
} }
void FileSystemModel::setPath(const QString& path) { void FileSystemModel::setPath(const QString& path) {
if (m_path == path) { if (m_path == path) {
return; return;
} }
m_path = path; m_path = path;
emit pathChanged(); emit pathChanged();
m_dir.setPath(m_path); m_dir.setPath(m_path);
for (const auto& entry : std::as_const(m_entries)) { for (const auto& entry : std::as_const(m_entries)) {
entry->updateRelativePath(m_dir); entry->updateRelativePath(m_dir);
} }
update(); update();
} }
bool FileSystemModel::recursive() const { bool FileSystemModel::recursive() const {
return m_recursive; return m_recursive;
} }
void FileSystemModel::setRecursive(bool recursive) { void FileSystemModel::setRecursive(bool recursive) {
if (m_recursive == recursive) { if (m_recursive == recursive) {
return; return;
} }
m_recursive = recursive; m_recursive = recursive;
emit recursiveChanged(); emit recursiveChanged();
update(); update();
} }
bool FileSystemModel::watchChanges() const { bool FileSystemModel::watchChanges() const {
return m_watchChanges; return m_watchChanges;
} }
void FileSystemModel::setWatchChanges(bool watchChanges) { void FileSystemModel::setWatchChanges(bool watchChanges) {
if (m_watchChanges == watchChanges) { if (m_watchChanges == watchChanges) {
return; return;
} }
m_watchChanges = watchChanges; m_watchChanges = watchChanges;
emit watchChangesChanged(); emit watchChangesChanged();
update(); update();
} }
bool FileSystemModel::showHidden() const { bool FileSystemModel::showHidden() const {
return m_showHidden; return m_showHidden;
} }
void FileSystemModel::setShowHidden(bool showHidden) { void FileSystemModel::setShowHidden(bool showHidden) {
if (m_showHidden == showHidden) { if (m_showHidden == showHidden) {
return; return;
} }
m_showHidden = showHidden; m_showHidden = showHidden;
emit showHiddenChanged(); emit showHiddenChanged();
update(); update();
} }
bool FileSystemModel::sortReverse() const { bool FileSystemModel::sortReverse() const {
return m_sortReverse; return m_sortReverse;
} }
void FileSystemModel::setSortReverse(bool sortReverse) { void FileSystemModel::setSortReverse(bool sortReverse) {
if (m_sortReverse == sortReverse) { if (m_sortReverse == sortReverse) {
return; return;
} }
m_sortReverse = sortReverse; m_sortReverse = sortReverse;
emit sortReverseChanged(); emit sortReverseChanged();
update(); update();
} }
FileSystemModel::Filter FileSystemModel::filter() const { FileSystemModel::Filter FileSystemModel::filter() const {
return m_filter; return m_filter;
} }
void FileSystemModel::setFilter(Filter filter) { void FileSystemModel::setFilter(Filter filter) {
if (m_filter == filter) { if (m_filter == filter) {
return; return;
} }
m_filter = filter; m_filter = filter;
emit filterChanged(); emit filterChanged();
update(); update();
} }
QStringList FileSystemModel::nameFilters() const { QStringList FileSystemModel::nameFilters() const {
return m_nameFilters; return m_nameFilters;
} }
void FileSystemModel::setNameFilters(const QStringList& nameFilters) { void FileSystemModel::setNameFilters(const QStringList& nameFilters) {
if (m_nameFilters == nameFilters) { if (m_nameFilters == nameFilters) {
return; return;
} }
m_nameFilters = nameFilters; m_nameFilters = nameFilters;
emit nameFiltersChanged(); emit nameFiltersChanged();
update(); update();
} }
QQmlListProperty<FileSystemEntry> FileSystemModel::entries() { QQmlListProperty<FileSystemEntry> FileSystemModel::entries() {
return QQmlListProperty<FileSystemEntry>(this, &m_entries); return QQmlListProperty<FileSystemEntry>(this, &m_entries);
} }
void FileSystemModel::watchDirIfRecursive(const QString& path) { void FileSystemModel::watchDirIfRecursive(const QString& path) {
if (m_recursive && m_watchChanges) { if (m_recursive && m_watchChanges) {
const auto currentDir = m_dir; const auto currentDir = m_dir;
const bool showHidden = m_showHidden; const bool showHidden = m_showHidden;
const auto future = QtConcurrent::run([showHidden, path]() { auto future = QtConcurrent::run([showHidden, path]() {
QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot; QDir::Filters filters = QDir::Dirs | QDir::NoDotAndDotDot;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
QDirIterator iter(path, filters, QDirIterator::Subdirectories); QDirIterator iter(path, filters, QDirIterator::Subdirectories);
QStringList dirs; QStringList dirs;
while (iter.hasNext()) { while (iter.hasNext()) {
dirs << iter.next(); dirs << iter.next();
} }
return dirs; return dirs;
}); });
const auto watcher = new QFutureWatcher<QStringList>(this); future.then(this, [currentDir, showHidden, this](const QStringList& paths) {
connect(watcher, &QFutureWatcher<QStringList>::finished, this, [currentDir, showHidden, watcher, this]() { if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) {
const auto paths = watcher->result(); // Ignore if dir or showHidden has changed
if (currentDir == m_dir && showHidden == m_showHidden && !paths.isEmpty()) { m_watcher.addPaths(paths);
// Ignore if dir or showHidden has changed }
m_watcher.addPaths(paths); });
} }
watcher->deleteLater();
});
watcher->setFuture(future);
}
} }
void FileSystemModel::update() { void FileSystemModel::update() {
updateWatcher(); updateWatcher();
updateEntries(); updateEntries();
} }
void FileSystemModel::updateWatcher() { void FileSystemModel::updateWatcher() {
if (!m_watcher.directories().isEmpty()) { if (!m_watcher.directories().isEmpty()) {
m_watcher.removePaths(m_watcher.directories()); m_watcher.removePaths(m_watcher.directories());
} }
if (!m_watchChanges || m_path.isEmpty()) { if (!m_watchChanges || m_path.isEmpty()) {
return; return;
} }
m_watcher.addPath(m_path); m_watcher.addPath(m_path);
watchDirIfRecursive(m_path); watchDirIfRecursive(m_path);
} }
void FileSystemModel::updateEntries() { void FileSystemModel::updateEntries() {
if (m_path.isEmpty()) { if (m_path.isEmpty()) {
if (!m_entries.isEmpty()) { if (!m_entries.isEmpty()) {
beginResetModel(); beginResetModel();
qDeleteAll(m_entries); qDeleteAll(m_entries);
m_entries.clear(); m_entries.clear();
endResetModel(); endResetModel();
emit entriesChanged(); emit entriesChanged();
} }
return; return;
} }
for (auto& future : m_futures) { for (auto& future : m_futures) {
future.cancel(); future.cancel();
} }
m_futures.clear(); m_futures.clear();
updateEntriesForDir(m_path); updateEntriesForDir(m_path);
} }
void FileSystemModel::updateEntriesForDir(const QString& dir) { void FileSystemModel::updateEntriesForDir(const QString& dir) {
const auto recursive = m_recursive; const auto recursive = m_recursive;
const auto showHidden = m_showHidden; const auto showHidden = m_showHidden;
const auto filter = m_filter; const auto filter = m_filter;
const auto nameFilters = m_nameFilters; const auto nameFilters = m_nameFilters;
QSet<QString> oldPaths; QSet<QString> oldPaths;
for (const auto& entry : std::as_const(m_entries)) { for (const auto& entry : std::as_const(m_entries)) {
oldPaths << entry->path(); oldPaths << entry->path();
} }
const auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString>>>& promise) { auto future = QtConcurrent::run([=](QPromise<QPair<QSet<QString>, QSet<QString> > >& promise) {
const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags; const auto flags = recursive ? QDirIterator::Subdirectories : QDirIterator::NoIteratorFlags;
std::optional<QDirIterator> iter; std::optional<QDirIterator> iter;
if (filter == Images) { if (filter == Images) {
QStringList extraNameFilters = nameFilters; QStringList extraNameFilters = nameFilters;
const auto formats = QImageReader::supportedImageFormats(); const auto formats = QImageReader::supportedImageFormats();
for (const auto& format : formats) { for (const auto& format : formats) {
extraNameFilters << "*." + format; extraNameFilters << "*." + format;
} }
QDir::Filters filters = QDir::Files; QDir::Filters filters = QDir::Files;
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
iter.emplace(dir, extraNameFilters, filters, flags); iter.emplace(dir, extraNameFilters, filters, flags);
} else { } else {
QDir::Filters filters; QDir::Filters filters;
if (filter == Files) { if (filter == Files) {
filters = QDir::Files; filters = QDir::Files;
} else if (filter == Dirs) { } else if (filter == Dirs) {
filters = QDir::Dirs | QDir::NoDotAndDotDot; filters = QDir::Dirs | QDir::NoDotAndDotDot;
} else { } else {
filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot; filters = QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot;
} }
if (showHidden) { if (showHidden) {
filters |= QDir::Hidden; filters |= QDir::Hidden;
} }
if (nameFilters.isEmpty()) { if (nameFilters.isEmpty()) {
iter.emplace(dir, filters, flags); iter.emplace(dir, filters, flags);
} else { } else {
iter.emplace(dir, nameFilters, filters, flags); iter.emplace(dir, nameFilters, filters, flags);
} }
} }
QSet<QString> newPaths; QSet<QString> newPaths;
while (iter->hasNext()) { while (iter->hasNext()) {
if (promise.isCanceled()) { if (promise.isCanceled()) {
return; return;
} }
QString path = iter->next(); QString path = iter->next();
if (filter == Images) { if (filter == Images) {
QImageReader reader(path); QImageReader reader(path);
if (!reader.canRead()) { if (!reader.canRead()) {
continue; continue;
} }
} }
newPaths.insert(path); newPaths.insert(path);
} }
if (promise.isCanceled() || newPaths == oldPaths) { if (promise.isCanceled()) {
return; return;
} }
promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths)); promise.addResult(qMakePair(oldPaths - newPaths, newPaths - oldPaths));
}); });
if (m_futures.contains(dir)) { if (m_futures.contains(dir)) {
m_futures[dir].cancel(); m_futures[dir].cancel();
} }
m_futures.insert(dir, future); m_futures.insert(dir, future);
const auto watcher = new QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>(this); future
.then(this,
connect(watcher, &QFutureWatcher<QPair<QSet<QString>, QSet<QString>>>::finished, this, [dir, watcher, this]() { [dir, this](QPair<QSet<QString>, QSet<QString> > result) {
m_futures.remove(dir); m_futures.remove(dir);
if (!result.first.isEmpty() || !result.second.isEmpty()) {
if (!watcher->future().isResultReadyAt(0)) { applyChanges(result.first, result.second);
watcher->deleteLater(); }
return; })
} .onCanceled(this, [dir, this]() {
m_futures.remove(dir);
const auto result = watcher->result(); });
applyChanges(result.first, result.second);
watcher->deleteLater();
});
watcher->setFuture(future);
} }
void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) { void FileSystemModel::applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths) {
QList<int> removedIndices; QList<int> removedIndices;
for (int i = 0; i < m_entries.size(); ++i) { for (int i = 0; i < m_entries.size(); ++i) {
if (removedPaths.contains(m_entries[i]->path())) { if (removedPaths.contains(m_entries[i]->path())) {
removedIndices << i; removedIndices << i;
} }
} }
std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>()); std::sort(removedIndices.begin(), removedIndices.end(), std::greater<int>());
// Batch remove old entries // Batch remove old entries
int start = -1; int start = -1;
int end = -1; int end = -1;
for (int idx : std::as_const(removedIndices)) { for (int idx : std::as_const(removedIndices)) {
if (start == -1) { if (start == -1) {
start = idx; start = idx;
end = idx; end = idx;
} else if (idx == end - 1) { } else if (idx == end - 1) {
end = idx; end = idx;
} else { } else {
beginRemoveRows(QModelIndex(), end, start); beginRemoveRows(QModelIndex(), end, start);
for (int i = start; i >= end; --i) { for (int i = start; i >= end; --i) {
m_entries.takeAt(i)->deleteLater(); m_entries.takeAt(i)->deleteLater();
} }
endRemoveRows(); endRemoveRows();
start = idx; start = idx;
end = idx; end = idx;
} }
} }
if (start != -1) { if (start != -1) {
beginRemoveRows(QModelIndex(), end, start); beginRemoveRows(QModelIndex(), end, start);
for (int i = start; i >= end; --i) { for (int i = start; i >= end; --i) {
m_entries.takeAt(i)->deleteLater(); m_entries.takeAt(i)->deleteLater();
} }
endRemoveRows(); endRemoveRows();
} }
// Create new entries // Create new entries
QList<FileSystemEntry*> newEntries; QList<FileSystemEntry*> newEntries;
for (const auto& path : addedPaths) { for (const auto& path : addedPaths) {
newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this); newEntries << new FileSystemEntry(path, m_dir.relativeFilePath(path), this);
} }
std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) { std::sort(newEntries.begin(), newEntries.end(), [this](const FileSystemEntry* a, const FileSystemEntry* b) {
return compareEntries(a, b); return compareEntries(a, b);
}); });
// Batch insert new entries // Batch insert new entries
int insertStart = -1; int insertStart = -1;
QList<FileSystemEntry*> batchItems; QList<FileSystemEntry*> batchItems;
for (const auto& entry : std::as_const(newEntries)) { for (const auto& entry : std::as_const(newEntries)) {
const auto it = std::lower_bound( const auto it = std::lower_bound(
m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) { m_entries.begin(), m_entries.end(), entry, [this](const FileSystemEntry* a, const FileSystemEntry* b) {
return compareEntries(a, b); return compareEntries(a, b);
}); });
const auto row = static_cast<int>(it - m_entries.begin()); const auto row = static_cast<int>(it - m_entries.begin());
if (insertStart == -1) { if (insertStart == -1) {
insertStart = row; insertStart = row;
batchItems << entry; batchItems << entry;
} else if (row == insertStart + batchItems.size()) { } else if (row == insertStart + batchItems.size()) {
batchItems << entry; batchItems << entry;
} else { } else {
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1); beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
for (int i = 0; i < batchItems.size(); ++i) { for (int i = 0; i < batchItems.size(); ++i) {
m_entries.insert(insertStart + i, batchItems[i]); m_entries.insert(insertStart + i, batchItems[i]);
} }
endInsertRows(); endInsertRows();
insertStart = row; insertStart = row;
batchItems.clear(); batchItems.clear();
batchItems << entry; batchItems << entry;
} }
} }
if (!batchItems.isEmpty()) { if (!batchItems.isEmpty()) {
beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1); beginInsertRows(QModelIndex(), insertStart, insertStart + static_cast<int>(batchItems.size()) - 1);
for (int i = 0; i < batchItems.size(); ++i) { for (int i = 0; i < batchItems.size(); ++i) {
m_entries.insert(insertStart + i, batchItems[i]); m_entries.insert(insertStart + i, batchItems[i]);
} }
endInsertRows(); endInsertRows();
} }
emit entriesChanged(); emit entriesChanged();
} }
bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const { bool FileSystemModel::compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const {
if (a->isDir() != b->isDir()) { if (a->isDir() != b->isDir()) {
return m_sortReverse ^ a->isDir(); return m_sortReverse ^ a->isDir();
} }
const auto cmp = a->relativePath().localeAwareCompare(b->relativePath()); const auto cmp = a->relativePath().localeAwareCompare(b->relativePath());
return m_sortReverse ? cmp > 0 : cmp < 0; return m_sortReverse ? cmp > 0 : cmp < 0;
} }
} // namespace ZShell::models } // namespace ZShell::models
+95 -95
View File
@@ -13,136 +13,136 @@
namespace ZShell::models { namespace ZShell::models {
class FileSystemEntry : public QObject { class FileSystemEntry : public QObject {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel") QML_UNCREATABLE("FileSystemEntry instances can only be retrieved from a FileSystemModel")
Q_PROPERTY(QString path READ path CONSTANT) Q_PROPERTY(QString path READ path CONSTANT)
Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged) Q_PROPERTY(QString relativePath READ relativePath NOTIFY relativePathChanged)
Q_PROPERTY(QString name READ name CONSTANT) Q_PROPERTY(QString name READ name CONSTANT)
Q_PROPERTY(QString baseName READ baseName CONSTANT) Q_PROPERTY(QString baseName READ baseName CONSTANT)
Q_PROPERTY(QString parentDir READ parentDir CONSTANT) Q_PROPERTY(QString parentDir READ parentDir CONSTANT)
Q_PROPERTY(QString suffix READ suffix CONSTANT) Q_PROPERTY(QString suffix READ suffix CONSTANT)
Q_PROPERTY(qint64 size READ size CONSTANT) Q_PROPERTY(qint64 size READ size CONSTANT)
Q_PROPERTY(bool isDir READ isDir CONSTANT) Q_PROPERTY(bool isDir READ isDir CONSTANT)
Q_PROPERTY(bool isImage READ isImage CONSTANT) Q_PROPERTY(bool isImage READ isImage CONSTANT)
Q_PROPERTY(QString mimeType READ mimeType CONSTANT) Q_PROPERTY(QString mimeType READ mimeType CONSTANT)
public: public:
explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr); explicit FileSystemEntry(const QString& path, const QString& relativePath, QObject* parent = nullptr);
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
[[nodiscard]] QString relativePath() const; [[nodiscard]] QString relativePath() const;
[[nodiscard]] QString name() const; [[nodiscard]] QString name() const;
[[nodiscard]] QString baseName() const; [[nodiscard]] QString baseName() const;
[[nodiscard]] QString parentDir() const; [[nodiscard]] QString parentDir() const;
[[nodiscard]] QString suffix() const; [[nodiscard]] QString suffix() const;
[[nodiscard]] qint64 size() const; [[nodiscard]] qint64 size() const;
[[nodiscard]] bool isDir() const; [[nodiscard]] bool isDir() const;
[[nodiscard]] bool isImage() const; [[nodiscard]] bool isImage() const;
[[nodiscard]] QString mimeType() const; [[nodiscard]] QString mimeType() const;
void updateRelativePath(const QDir& dir); void updateRelativePath(const QDir& dir);
signals: signals:
void relativePathChanged(); void relativePathChanged();
private: private:
const QFileInfo m_fileInfo; const QFileInfo m_fileInfo;
const QString m_path; const QString m_path;
QString m_relativePath; QString m_relativePath;
mutable bool m_isImage; mutable bool m_isImage;
mutable bool m_isImageInitialised; mutable bool m_isImageInitialised;
mutable QString m_mimeType; mutable QString m_mimeType;
mutable bool m_mimeTypeInitialised; mutable bool m_mimeTypeInitialised;
}; };
class FileSystemModel : public QAbstractListModel { class FileSystemModel : public QAbstractListModel {
Q_OBJECT Q_OBJECT
QML_ELEMENT QML_ELEMENT
Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged) Q_PROPERTY(QString path READ path WRITE setPath NOTIFY pathChanged)
Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged) Q_PROPERTY(bool recursive READ recursive WRITE setRecursive NOTIFY recursiveChanged)
Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged) Q_PROPERTY(bool watchChanges READ watchChanges WRITE setWatchChanges NOTIFY watchChangesChanged)
Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged) Q_PROPERTY(bool showHidden READ showHidden WRITE setShowHidden NOTIFY showHiddenChanged)
Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged) Q_PROPERTY(bool sortReverse READ sortReverse WRITE setSortReverse NOTIFY sortReverseChanged)
Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged) Q_PROPERTY(Filter filter READ filter WRITE setFilter NOTIFY filterChanged)
Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged) Q_PROPERTY(QStringList nameFilters READ nameFilters WRITE setNameFilters NOTIFY nameFiltersChanged)
Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged) Q_PROPERTY(QQmlListProperty<ZShell::models::FileSystemEntry> entries READ entries NOTIFY entriesChanged)
public: public:
enum Filter { enum Filter {
NoFilter, NoFilter,
Images, Images,
Files, Files,
Dirs Dirs
}; };
Q_ENUM(Filter) Q_ENUM(Filter)
explicit FileSystemModel(QObject* parent = nullptr); explicit FileSystemModel(QObject* parent = nullptr);
int rowCount(const QModelIndex& parent = QModelIndex()) const override; int rowCount(const QModelIndex& parent = QModelIndex()) const override;
QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex& index, int role = Qt::DisplayRole) const override;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
[[nodiscard]] QString path() const; [[nodiscard]] QString path() const;
void setPath(const QString& path); void setPath(const QString& path);
[[nodiscard]] bool recursive() const; [[nodiscard]] bool recursive() const;
void setRecursive(bool recursive); void setRecursive(bool recursive);
[[nodiscard]] bool watchChanges() const; [[nodiscard]] bool watchChanges() const;
void setWatchChanges(bool watchChanges); void setWatchChanges(bool watchChanges);
[[nodiscard]] bool showHidden() const; [[nodiscard]] bool showHidden() const;
void setShowHidden(bool showHidden); void setShowHidden(bool showHidden);
[[nodiscard]] bool sortReverse() const; [[nodiscard]] bool sortReverse() const;
void setSortReverse(bool sortReverse); void setSortReverse(bool sortReverse);
[[nodiscard]] Filter filter() const; [[nodiscard]] Filter filter() const;
void setFilter(Filter filter); void setFilter(Filter filter);
[[nodiscard]] QStringList nameFilters() const; [[nodiscard]] QStringList nameFilters() const;
void setNameFilters(const QStringList& nameFilters); void setNameFilters(const QStringList& nameFilters);
[[nodiscard]] QQmlListProperty<FileSystemEntry> entries(); [[nodiscard]] QQmlListProperty<FileSystemEntry> entries();
signals: signals:
void pathChanged(); void pathChanged();
void recursiveChanged(); void recursiveChanged();
void watchChangesChanged(); void watchChangesChanged();
void showHiddenChanged(); void showHiddenChanged();
void sortReverseChanged(); void sortReverseChanged();
void filterChanged(); void filterChanged();
void nameFiltersChanged(); void nameFiltersChanged();
void entriesChanged(); void entriesChanged();
private: private:
QDir m_dir; QDir m_dir;
QFileSystemWatcher m_watcher; QFileSystemWatcher m_watcher;
QList<FileSystemEntry*> m_entries; QList<FileSystemEntry*> m_entries;
QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString>>>> m_futures; QHash<QString, QFuture<QPair<QSet<QString>, QSet<QString> > > > m_futures;
QString m_path; QString m_path;
bool m_recursive; bool m_recursive;
bool m_watchChanges; bool m_watchChanges;
bool m_showHidden; bool m_showHidden;
bool m_sortReverse; bool m_sortReverse = false;
Filter m_filter; Filter m_filter;
QStringList m_nameFilters; QStringList m_nameFilters;
void watchDirIfRecursive(const QString& path); void watchDirIfRecursive(const QString& path);
void update(); void update();
void updateWatcher(); void updateWatcher();
void updateEntries(); void updateEntries();
void updateEntriesForDir(const QString& dir); void updateEntriesForDir(const QString& dir);
void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths); void applyChanges(const QSet<QString>& removedPaths, const QSet<QString>& addedPaths);
[[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const; [[nodiscard]] bool compareEntries(const FileSystemEntry* a, const FileSystemEntry* b) const;
}; };
} // namespace ZShell::models } // namespace ZShell::models

Some files were not shown because too many files have changed in this diff Show More