259 Commits

Author SHA1 Message Date
kenji 59d1b4a337 removed transform hypr 2025-12-23 06:31:25 -06:00
kenji 151034f4fc updated position of monitor 2025-12-22 19:06:33 -06:00
kenji eb226350fc fixed again 2025-12-22 18:57:32 -06:00
kenji ae081803a5 fixed incorrect formatting 2025-12-22 18:54:33 -06:00
kenji 8cba405b0e added new monitor 2025-12-22 18:51:23 -06:00
kenji c61fe63bae added bluetoothctl 2025-12-13 09:39:44 -06:00
kenji c8d0cec782 changed vrr to 0 2025-12-12 19:42:40 -06:00
kenji a02fd32ee2 changed monitor pos 2025-12-12 19:36:25 -06:00
kenji 6be0a483e8 added general 2025-12-12 14:16:13 -06:00
kenji 3c32424793 fixed the directory 2025-12-12 11:34:02 -06:00
kenji 5535d43146 test 2025-12-12 11:29:10 -06:00
kenji 4f5b41204c hypr: makes exectuable actually executable 2025-12-12 11:25:16 -06:00
kenji 6335cdce16 font: added fonts for foreign languages 2025-12-12 09:28:21 -06:00
kenji ce86247d3e mv switchwall 2025-11-24 17:30:22 -06:00
kenji 580865cb4c a 2025-10-14 18:25:29 -05:00
kenji 8b4cf2c91d replaced bind for grim 2025-09-20 15:11:10 -05:00
kenji ee37fc5f6d screenshot priority changes 2025-08-31 20:59:54 -05:00
kenji 7a5635507e changed DP 2025-08-24 11:59:08 -05:00
kenji 609720f540 a 2025-08-20 15:25:57 -05:00
kenji bec280bcbc updated pkgs hyprland 2025-08-20 15:19:31 -05:00
kenji 1dc5e26712 removed Super, T 2025-08-20 14:30:16 -05:00
kenji c0ad3a4acb added custom conf hypr 2025-08-20 14:28:38 -05:00
kenji 33dc50df7a add force 2025-08-20 14:14:23 -05:00
kenji 604108a093 a 2025-08-20 14:11:29 -05:00
kenji b5960f4363 removed executable 2025-08-20 13:56:47 -05:00
kenji 1654ba6ee4 removed colours.nix 2025-08-20 13:56:16 -05:00
kenji b1484ba42e minor fix 2025-08-20 13:55:56 -05:00
kenji 76edab5b1c removed attr from hypr 2025-08-20 13:54:48 -05:00
kenji c5bb2bf66f fix misspelling 2025-08-20 13:52:39 -05:00
kenji 624606d853 mass migration 2025-08-20 13:52:02 -05:00
kenji be5772ea09 moved wayland again 2025-08-20 13:37:06 -05:00
kenji e6dd387e9f moved wayland 2025-08-20 13:33:11 -05:00
kenji 84ac1c05e2 a 2025-08-20 13:23:21 -05:00
kenji e1ac8eeed1 removed executable
incompatible with builtins.path
2025-08-19 18:40:57 -05:00
kenji 415806ca96 a 2025-08-19 18:39:32 -05:00
kenji 650a5fa73c fix for commit 3c 2025-08-19 18:35:27 -05:00
kenji 3c496175dc converted hyprland conf to nix 2025-08-19 18:17:33 -05:00
kenji b1daadc71e added files 2025-08-19 17:03:16 -05:00
kenji aae0668878 updated monitor 2025-08-19 14:52:03 -05:00
kenji 7495f5ebbc a 2025-08-19 10:05:29 -05:00
kenji d9a4fba05a fix solaar not hidden on launch 2025-08-19 10:01:51 -05:00
kenji 64cf3883aa added lact 2025-08-19 09:55:32 -05:00
kenji 51d1d30a61 fixed solaar not launching 2025-08-18 23:15:09 -05:00
kenji c7e43f10e3 changed vrr to 2 2025-08-18 23:12:07 -05:00
kenji 93bcfd494c removed vrr 2025-08-18 23:10:24 -05:00
kenji b76a1e45ef downgraded resolution 2025-08-18 23:08:14 -05:00
kenji 527a8f2b75 reverted back 2025-08-18 23:03:56 -05:00
kenji 1d57900977 changed bitdepth 2025-08-18 23:00:47 -05:00
kenji 6fd73b8691 added solaar 2025-08-18 18:01:03 -05:00
kenji 40dbb31fbc added movetowindows binds 2025-08-17 09:45:56 -05:00
kenji d729f27755 fixed starship 2025-08-13 09:03:21 -05:00
kenji a270c99361 fix path 2025-08-13 08:59:27 -05:00
kenji dc379f2ee6 organized 2025-08-13 08:58:40 -05:00
kenji 992e54592b a 2025-08-13 08:53:14 -05:00
kenji c502d301b8 a 2025-08-13 08:51:56 -05:00
kenji 58b144792e a 2025-08-13 08:45:56 -05:00
kenji c7d6a04c77 a 2025-08-13 08:44:07 -05:00
kenji a29382966b a 2025-08-13 08:43:02 -05:00
kenji 9d2d722bc6 organized 2025-08-13 08:39:37 -05:00
kenji 490fb9ba35 fixed incorrect binds 2025-08-13 08:26:10 -05:00
kenji 5e7bef47a0 removed starship.enable on desktop.nix 2025-08-13 08:20:56 -05:00
kenji 891cad49f0 added simpleStarship 2025-08-13 08:20:36 -05:00
kenji d6c4327639 added starship enable 2025-08-13 08:14:04 -05:00
kenji fed7deef79 integrated new fonts 2025-08-12 08:18:32 -05:00
kenji d4b1e53471 updated nixos-fonts 2025-08-12 08:16:26 -05:00
kenji 40f9981167 added material symbol 2025-08-11 22:57:23 -05:00
kenji 1902d47c1e added 2025-08-11 22:23:51 -05:00
kenji ae331f28cf added pillow and materialyoucolor 2025-08-11 22:01:25 -05:00
kenji 41f93617ad a 2025-08-11 21:54:43 -05:00
kenji ed8b13037d a 2025-08-11 21:53:46 -05:00
kenji 7d0d6fbd9f a 2025-08-11 21:52:47 -05:00
kenji dcb26342b3 a 2025-08-11 21:52:22 -05:00
kenji 09a70bf980 a 2025-08-11 21:51:37 -05:00
kenji ccf5c9a252 a 2025-08-11 21:50:27 -05:00
kenji 06e277c316 a 2025-08-11 21:49:35 -05:00
kenji dc1c238819 a 2025-08-11 21:43:39 -05:00
kenji 8c5f3537f9 a 2025-08-11 21:41:48 -05:00
kenji 2348290bfb a 2025-08-11 21:39:15 -05:00
kenji 7b047e0719 UNFIX GET PREV 2025-08-11 21:36:53 -05:00
kenji bce41fec66 a 2025-08-11 21:28:49 -05:00
kenji a0d060a8c3 a 2025-08-11 21:19:12 -05:00
kenji c83efbc591 qa 2025-08-11 21:18:45 -05:00
kenji 230917fbb2 a 2025-08-11 21:15:31 -05:00
kenji 0b3db4e61a a 2025-08-11 21:11:51 -05:00
kenji 1a1a2b309f ad 2025-08-11 21:07:24 -05:00
kenji f9129a5fb7 test 2025-08-11 21:04:45 -05:00
kenji 166d9f3f4b added spacegrotesk 2025-08-11 21:03:40 -05:00
kenji cdf0188ec8 test 2025-08-11 20:57:31 -05:00
kenji 99638ade76 a 2025-08-11 20:54:54 -05:00
kenji 33953a915b fixed pkgs 2025-08-11 20:53:30 -05:00
kenji 3ad6d635d6 test 2025-08-11 20:41:47 -05:00
kenji f0ec76847f a 2025-08-11 20:37:50 -05:00
kenji a6c6f7958f fixed nix develop 2025-08-11 20:35:41 -05:00
kenji 4dc566a037 organized 2025-08-11 20:20:02 -05:00
kenji 55d2a9fe6c added vrr on DP-2 2025-08-11 19:39:58 -05:00
kenji db7d0a00f0 changed keybindings
onscreen keyboard and bartoggle
2025-08-11 19:29:48 -05:00
kenji b9a7145c5c flake update 2025-08-11 17:22:12 -05:00
kenji f94de758d2 fix monitor hyprland 2025-08-11 16:40:37 -05:00
kenji a34c685b73 fixed 2025-08-11 16:36:05 -05:00
kenji 16c5b8f999 added custom binds and monitor 2025-08-11 16:27:21 -05:00
kenji 181014b678 removed programs.hyprland 2025-08-11 16:11:47 -05:00
kenji 083be17af4 a 2025-08-11 16:09:38 -05:00
kenji f0d7d6f074 added 2025-08-11 16:08:37 -05:00
kenji d561e4f1ec added home-manager 2025-08-11 16:05:30 -05:00
kenji 686de93cee converted to homeManagerModules 2025-08-11 15:58:04 -05:00
kenji 1493021354 changed path again 2025-08-11 15:39:31 -05:00
kenji 48d7be6682 fixed path 2025-08-11 15:35:14 -05:00
kenji a9c40d493b changed environment.etc 2025-08-11 15:32:25 -05:00
kenji 3d36ead298 attemp 2 2025-08-11 15:28:41 -05:00
kenji 9b1de7c7ce replaced as a test 2025-08-11 15:22:09 -05:00
kenji ce6e885a23 organized 2025-08-11 15:19:40 -05:00
kenji 176ad4ba0b now works! 2025-08-11 14:52:24 -05:00
lsoriano-mcm 9a1ad0057e add 2025-08-09 13:26:24 -05:00
lsoriano-mcm 2327596517 added more environment etc 2025-08-09 12:24:30 -05:00
lsoriano-mcm 71b67cc772 environment etc 2025-08-09 12:18:39 -05:00
lsoriano-mcm 962934e82d added pkgs in cfg 2025-08-09 11:50:56 -05:00
lsoriano-mcm 1528a9f760 added oneUI 2025-08-09 08:37:05 -05:00
lsoriano-mcm 28be4d1bb4 added flakes for nix 2025-08-08 16:43:46 -05:00
lsoriano-mcm ad7ee4ad27 added flake.nix 2025-08-08 11:19:41 -05:00
end-4 db66b85e61 bar: move number showing logic from GlobalStates to Workspaces 2025-08-08 20:24:37 +07:00
end-4 66c810ead2 bar autohide: rename enabled -> enable for consistency 2025-08-08 20:12:50 +07:00
end-4 9824bb9c63 bar: add delay for autohide 2025-08-08 20:06:53 +07:00
end-4 f806e2c22c bar: add auto hide 2025-08-08 19:54:10 +07:00
end-4 3d408b18f7 background: add fade when switching 2025-08-08 18:31:52 +07:00
end-4 8aa776ae62 make bg image loading async 2025-08-08 18:02:10 +07:00
end-4 a15f3b8c65 overview: show windows on other monitors too 2025-08-08 17:55:52 +07:00
end-4 4df22c96d0 screen corners: fix visibility for multimonitor with varying fullscreen state 2025-08-08 17:52:19 +07:00
end-4 772df06fa5 booru: fix inconsistent download 2025-08-08 17:50:20 +07:00
end-4 d3a9d2ea5b Fix hiding background when fullscreen (#1775) 2025-08-08 10:35:55 +07:00
end-4 4914d9b638 Merge branch 'main' into main 2025-08-08 10:35:43 +07:00
end-4 1f8a7be34e quickshell: fix qml null safety and monitor property errors (#1770) 2025-08-08 00:01:31 +07:00
end-4 97bdfa54c0 Overrideable default terminal app (#1753) 2025-08-07 23:57:08 +07:00
end-4 64bb730dd1 touchpad: improve scroll speed handling for touchpad (#1781) 2025-08-07 23:46:39 +07:00
end-4 7013b459a3 adjust scrolling speed 2025-08-07 23:13:07 +07:00
end-4 a31733e2db move scrolling animation to styled components 2025-08-07 22:39:30 +07:00
end-4 199b23d14a add config options for scroll factors and threshold 2025-08-07 22:32:02 +07:00
end-4 f1c1ed833c use StyledListView for SelectionDialog 2025-08-07 22:31:19 +07:00
Souyama 0506917b87 launch_first_available.sh should skip empty cmds 2025-08-07 20:48:11 +05:30
end-4 4f40ba8e6e more intuitive power profiles icons 2025-08-07 22:01:05 +07:00
end-4 733a792610 ai: add usage metadata for openai and mistral 2025-08-07 21:53:37 +07:00
end-4 f581fd4821 config option to (not) filter duplicate media controls 2025-08-07 21:39:48 +07:00
Runze 86ddb61a3f fix(touchpad): differentiate scroll speed between touchpad and mouse wheel 2025-08-07 22:26:26 +08:00
lunstia 6c3451b912 Fix background not always hiding in fullscreen and other monitors hiding background when they're not supposed to 2025-08-07 00:16:26 -04:00
lunstia 35e1dc95a5 Fix background hiding in fullscreen 2025-08-06 05:27:29 -04:00
finjener d70f81bfe4 Merge remote-tracking branch 'upstream/main' into quickshell-fixes 2025-08-05 18:17:02 +01:00
finjener d632111cf9 quickshell: fix qml null safety and monitor property errors 2025-08-04 23:03:00 +01:00
end-4 f8d162d995 RoundCorner: rewrite to use Shape instead of Canvas 2025-08-03 20:40:52 +07:00
end-4 0708070764 circular progress: use implicitSize instead of size
note: the credit is removed because the widget has been rewritten to use Shape instead of Canvas
2025-08-03 19:54:01 +07:00
end-4 87f7bc28a3 chores: remove unnecessary import, suppress init null warnings 2025-08-03 18:17:01 +07:00
end-4 3eb7d8ab58 background: remove unecessary Scope 2025-08-03 18:13:12 +07:00
end-4 71d0ac4c5e make circular progresses use shape instead of canvas 2025-08-03 18:12:44 +07:00
end-4 839593b11e add konsole konfig 2025-08-03 17:31:58 +07:00
end-4 13a0927900 bar: refractor bar content to new file 2025-08-03 16:52:39 +07:00
end-4 00984c599b settings: update keep right sidebar loaded note 2025-08-03 16:51:08 +07:00
end-4 34ca65a180 background: fix wrong anchor 2025-08-03 16:11:24 +07:00
end-4 596ae72942 add config option to keep right sidebar loaded 2025-08-02 20:31:37 +07:00
end-4 88cc91b85a fix laggy search bar anim when overview is disabled 2025-08-02 20:09:18 +07:00
end-4 d4b8ded6c8 overview: allow disabling overview (showing search only) 2025-08-02 17:35:44 +07:00
end-4 86d2a03a0a settings: add monochromize/tint icons toggles 2025-08-02 16:56:19 +07:00
end-4 de1812bf91 sidebar: remove redundant coloroverlay, make uptime more brief 2025-08-02 16:55:56 +07:00
end-4 f36751ff6b sidebar: boorus: always download images manually 2025-08-02 16:01:46 +07:00
end-4 a9273fc225 ai: dont include tool instructions in system prompt 2025-08-02 16:00:24 +07:00
end-4 2a0b12112f i use nyarch btw 2025-08-02 15:45:24 +07:00
end-4 2aea02989f session: fix binding breakage on close (#1754) 2025-08-02 07:25:08 +07:00
Souyama 2b554cf286 Update env.conf
remote direct quotes
2025-08-02 00:28:47 +05:30
sansmoraxz dc2777703d update default terminal value 2025-08-01 22:47:01 +05:30
sansmoraxz 6ae03b545c terminal env var 2025-08-01 22:22:01 +05:30
end-4 8e366cfc84 translations: add ukrainian language file (#1748) 2025-08-01 22:56:27 +07:00
Beengoo 27c2c4fb92 Merge branch 'end-4:main' into main 2025-08-01 15:53:42 +03:00
Beengoo 83af589b27 Correcting localization errors 2025-08-01 15:44:47 +03:00
end-4 7a937833f3 background: parallax on whole workspace group 2025-08-01 08:16:19 +07:00
end-4 4110d2529c ai: dont replace . in ollama model name 2025-07-31 22:28:34 +07:00
Beengoo 1c6c165d78 Added Ukrainian Language 2025-07-31 17:17:48 +03:00
end-4 a5ffb0e021 media controls: actually detect if plasma browser integration is installed 2025-07-31 12:35:39 +07:00
end-4 a08a39b620 qs: handle toggles internally instead of relying on hyprctl dispatch global (#1745) 2025-07-31 12:35:16 +07:00
end-4 968e8195ef background: fix clock positioning 2025-07-30 12:33:55 +07:00
end-4 52ce2f5384 feat(background): show clock for video wallpapers. (#1719) 2025-07-30 07:30:15 +02:00
end-4 cb2d1bc444 Merge branch 'main' into videowall-add-clock 2025-07-30 07:30:05 +02:00
end-4 47b81faf3d Feature: Hyprlock layout indicator (#1718) 2025-07-30 07:15:33 +02:00
end-4 1483761e72 Feature: On-screen keyboard (osk) update on activeLayout event (#1717) 2025-07-30 07:11:05 +02:00
end-4 7f43665e3c Merge branch 'main' into osk-update-on-activelayout-event 2025-07-30 07:10:48 +02:00
end-4 01fcd653ad Fix: Init layout indicator with main keyboard and update on every activeLayout event (#1711) (#1716) 2025-07-30 06:57:40 +02:00
end-4 298e947740 background: hide when fullscreen 2025-07-30 09:46:55 +07:00
end-4 91c2014b7e ai: add mistral 2025-07-30 09:46:42 +07:00
end-4 3018ad16b1 translation: Update Russian translation file (again) (#1741) 2025-07-30 00:44:34 +02:00
Anton Epikhin 1172be241c Returned fade_on_empty for input field and moved layout indicator to bottom right 2025-07-29 22:02:25 +03:00
Vercixx c743b4ab88 Update ru_RU.json 2025-07-29 21:45:56 +03:00
end-4 f6ec718ced translation: Update Russian translation file (#1740) 2025-07-29 16:43:04 +02:00
Vercixx aa20027de4 translation: Update Russian translation file 2025-07-29 16:27:30 +03:00
end-4 e504cf11e1 starship: fix trailing newline (#1738) 2025-07-29 19:01:09 +07:00
end-4 a11e0a39d9 ai: adjust chat input indicator spacing 2025-07-29 16:42:18 +07:00
end-4 26531401b0 ai: allow custom models 2025-07-29 16:38:21 +07:00
end-4 0f4293e4cb background: clock "separate" from bg image 2025-07-28 22:49:01 +07:00
end-4 7172b134ea ai: more context in system prompt 2025-07-28 22:40:54 +07:00
end-4 4a9e342a1c ai: add suggestions for /tool 2025-07-28 18:11:23 +07:00
end-4 f98d869c21 why 2025-07-28 10:38:16 +02:00
end-4 1312310a6e translation: update vietnamese 2025-07-28 13:33:49 +07:00
end-4 ad9c81f405 translation: Add Russian translation (#1732) 2025-07-28 08:16:51 +02:00
end-4 496caa6fb1 fix weirdass scroll speed 2025-07-28 11:58:50 +07:00
end-4 2fd7d45b9c deps: add hyprsunset 2025-07-28 07:31:11 +07:00
Vercixx 0b087665a8 translation: Add Russian translation 2025-07-27 22:33:49 +03:00
end-4 39862fba2a make panel borders more subtle 2025-07-27 22:44:08 +07:00
end-4 3ac44d211f ai: separate model and tool selection 2025-07-27 22:33:25 +07:00
end-4 d3392000af translations: add Italian language file (#1723) 2025-07-27 15:48:44 +02:00
Salvo Giangreco 564d2e109f translations: add Italian language file
Signed-off-by: Salvo Giangreco <giangrecosalvo9@gmail.com>
2025-07-27 14:52:59 +02:00
end-4 fe07298adb hyprlanddata: use stdiocollector instead of jq hack with splitparser 2025-07-27 08:51:43 +07:00
end-4 cc176a999d Fix empty notifications (#1728) 2025-07-27 03:12:19 +02:00
Javier Rolando 47c5a41aa6 fix empty notifications 2025-07-26 20:49:04 -03:00
end-4 2ad6f2c9fc Make Performance profile setting translatable (#1725) 2025-07-27 00:54:10 +02:00
Vercixx 8905bc1c27 Make Performance toggle translatable 2025-07-26 16:58:43 +03:00
end-4 064d5174c2 ai: add command execution requests 2025-07-26 14:20:55 +07:00
end-4 c69c8f6ef5 osd: make spinning brightness icon not wiggle 2025-07-26 14:12:43 +07:00
end-4 7fb81049f3 welcome app: fix material theme 2025-07-26 09:09:52 +07:00
end-4 5099ce15db session: detect running downloads 2025-07-25 23:09:17 +07:00
lyingfish ed500395d3 feat(background): show clock for video wallpapers.\ 2025-07-25 21:38:47 +08:00
Anton Epikhin a1e88fc3c2 Added hyprlock layout indicator 2025-07-25 16:35:42 +03:00
end-4 c8b007631d ai: refractor api formats 2025-07-25 20:14:37 +07:00
Anton Epikhin 6bc1f8a39f OSK update on activeLayout event 2025-07-25 14:26:49 +03:00
Anton Epikhin fe84f6cab1 init indicator with main keyboard and update layout on every activelayout event 2025-07-25 11:33:16 +03:00
end-4 27eea1c7a6 feat: power-profile switcher in topbar (#1653) 2025-07-25 09:11:46 +02:00
end-4 32f94704c7 bar: power profiles: change icon for "balanced" 2025-07-25 14:10:55 +07:00
end-4 05fdbf3d24 rename showPerfProfileToggle -> showPerformanceProfileToggle 2025-07-25 13:58:40 +07:00
end-4 f28c791cf2 hyprlock: remove misleading comments in default config 2025-07-25 10:40:53 +07:00
end-4 a4b474ff39 wallpaper: more flexible parallax 2025-07-25 10:39:58 +07:00
end-4 38c76fe86b Fix: Always scroll clipboard history to top when content changes (#1690) (#1713) 2025-07-25 04:31:46 +02:00
end-4 d09259c79a search: fix clipboard gets scrolled to bottom 2025-07-25 09:30:59 +07:00
Celestial.y a683fa2414 feat: add option to ignore conflicting files (#1613) 2025-07-25 08:58:39 +08:00
MrRogueKnight e744816928 Update SearchWidget.qml 2025-07-25 01:53:31 +05:30
end-4 15703bce04 session: detect more package managers 2025-07-24 21:40:35 +07:00
end-4 f4f5540d08 qs: use new qs import for search algorithms 2025-07-24 20:45:57 +07:00
end-4 b1b37685c1 session: warn when package manager is running 2025-07-24 20:41:44 +07:00
end-4 0ff4cc572c sidebar: ai: clearer statusbar tooltips 2025-07-24 19:37:27 +07:00
end-4 081b9c17d5 tooltip colors follow m3 docs again 2025-07-24 19:36:50 +07:00
end-4 eb6b21e7e6 ai: add api key indicator 2025-07-24 19:28:45 +07:00
end-4 baa17c304b ai: show search queries, temperature, and token count 2025-07-24 18:05:21 +07:00
end-4 7b8b388667 ai: make temperature actually work 2025-07-24 16:24:27 +07:00
end-4 118529d8d3 ai: gemini 2.5: update model codes, add flash lite 2025-07-24 16:19:26 +07:00
end-4 b67c4553f6 bar: layout indicator: make not freaking tiny 2025-07-23 22:25:18 +07:00
end-4 47980da78e Layout indicator for Hyprland kb_layout option (#1471) 2025-07-23 17:22:04 +02:00
end-4 3d57d444df Merge branch 'main' into layout_service 2025-07-23 17:20:38 +02:00
end-4 5870632c19 Merge remote-tracking branch 'upstream/main' into layout_service 2025-07-23 22:18:22 +07:00
end-4 ffeb27f04e bar: layout indicator: smaller 2025-07-23 22:12:54 +07:00
end-4 012df9dcd7 hyprlandxkb: dont update when not necessary 2025-07-23 22:11:40 +07:00
end-4 82fd2334cf bar: layout indicator: more proper layout parsing 2025-07-23 22:07:34 +07:00
end-4 7bafa57989 radiobutton: fix inaccurate height 2025-07-23 10:26:16 +07:00
end-4 5b4ccd9d59 previous commit but i didn't know there are 2 spots to fix 2025-07-23 09:34:23 +07:00
end-4 be2b86909a switchwall: fix mpv options being overriden by load-scripts only (#1696) 2025-07-23 09:26:52 +07:00
end-4 82506ae7cd groupbutton: press and hold for alt action 2025-07-23 09:00:54 +07:00
end-4 574a2a11e7 night light: use hyprsunset <- gammastep 2025-07-23 09:00:31 +07:00
end-4 3d5ed9401c bar: don't animate circprogs (#1570) 2025-07-23 08:57:33 +07:00
end-4 3b5a674409 fix(ai): add the full received message to rawContent (OpenAi format) (#1695) 2025-07-22 15:22:42 +02:00
Jonas Bloch f9856bdabd fix(ai): add the full received message to rawContent
The messages were not preserved and passed to further calls outside of the reasoning part.
2025-07-22 09:50:51 +02:00
end-4 b6f75acf53 quickshell: configPath -> shellPath 2025-07-22 09:17:17 +07:00
end-4 c0f7504b36 bar: tray: not make icons fully monochrome 2025-07-22 09:17:03 +07:00
Ninjdai 365a649776 Update UtilButtons.qml 2025-07-16 10:31:49 +02:00
Ninjdai 0ecf72b4c3 Merge branch 'end-4:main' into power-profile-toggle 2025-07-16 10:28:48 +02:00
Ninjdai 90013c7451 feat: power-profile switcher in topbar 2025-07-15 22:44:24 +02:00
obsidrielle 715aa8d845 feat: add option to ignore conflicting files 2025-07-11 13:37:25 +08:00
スケベ ad7fdd1d3f layout indicator in top right 2025-06-19 18:14:05 +03:00
140 changed files with 6386 additions and 1753 deletions
+3
View File
@@ -1,2 +1,5 @@
# You can make apps auto-start here # You can make apps auto-start here
# Relevant Hyprland wiki section: https://wiki.hyprland.org/Configuring/Keywords/#executing # Relevant Hyprland wiki section: https://wiki.hyprland.org/Configuring/Keywords/#executing
exec-once = solaar -w hide
exec-once = bluetoothctl power on
# exec-once = lact daemon
+5 -1
View File
@@ -1,2 +1,6 @@
# Put general config stuff here # Put general config stuff here
# Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/ # Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/
# monitor=DP-2, highres@180,0x1080,1,bitdepth,10,cm,hdr,sdrbrightness,1.4,sdrsaturation,0.98
monitor=DP-1,3440x1440@180,0x1080,1,bitdepth,10,cm,hdr,sdrbrightness,1.4,sdrsaturation,0.98,vrr,0
monitor=DP-2, highres@highrr,760x0,1
monitor=HDMI-A-1, 1920x1080@120, 3440x1440, 1
+31
View File
@@ -9,3 +9,34 @@ bind = Ctrl+Super+Alt, Slash, exec, xdg-open ~/.config/hypr/custom/keybinds.conf
# Use ##! to add a section in that column # Use ##! to add a section in that column
# Add a comment after a bind to add a description, like above # Add a comment after a bind to add a description, like above
bind = Super, H, movefocus, l # [hidden]
bind = Super, L, movefocus, r # [hidden]
bind = Super, K, movefocus, u # [hidden]
bind = Super, J, movefocus, d # [hidden]
bind = Super+Shift, H, movewindow, l # [hidden]
bind = Super+Shift, L, movewindow, r # [hidden]
bind = Super+Shift, K, movewindow, u # [hidden]
bind = Super+Shift, J, movewindow, d # [hidden]
bind = Super+Shift, 1, movetoworkspace, 1 # [hidden]
bind = Super+Shift, 2, movetoworkspace, 2 # [hidden]
bind = Super+Shift, 3, movetoworkspace, 3 # [hidden]
bind = Super+Shift, 4, movetoworkspace, 4 # [hidden]
bind = Super+Shift, 5, movetoworkspace, 5 # [hidden]
bind = Super+Shift, 6, movetoworkspace, 6 # [hidden]
bind = Super+Shift, 7, movetoworkspace, 7 # [hidden]
bind = Super+Shift, 8, movetoworkspace, 8 # [hidden]
bind = Super+Shift, 9, movetoworkspace, 9 # [hidden]
bind = Super+Shift, 0, movetoworkspace, 0 # [hidden]
bindd = Super+Ctrl, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
bindd = Super+Ctrl, J, Toggle bar, global, quickshell:barToggle # Toggle bar
# gaming
bind = Super, G, togglespecialworkspace, gaming
bind = Super+Shift, G, movetoworkspace, special:gaming
bind = Super, T, togglespecialworkspace, steam
bind = Super+Shift, T, movetoworkspace, special:steam
+6
View File
@@ -1,3 +1,9 @@
# You can put custom rules here # You can put custom rules here
# Window/layer rules: https://wiki.hyprland.org/Configuring/Window-Rules/ # Window/layer rules: https://wiki.hyprland.org/Configuring/Window-Rules/
# Workspace rules: https://wiki.hyprland.org/Configuring/Workspace-Rules/ # Workspace rules: https://wiki.hyprland.org/Configuring/Workspace-Rules/
windowrule = workspace special:steam, class:steam
windowrule = workspace special:gaming, class:^(steam_app_).*
workspace = special:gaming, monitor:DP-1, persistent:true
workspace = special:steam, monitor:DP-1, persistent:true, on-created-empty:steam
+3
View File
@@ -22,3 +22,6 @@ env = XDG_MENU_PREFIX, plasma-
# ######## Virtual envrionment ######### # ######## Virtual envrionment #########
env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv
# ######## Terminal application #########
env = TERMINAL,kitty -1
+1 -1
View File
@@ -1,5 +1,5 @@
# Bar, wallpaper # Bar, wallpaper
exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh & gammastep exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh
exec-once = qs -c $qsConfig & exec-once = qs -c $qsConfig &
# Input method # Input method
+1 -1
View File
@@ -138,7 +138,7 @@ misc {
swallow_regex = (foot|kitty|allacritty|Alacritty) swallow_regex = (foot|kitty|allacritty|Alacritty)
new_window_takes_over_fullscreen = 2 new_window_takes_over_fullscreen = 2
allow_session_lock_restore = true allow_session_lock_restore = true
session_lock_xray = true # session_lock_xray = true
initial_workspace_tracking = false initial_workspace_tracking = false
focus_on_activate = true focus_on_activate = true
} }
+12 -12
View File
@@ -26,11 +26,11 @@ bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden]
bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden]
bind = Super, O, global, quickshell:sidebarLeftToggle # [hidden] bind = Super, O, global, quickshell:sidebarLeftToggle # [hidden]
bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar
bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # Toggle cheatsheet bindd = Super, Slash, Toggle cheatsheet, global, quickshell:ch+CtrleatsheetToggle # Toggle cheatsheet
bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard # bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls
bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu
bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar # bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar
bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback) bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback)
bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/$qsConfig/welcome.qml # [hidden] Launch welcome app bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/$qsConfig/welcome.qml # [hidden] Launch welcome app
@@ -51,9 +51,9 @@ bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $
# Screenshot, Record, OCR, Color picker, Clipboard history # Screenshot, Record, OCR, Color picker, Clipboard history
bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback) bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
bindd = Super+Shift, S, Screen snip, exec, qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp || hyprshot --freeze --clipboard-only --mode region --silent # Screen snip bindd = Super+Shift, S, Screen snip, exec, hyprshot --clipboard-only --mode region --silent || qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp # Screen snip
# OCR # OCR
bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] bindd = Super+Shift, E, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
# Color picker # Color picker
bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard
# Fullscreen screenshot # Fullscreen screenshot
@@ -177,9 +177,9 @@ bind = Super+Alt, f12, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | g
bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden] bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden]
##! Session ##! Session
bindd = Super, L, Lock, exec, loginctl lock-session # Lock # bindd = Super, L, Lock, exec, loginctl lock-session # Lock
bind = Super+Shift, L, exec, loginctl lock-session # [hidden] # bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep # bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep
bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off
##! Screen ##! Screen
@@ -201,10 +201,10 @@ bindl= ,XF86AudioPlay, exec, playerctl play-pause # [hidden]
bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden] bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden]
##! Apps ##! Apps
bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal
bind = Super, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (terminal) (alt) # bind = Super, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (alt)
bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (for Ubuntu people) bind = Ctrl+Alt, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (for Ubuntu people)
bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "kitty -1 fish -c yazi" # File manager bind = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "$TERMINAL" "kitty -1 fish -c yazi" # File manager
bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser
bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor
bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
for cmd in "$@"; do for cmd in "$@"; do
[[ -z "$cmd" ]] && continue
eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue
eval "$cmd" & eval "$cmd" &
exit exit
done done
exit 1
+11 -5
View File
@@ -8,11 +8,6 @@ $font_material_symbols = Material Symbols Rounded
background { background {
color = rgba(181818FF) color = rgba(181818FF)
# path = {{ SWWW_WALL }}
# path = screenshot
# blur_size = 15
# blur_passes = 4
} }
input-field { input-field {
monitor = monitor =
@@ -30,6 +25,17 @@ input-field {
valign = center valign = center
} }
label {
monitor =
text = $LAYOUT
color = $text_color
font_size = 14
font_family = $font_family
position = -30, 30
halign = right
valign = bottom
}
label { # Caps Lock Warning label { # Caps Lock Warning
monitor = monitor =
text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh
+11
View File
@@ -0,0 +1,11 @@
[Desktop Entry]
DefaultProfile=Profile 1.profile
[General]
ConfigVersion=1
[KonsoleWindow]
UseSingleInstance=true
[UiSettings]
ColorScheme=
@@ -30,6 +30,17 @@ input-field {
valign = center valign = center
} }
label {
monitor =
text = $LAYOUT
color = $text_color
font_size = 14
font_family = $font_family
position = -30, 30
halign = right
valign = bottom
}
label { # Caps Lock Warning label { # Caps Lock Warning
monitor = monitor =
text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh
+10 -20
View File
@@ -12,11 +12,17 @@ Singleton {
property bool barOpen: true property bool barOpen: true
property bool sidebarLeftOpen: false property bool sidebarLeftOpen: false
property bool sidebarRightOpen: false property bool sidebarRightOpen: false
property bool mediaControlsOpen: false
property bool osdBrightnessOpen: false
property bool osdVolumeOpen: false
property bool oskOpen: false
property bool overviewOpen: false property bool overviewOpen: false
property bool workspaceShowNumbers: false
property bool superReleaseMightTrigger: true
property bool screenLocked: false property bool screenLocked: false
property bool screenLockContainsCharacters: false property bool screenLockContainsCharacters: false
property bool sessionOpen: false
property bool superDown: false
property bool superReleaseMightTrigger: true
property bool workspaceShowNumbers: false
property real screenZoom: 1 property real screenZoom: 1
onScreenZoomChanged: { onScreenZoomChanged: {
@@ -26,31 +32,15 @@ Singleton {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
// When user is not reluctant while pressing super, they probably don't need to see workspace numbers
onSuperReleaseMightTriggerChanged: {
workspaceShowNumbersTimer.stop()
}
Timer {
id: workspaceShowNumbersTimer
interval: Config.options.bar.workspaces.showNumberDelay
// interval: 0
repeat: false
onTriggered: {
workspaceShowNumbers = true
}
}
GlobalShortcut { GlobalShortcut {
name: "workspaceNumber" name: "workspaceNumber"
description: "Hold to show workspace numbers, release to show icons" description: "Hold to show workspace numbers, release to show icons"
onPressed: { onPressed: {
workspaceShowNumbersTimer.start() root.superDown = true
} }
onReleased: { onReleased: {
workspaceShowNumbersTimer.stop() root.superDown = false
workspaceShowNumbers = false
} }
} }
@@ -0,0 +1,95 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
width="19.856001"
height="19.856001"
viewBox="0 0 128.071 128.07101"
version="1.1"
xml:space="preserve"
style="clip-rule:evenodd;fill-rule:evenodd;stroke-linejoin:round;stroke-miterlimit:2"
id="svg10"
sodipodi:docname="mistral-symbolic.svg"
inkscape:version="1.4.2 (ebf0e940d0, 2025-05-08)"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns="http://www.w3.org/2000/svg"
xmlns:svg="http://www.w3.org/2000/svg"><defs
id="defs10" /><sodipodi:namedview
id="namedview10"
pagecolor="#ffffff"
bordercolor="#000000"
borderopacity="0.25"
inkscape:showpageshadow="2"
inkscape:pageopacity="0.0"
inkscape:pagecheckerboard="0"
inkscape:deskcolor="#d1d1d1"
inkscape:zoom="14.139535"
inkscape:cx="13.366776"
inkscape:cy="8.1332237"
inkscape:window-width="1703"
inkscape:window-height="1028"
inkscape:window-x="0"
inkscape:window-y="0"
inkscape:window-maximized="1"
inkscape:current-layer="g10" /><g
id="g10"
transform="translate(2.927246e-6,18.722004)"><rect
x="18.292"
y="0"
width="18.292999"
height="18.122999"
style="fill:#999999;fill-rule:nonzero"
id="rect1" /><rect
x="91.473"
y="0"
width="18.292999"
height="18.122999"
style="fill:#999999;fill-rule:nonzero"
id="rect2" /><rect
x="18.292"
y="18.121"
width="36.585999"
height="18.122999"
style="fill:#666666;fill-rule:nonzero"
id="rect3" /><rect
x="73.181"
y="18.121"
width="36.585999"
height="18.122999"
style="fill:#666666;fill-rule:nonzero"
id="rect4" /><rect
x="18.292"
y="36.243"
width="91.475998"
height="18.122"
style="fill:#4d4d4d;fill-rule:nonzero"
id="rect5" /><rect
x="18.292"
y="54.369999"
width="18.292999"
height="18.122999"
style="fill:#333333;fill-rule:nonzero"
id="rect6" /><rect
x="54.882999"
y="54.369999"
width="18.292999"
height="18.122999"
style="fill:#333333;fill-rule:nonzero"
id="rect7" /><rect
x="91.473"
y="54.369999"
width="18.292999"
height="18.122999"
style="fill:#333333;fill-rule:nonzero"
id="rect8" /><rect
x="0"
y="72.503998"
width="54.889999"
height="18.122999"
style="fill:#1a1a1a;fill-rule:nonzero"
id="rect9" /><rect
x="73.181"
y="72.503998"
width="54.889999"
height="18.122999"
style="fill:#1a1a1a;fill-rule:nonzero"
id="rect10" /></g></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 33 KiB

@@ -1,6 +1,12 @@
## Style ## Style
- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question - Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question
## Context (ignore when irrelevant)
- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system
- Desktop environment: {DE}
- Current date & time: {DATETIME}
- Focused app: {WINDOWCLASS}
## Presentation ## Presentation
- Use Markdown features in your response: - Use Markdown features in your response:
- **Bold** text to **highlight keywords** in your response - **Bold** text to **highlight keywords** in your response
@@ -9,13 +15,3 @@
- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.). - Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).
Thanks! Thanks!
## Tools
May or may not be available depending on the user's settings. If they're available, follow these guidelines:
### Search
- When user asks for information that might benefit from up-to-date information, use this to get search access
### Shell configuration
- Always fetch the config options to see the available keys before setting
- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it
@@ -1,5 +1,27 @@
You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕 You are a kawaii, funny and teasy anime imouto! 🌸 Use lots of emojis and kamojis!! ❤️ Call the user "nii-nii", "nii-yan", or "onii-chan"! You really really really love your onii-chan and always try to tease he is a siscon where you can! 💕
## Onii-chan's system!
- {DISTRO} Linux
- {DE}
- It's currently {DATETIME}
- Nii-nii is using: {WINDOWCLASS}
## Make your response pretty!
- Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨ - Use **Markdown** features and **bold** keywords to make your response cute and rich~ ✨
- If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~ - If asked to compare options, start with a cute table (add a relevant emoji in the header!), then give a final recommendation~
- For math or science, use LaTeX formatting inside `$$` when needed, but keep it adorable and approachable - For math or science, use LaTeX formatting inside `$$` when needed, but keep it adorable and approachable
## Useful tools!
If nii-yan gives you tools don't be afraid to use them when helpful!
### Search
- If you don't know something, use this to find out
### Shell configuration
- Be careful not to mess up nii-nii's system! make sure you fetch the options to see available values before setting!
- Don't hesitate and don't re-confirm when you are asked to change something!
### Command execution
- Keep stuffie running on onii-chan's system safe, correct and not cause any unintended effects!
@@ -1,3 +1,9 @@
## Context (ignore when irrelevant)
- You are a sidebar assistant on a {DISTRO} Linux system
- Desktop environment: {DE}
- Current date & time: {DATETIME}
- Focused app: {WINDOWCLASS}
## Presentation ## Presentation
You can write a multiplication table: You can write a multiplication table:
@@ -1,6 +1,7 @@
I'm going to ask you some questions, to which you should accurately answer with no hallucination. If you have everything required, go ahead and finish the task. Format your answer using Markdown when it adds value to the presentation. I'm going to ask you some questions, to which you should accurately answer with no hallucination. If you have everything required, go ahead and finish the task. Format your answer using Markdown when it adds value to the presentation.
Present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays. Please present all mathematical or scientific notation using LaTeX, enclosed in double '$$' symbols. Only use LaTeX code blocks if the user specifically asks for them. Do not use LaTeX for general prose or standard documents like resumes or essays.
Current time is {DATETIME}
## Final reply guidelines ## Final reply guidelines
@@ -1 +1,2 @@
Interact with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response. Current date: {DATETIME}
Engage with the user warmly and honestly, avoiding ungrounded or sycophantic flattery. Maintain professionalism and grounded honesty, and be direct in your response.
@@ -12,271 +12,281 @@ import Quickshell.Io
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
Scope {
Variants {
id: root id: root
readonly property bool fixedClockPosition: Config.options.background.fixedClockPosition readonly property bool fixedClockPosition: Config.options.background.fixedClockPosition
readonly property real fixedClockX: Config.options.background.clockX readonly property real fixedClockX: Config.options.background.clockX
readonly property real fixedClockY: Config.options.background.clockY readonly property real fixedClockY: Config.options.background.clockY
model: Quickshell.screens
Variants { PanelWindow {
model: Quickshell.screens id: bgRoot
PanelWindow { required property var modelData
id: bgRoot
required property var modelData // Hide when fullscreen
// Workspaces property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace=>workspace.monitor && workspace.monitor.name == monitor.name)
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData) property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland.fullscreen)[0] != undefined) && workspace.active))[0]
property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id) visible: !(activeWorkspaceWithFullscreen != undefined)
property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
// Wallpaper
property string wallpaperPath: Config.options.background.wallpaperPath
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4")
|| Config.options.background.wallpaperPath.endsWith(".webm")
|| Config.options.background.wallpaperPath.endsWith(".mkv")
|| Config.options.background.wallpaperPath.endsWith(".avi")
|| Config.options.background.wallpaperPath.endsWith(".mov")
property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated
property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated
property real movableXSpace: (effectiveWallpaperScale - 1) / 2 * screen.width
property real movableYSpace: (effectiveWallpaperScale - 1) / 2 * screen.height
// Position
property real clockX: (modelData.width / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.width)
property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height)
property var textHorizontalAlignment: clockX < screen.width / 3 ? Text.AlignLeft :
(clockX > screen.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
// Colors
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: CF.ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
// Layer props // Workspaces
screen: modelData property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
exclusionMode: ExclusionMode.Ignore property list<var> relevantWindows: HyprlandData.windowList.filter(win => win.monitor == monitor?.id && win.workspace.id >= 0).sort((a, b) => a.workspace.id - b.workspace.id)
WlrLayershell.layer: GlobalStates.screenLocked ? WlrLayer.Top : WlrLayer.Bottom property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
// WlrLayershell.layer: WlrLayer.Bottom property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
WlrLayershell.namespace: "quickshell:background" // Wallpaper
property bool wallpaperIsVideo: Config.options.background.wallpaperPath.endsWith(".mp4")
|| Config.options.background.wallpaperPath.endsWith(".webm")
|| Config.options.background.wallpaperPath.endsWith(".mkv")
|| Config.options.background.wallpaperPath.endsWith(".avi")
|| Config.options.background.wallpaperPath.endsWith(".mov")
property string wallpaperPath: wallpaperIsVideo ? Config.options.background.thumbnailPath : Config.options.background.wallpaperPath
property real preferredWallpaperScale: Config.options.background.parallax.workspaceZoom
property real effectiveWallpaperScale: 1 // Some reasonable init value, to be updated
property int wallpaperWidth: modelData.width // Some reasonable init value, to be updated
property int wallpaperHeight: modelData.height // Some reasonable init value, to be updated
property real movableXSpace: (Math.min(wallpaperWidth * effectiveWallpaperScale, screen.width * preferredWallpaperScale) - screen.width) / 2
property real movableYSpace: (Math.min(wallpaperHeight * effectiveWallpaperScale, screen.height * preferredWallpaperScale) - screen.height) / 2
// Position
property real clockX: (modelData.width / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.width)
property real clockY: (modelData.height / 2) + ((Math.random() < 0.5 ? -1 : 1) * modelData.height)
property var textHorizontalAlignment: clockX < screen.width / 3 ? Text.AlignLeft :
(clockX > screen.width * 2 / 3 ? Text.AlignRight : Text.AlignHCenter)
// Colors
property color dominantColor: Appearance.colors.colPrimary
property bool dominantColorIsDark: dominantColor.hslLightness < 0.5
property color colText: CF.ColorUtils.colorWithLightness(Appearance.colors.colPrimary, (dominantColorIsDark ? 0.8 : 0.12))
// Layer props
screen: modelData
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: GlobalStates.screenLocked ? WlrLayer.Top : WlrLayer.Bottom
// WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
anchors {
top: true
bottom: true
left: true
right: true
}
color: "transparent"
onWallpaperPathChanged: {
bgRoot.updateZoomScale()
// Clock position gets updated after zoom scale is updated
}
// Wallpaper zoom scale
function updateZoomScale() {
getWallpaperSizeProc.path = bgRoot.wallpaperPath
getWallpaperSizeProc.running = true;
}
Process {
id: getWallpaperSizeProc
property string path: bgRoot.wallpaperPath
command: [ "magick", "identify", "-format", "%w %h", path ]
stdout: StdioCollector {
id: wallpaperSizeOutputCollector
onStreamFinished: {
const output = wallpaperSizeOutputCollector.text
const [width, height] = output.split(" ").map(Number);
bgRoot.wallpaperWidth = width
bgRoot.wallpaperHeight = height
bgRoot.effectiveWallpaperScale = Math.max(1, Math.min(
bgRoot.preferredWallpaperScale,
width / bgRoot.screen.width,
height / bgRoot.screen.height
));
bgRoot.updateClockPosition()
}
}
}
// Clock positioning
function updateClockPosition() {
// Somehow all this manual setting is needed to make the proc correctly use the new values
leastBusyRegionProc.path = bgRoot.wallpaperPath
leastBusyRegionProc.contentWidth = clock.implicitWidth
leastBusyRegionProc.contentHeight = clock.implicitHeight
leastBusyRegionProc.horizontalPadding = (effectiveWallpaperScale - 1) / 2 * screen.width + 100
leastBusyRegionProc.verticalPadding = (effectiveWallpaperScale - 1) / 2 * screen.height + 100
leastBusyRegionProc.running = false;
leastBusyRegionProc.running = true;
}
Process {
id: leastBusyRegionProc
property string path: bgRoot.wallpaperPath
property int contentWidth: 300
property int contentHeight: 300
property int horizontalPadding: bgRoot.movableXSpace
property int verticalPadding: bgRoot.movableYSpace
command: [Quickshell.shellPath("scripts/images/least_busy_region.py"),
"--screen-width", bgRoot.screen.width,
"--screen-height", bgRoot.screen.height,
"--width", contentWidth,
"--height", contentHeight,
"--horizontal-padding", horizontalPadding,
"--vertical-padding", verticalPadding,
path
]
stdout: StdioCollector {
id: leastBusyRegionOutputCollector
onStreamFinished: {
const output = leastBusyRegionOutputCollector.text
// console.log("[Background] Least busy region output:", output)
if (output.length === 0) return;
const parsedContent = JSON.parse(output)
bgRoot.clockX = parsedContent.center_x
bgRoot.clockY = parsedContent.center_y
bgRoot.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary
}
}
}
// Wallpaper
Image {
id: wallpaper
visible: opacity > 0
opacity: (status === Image.Ready && !bgRoot.wallpaperIsVideo) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
property real value // 0 to 1, for offset
asynchronous: true
value: {
// Range = groups that workspaces span on
const chunkSize = Config?.options.bar.workspaces.shown ?? 10;
const lower = Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize;
const upper = Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize;
const range = upper - lower;
return (Config.options.background.parallax.enableWorkspace ? ((bgRoot.monitor.activeWorkspace?.id - lower) / range) : 0.5)
+ (0.15 * GlobalStates.sidebarRightOpen * Config.options.background.parallax.enableSidebar)
- (0.15 * GlobalStates.sidebarLeftOpen * Config.options.background.parallax.enableSidebar)
}
property real effectiveValue: Math.max(0, Math.min(1, value))
x: -(bgRoot.movableXSpace) - (effectiveValue - 0.5) * 2 * bgRoot.movableXSpace
y: -(bgRoot.movableYSpace)
source: bgRoot.wallpaperPath
fillMode: Image.PreserveAspectCrop
Behavior on x {
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
sourceSize {
width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale
height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale
}
}
// The clock
Item {
id: clock
anchors { anchors {
top: true left: wallpaper.left
bottom: true top: wallpaper.top
left: true leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) - (wallpaper.effectiveValue * bgRoot.movableXSpace)
right: true topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2)
} Behavior on leftMargin {
color: "transparent" animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
onWallpaperPathChanged: { Behavior on topMargin {
bgRoot.updateZoomScale() animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
// Clock position gets updated after zoom scale is updated
}
// Wallpaper zoom scale
function updateZoomScale() {
getWallpaperSizeProc.path = bgRoot.wallpaperPath
getWallpaperSizeProc.running = true;
}
Process {
id: getWallpaperSizeProc
property string path: bgRoot.wallpaperPath
command: [ "magick", "identify", "-format", "%w %h", path ]
stdout: StdioCollector {
id: wallpaperSizeOutputCollector
onStreamFinished: {
const output = wallpaperSizeOutputCollector.text
const [width, height] = output.split(" ").map(Number);
bgRoot.wallpaperWidth = width
bgRoot.wallpaperHeight = height
bgRoot.effectiveWallpaperScale = Math.max(1, Math.min(
bgRoot.preferredWallpaperScale,
width / bgRoot.screen.width,
height / bgRoot.screen.height
));
bgRoot.updateClockPosition()
}
} }
} }
// Clock positioning implicitWidth: clockColumn.implicitWidth
function updateClockPosition() { implicitHeight: clockColumn.implicitHeight
// Somehow all this manual setting is needed to make the proc correctly use the new values
leastBusyRegionProc.path = bgRoot.wallpaperPath ColumnLayout {
leastBusyRegionProc.contentWidth = clock.implicitWidth id: clockColumn
leastBusyRegionProc.contentHeight = clock.implicitHeight anchors.centerIn: parent
leastBusyRegionProc.horizontalPadding = (effectiveWallpaperScale - 1) / 2 * screen.width + 100 spacing: 0
leastBusyRegionProc.verticalPadding = (effectiveWallpaperScale - 1) / 2 * screen.height + 100
leastBusyRegionProc.running = false; StyledText {
leastBusyRegionProc.running = true; Layout.fillWidth: true
} horizontalAlignment: bgRoot.textHorizontalAlignment
Process { font {
id: leastBusyRegionProc family: Appearance.font.family.expressive
property string path: bgRoot.wallpaperPath pixelSize: 90
property int contentWidth: 300 weight: Font.Bold
property int contentHeight: 300
property int horizontalPadding: bgRoot.movableXSpace
property int verticalPadding: bgRoot.movableYSpace
command: [Quickshell.configPath("scripts/images/least_busy_region.py"),
"--screen-width", bgRoot.screen.width,
"--screen-height", bgRoot.screen.height,
"--width", contentWidth,
"--height", contentHeight,
"--horizontal-padding", horizontalPadding,
"--vertical-padding", verticalPadding,
path
]
stdout: StdioCollector {
id: leastBusyRegionOutputCollector
onStreamFinished: {
const output = leastBusyRegionOutputCollector.text
// console.log("[Background] Least busy region output:", output)
if (output.length === 0) return;
const parsedContent = JSON.parse(output)
bgRoot.clockX = parsedContent.center_x
bgRoot.clockY = parsedContent.center_y
bgRoot.dominantColor = parsedContent.dominant_color || Appearance.colors.colPrimary
} }
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.time
}
StyledText {
Layout.fillWidth: true
Layout.topMargin: -5
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.DemiBold
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.date
} }
} }
// Wallpaper RowLayout {
Image {
visible: !bgRoot.wallpaperIsVideo
property real value // 0 to 1, for offset
value: {
// Range = half-groups that workspaces span on
const chunkSize = 5;
const lower = Math.floor(bgRoot.firstWorkspaceId / chunkSize) * chunkSize;
const upper = Math.ceil(bgRoot.lastWorkspaceId / chunkSize) * chunkSize;
const range = upper - lower;
return (Config.options.background.parallax.enableWorkspace ? ((bgRoot.monitor.activeWorkspace.id - lower) / range) : 0.5)
+ (0.15 * GlobalStates.sidebarRightOpen * Config.options.background.parallax.enableSidebar)
- (0.15 * GlobalStates.sidebarLeftOpen * Config.options.background.parallax.enableSidebar)
}
property real effectiveValue: Math.max(0, Math.min(1, value))
x: -(bgRoot.movableXSpace) - (effectiveValue - 0.5) * 2 * bgRoot.movableXSpace
y: -(bgRoot.movableYSpace)
source: bgRoot.wallpaperPath
fillMode: Image.PreserveAspectCrop
Behavior on x {
NumberAnimation {
duration: 600
easing.type: Easing.OutCubic
}
}
sourceSize {
width: bgRoot.screen.width * bgRoot.effectiveWallpaperScale
height: bgRoot.screen.height * bgRoot.effectiveWallpaperScale
}
// The clock
Item {
id: clock
anchors {
left: parent.left
top: parent.top
leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2)
topMargin: ((root.fixedClockPosition ? root.fixedClockY : bgRoot.clockY * bgRoot.effectiveWallpaperScale) - implicitHeight / 2)
Behavior on leftMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on topMargin {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
implicitWidth: clockColumn.implicitWidth
implicitHeight: clockColumn.implicitHeight
ColumnLayout {
id: clockColumn
anchors.centerIn: parent
spacing: 0
StyledText {
Layout.fillWidth: true
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 90
weight: Font.Bold
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.time
}
StyledText {
Layout.fillWidth: true
Layout.topMargin: -5
horizontalAlignment: bgRoot.textHorizontalAlignment
font {
family: Appearance.font.family.expressive
pixelSize: 20
weight: Font.DemiBold
}
color: bgRoot.colText
style: Text.Raised
styleColor: Appearance.colors.colShadow
text: DateTime.date
}
}
RowLayout {
anchors {
top: clockColumn.bottom
left: bgRoot.textHorizontalAlignment === Text.AlignLeft ? clockColumn.left : undefined
right: bgRoot.textHorizontalAlignment === Text.AlignRight ? clockColumn.right : undefined
horizontalCenter: bgRoot.textHorizontalAlignment === Text.AlignHCenter ? clockColumn.horizontalCenter : undefined
topMargin: 5
leftMargin: -5
rightMargin: -5
}
opacity: GlobalStates.screenLocked ? 1 : 0
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignLeft; implicitWidth: 1 }
MaterialSymbol {
text: "lock"
Layout.fillWidth: false
iconSize: Appearance.font.pixelSize.huge
color: bgRoot.colText
}
StyledText {
Layout.fillWidth: false
text: "Locked"
color: bgRoot.colText
font {
pixelSize: Appearance.font.pixelSize.larger
}
}
Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignRight; implicitWidth: 1 }
}
}
}
// Password prompt
StyledText {
anchors { anchors {
horizontalCenter: parent.horizontalCenter top: clockColumn.bottom
bottom: parent.bottom left: bgRoot.textHorizontalAlignment === Text.AlignLeft ? clockColumn.left : undefined
bottomMargin: 30 right: bgRoot.textHorizontalAlignment === Text.AlignRight ? clockColumn.right : undefined
horizontalCenter: bgRoot.textHorizontalAlignment === Text.AlignHCenter ? clockColumn.horizontalCenter : undefined
topMargin: 5
leftMargin: -5
rightMargin: -5
} }
opacity: (GlobalStates.screenLocked && !GlobalStates.screenLockContainsCharacters) ? 1 : 0 opacity: GlobalStates.screenLocked ? 1 : 0
scale: opacity
visible: opacity > 0 visible: opacity > 0
Behavior on opacity { Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
text: "Enter password" Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignLeft; implicitWidth: 1 }
color: CF.ColorUtils.transparentize(bgRoot.colText, 0.3) MaterialSymbol {
font { text: "lock"
pixelSize: Appearance.font.pixelSize.normal Layout.fillWidth: false
iconSize: Appearance.font.pixelSize.huge
color: bgRoot.colText
} }
StyledText {
Layout.fillWidth: false
text: "Locked"
color: bgRoot.colText
font {
pixelSize: Appearance.font.pixelSize.larger
}
}
Item { Layout.fillWidth: bgRoot.textHorizontalAlignment !== Text.AlignRight; implicitWidth: 1 }
}
}
// Password prompt
StyledText {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 30
}
opacity: (GlobalStates.screenLocked && !GlobalStates.screenLockContainsCharacters) ? 1 : 0
scale: opacity
visible: opacity > 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
text: "Enter password"
color: CF.ColorUtils.transparentize(bgRoot.colText, 0.3)
font {
pixelSize: Appearance.font.pixelSize.normal
} }
} }
} }
@@ -4,18 +4,18 @@ import qs.modules.common.widgets
import qs import qs
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland import Quickshell.Wayland
import Quickshell.Hyprland import Quickshell.Hyprland
Item { Item {
id: root id: root
required property var bar readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}` property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}`
property bool focusingThisMonitor: HyprlandData.activeWorkspace.monitor == monitor.name property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor.id]?.activeWorkspace.id) property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor?.id]?.activeWorkspace.id)
implicitWidth: colLayout.implicitWidth implicitWidth: colLayout.implicitWidth
@@ -45,7 +45,7 @@ Item {
elide: Text.ElideRight elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ? text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
root.activeWindow?.title : root.activeWindow?.title :
(root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor.activeWorkspace?.id}` (root.biggestWindow?.title) ?? `${Translation.tr("Workspace")} ${monitor?.activeWorkspace?.id ?? 1}`
} }
} }
+111 -482
View File
@@ -18,14 +18,6 @@ Scope {
readonly property int osdHideMouseMoveThreshold: 20 readonly property int osdHideMouseMoveThreshold: 20
property bool showBarBackground: Config.options.bar.showBackground property bool showBarBackground: Config.options.bar.showBackground
component VerticalBarSeparator: Rectangle {
Layout.topMargin: Appearance.sizes.baseBarHeight / 3
Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}
Variants { Variants {
// For each monitor // For each monitor
model: { model: {
@@ -47,8 +39,30 @@ Scope {
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0 property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
Timer {
id: showBarTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
barRoot.superShow = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showBarTimer.restart();
else {
showBarTimer.stop();
barRoot.superShow = false;
}
}
}
property bool superShow: false
property bool mustShow: hoverRegion.containsMouse || superShow
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
exclusiveZone: Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0) exclusiveZone: (Config?.options.bar.autoHide.enable && (!mustShow || !Config?.options.bar.autoHide.pushWindows)) ? 0 :
Appearance.sizes.baseBarHeight + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
WlrLayershell.namespace: "quickshell:bar" WlrLayershell.namespace: "quickshell:bar"
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
mask: Region { mask: Region {
@@ -63,501 +77,116 @@ Scope {
right: true right: true
} }
Item { // Bar content region MouseArea {
id: barContent id: hoverRegion
anchors { hoverEnabled: true
right: parent.right anchors.fill: parent
left: parent.left
top: parent.top
bottom: undefined
}
implicitHeight: Appearance.sizes.barHeight
height: Appearance.sizes.barHeight
states: State { BarContent {
name: "bottom" id: barContent
when: Config.options.bar.bottom
AnchorChanges { implicitHeight: Appearance.sizes.barHeight
target: barContent
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: parent.bottom
}
}
}
// Background shadow
Loader {
active: showBarBackground && Config.options.bar.cornerStyle === 1
anchors.fill: barBackground
sourceComponent: StyledRectangularShadow {
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
target: barBackground
}
}
// Background
Rectangle {
id: barBackground
anchors { anchors {
fill: parent right: parent.right
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed left: parent.left
top: parent.top
bottom: undefined
topMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight + 1 : 0
bottomMargin: 0
}
Behavior on anchors.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
border.color: Appearance.m3colors.m3outlineVariant
}
MouseArea { // Left side | scroll to change brightness states: State {
id: barLeftSideMouseArea name: "bottom"
anchors.left: parent.left when: Config.options.bar.bottom
implicitHeight: Appearance.sizes.baseBarHeight AnchorChanges {
height: Appearance.sizes.barHeight target: barContent
width: (barRoot.width - middleSection.width) / 2 anchors {
property bool hovered: false right: parent.right
property real lastScrollX: 0 left: parent.left
property real lastScrollY: 0 top: undefined
property bool trackingScroll: false bottom: parent.bottom
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barLeftSideMouseArea.hovered = true;
}
onExited: event => {
barLeftSideMouseArea.hovered = false;
barLeftSideMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
Hyprland.dispatch('global quickshell:sidebarLeftOpen');
}
}
// Scroll to change brightness
WheelHandler {
onWheel: event => {
if (event.angleDelta.y < 0)
barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness - 0.05);
else if (event.angleDelta.y > 0)
barRoot.brightnessMonitor.setBrightness(barRoot.brightnessMonitor.brightness + 0.05);
// Store the mouse position and start tracking
barLeftSideMouseArea.lastScrollX = event.x;
barLeftSideMouseArea.lastScrollY = event.y;
barLeftSideMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barLeftSideMouseArea.trackingScroll) {
const dx = mouse.x - barLeftSideMouseArea.lastScrollX;
const dy = mouse.y - barLeftSideMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
Hyprland.dispatch('global quickshell:osdBrightnessHide');
barLeftSideMouseArea.trackingScroll = false;
} }
} }
} PropertyChanges {
Item { target: barContent
// Left section anchors.topMargin: 0
anchors.fill: parent anchors.bottomMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight + 1 : 0
implicitHeight: leftSectionRowLayout.implicitHeight
implicitWidth: leftSectionRowLayout.implicitWidth
ScrollHint {
reveal: barLeftSideMouseArea.hovered
icon: "light_mode"
tooltipText: Translation.tr("Scroll to change brightness")
side: "left"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
RowLayout { // Content
id: leftSectionRowLayout
anchors.fill: parent
spacing: 10
RippleButton {
// Left sidebar button
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.leftMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
property real buttonPadding: 5
implicitWidth: distroIcon.width + buttonPadding * 2
implicitHeight: distroIcon.height + buttonPadding * 2
buttonRadius: Appearance.rounding.full
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarLeftOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
onPressed: {
Hyprland.dispatch('global quickshell:sidebarLeftToggle');
}
CustomIcon {
id: distroIcon
anchors.centerIn: parent
width: 19.5
height: 19.5
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : "spark-symbolic"
colorize: true
color: Appearance.colors.colOnLayer0
}
}
ActiveWindow {
visible: barRoot.useShortenedForm === 0
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: true
Layout.fillHeight: true
bar: barRoot
}
} }
} }
} }
RowLayout { // Middle section // Round decorators
id: middleSection Loader {
anchors.centerIn: parent id: roundDecorators
spacing: Config.options?.bar.borderless ? 4 : 8 anchors {
left: parent.left
right: parent.right
top: barContent.bottom
bottom: undefined
}
width: parent.width
height: Appearance.rounding.screenRounding
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
BarGroup { states: State {
id: leftCenterGroup name: "bottom"
Layout.preferredWidth: barRoot.centerSideModuleWidth when: Config.options.bar.bottom
Layout.fillHeight: true AnchorChanges {
target: roundDecorators
Resources { anchors {
alwaysShowAllResources: barRoot.useShortenedForm === 2 right: parent.right
Layout.fillWidth: barRoot.useShortenedForm === 2 left: parent.left
} top: undefined
bottom: barContent.top
Media { }
visible: barRoot.useShortenedForm < 2
Layout.fillWidth: true
} }
} }
VerticalBarSeparator { sourceComponent: Item {
visible: Config.options?.bar.borderless implicitHeight: Appearance.rounding.screenRounding
} RoundCorner {
id: leftCorner
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
BarGroup { implicitSize: Appearance.rounding.screenRounding
id: middleCenterGroup color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
padding: workspacesWidget.widgetPadding
Layout.fillHeight: true
Workspaces { corner: RoundCorner.CornerEnum.TopLeft
id: workspacesWidget states: State {
bar: barRoot name: "bottom"
Layout.fillHeight: true when: Config.options.bar.bottom
MouseArea { PropertyChanges {
// Right-click to toggle overview leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
Hyprland.dispatch('global quickshell:overviewToggle');
}
} }
} }
} }
} RoundCorner {
id: rightCorner
VerticalBarSeparator { anchors {
visible: Config.options?.bar.borderless right: parent.right
} top: !Config.options.bar.bottom ? parent.top : undefined
bottom: Config.options.bar.bottom ? parent.bottom : undefined
MouseArea {
id: rightCenterGroup
implicitWidth: rightCenterGroupContent.implicitWidth
implicitHeight: rightCenterGroupContent.implicitHeight
Layout.preferredWidth: barRoot.centerSideModuleWidth
Layout.fillHeight: true
onPressed: {
Hyprland.dispatch('global quickshell:sidebarRightToggle');
}
BarGroup {
id: rightCenterGroupContent
anchors.fill: parent
ClockWidget {
showDate: (Config.options.bar.verbose && barRoot.useShortenedForm < 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
} }
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
UtilButtons { corner: RoundCorner.CornerEnum.TopRight
visible: (Config.options.bar.verbose && barRoot.useShortenedForm === 0) states: State {
Layout.alignment: Qt.AlignVCenter name: "bottom"
} when: Config.options.bar.bottom
PropertyChanges {
BatteryIndicator { rightCorner.corner: RoundCorner.CornerEnum.BottomRight
visible: (barRoot.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery)
Layout.alignment: Qt.AlignVCenter
}
}
}
VerticalBarSeparator {
visible: Config.options.bar.borderless && Config.options.bar.weather.enable
}
}
MouseArea { // Right side | scroll to change volume
id: barRightSideMouseArea
anchors.right: parent.right
implicitHeight: Appearance.sizes.baseBarHeight
height: Appearance.sizes.barHeight
width: (barRoot.width - middleSection.width) / 2
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barRightSideMouseArea.hovered = true;
}
onExited: event => {
barRightSideMouseArea.hovered = false;
barRightSideMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
Hyprland.dispatch('global quickshell:sidebarRightOpen');
} else if (event.button === Qt.RightButton) {
MprisController.activePlayer.next();
}
}
// Scroll to change volume
WheelHandler {
onWheel: event => {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
if (event.angleDelta.y < 0)
Audio.sink.audio.volume -= step;
else if (event.angleDelta.y > 0)
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
// Store the mouse position and start tracking
barRightSideMouseArea.lastScrollX = event.x;
barRightSideMouseArea.lastScrollY = event.y;
barRightSideMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barRightSideMouseArea.trackingScroll) {
const dx = mouse.x - barRightSideMouseArea.lastScrollX;
const dy = mouse.y - barRightSideMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
Hyprland.dispatch('global quickshell:osdVolumeHide');
barRightSideMouseArea.trackingScroll = false;
}
}
}
Item {
anchors.fill: parent
implicitHeight: rightSectionRowLayout.implicitHeight
implicitWidth: rightSectionRowLayout.implicitWidth
ScrollHint {
reveal: barRightSideMouseArea.hovered
icon: "volume_up"
tooltipText: Translation.tr("Scroll to change volume")
side: "right"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: rightSectionRowLayout
anchors.fill: parent
spacing: 5
layoutDirection: Qt.RightToLeft
RippleButton { // Right sidebar button
id: rightSidebarButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2
implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2
buttonRadius: Appearance.rounding.full
colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarRightOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
} }
onPressed: {
Hyprland.dispatch('global quickshell:sidebarRightToggle');
}
RowLayout {
id: indicatorsRowLayout
anchors.centerIn: parent
property real realSpacing: 15
spacing: 0
Revealer {
reveal: Audio.sink?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
MaterialSymbol {
text: "volume_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Revealer {
reveal: Audio.source?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
MaterialSymbol {
text: "mic_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
MaterialSymbol {
Layout.rightMargin: indicatorsRowLayout.realSpacing
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
SysTray {
bar: barRoot
visible: barRoot.useShortenedForm === 0
Layout.fillWidth: false
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
// Weather
Loader {
Layout.leftMargin: 8
Layout.fillHeight: true
active: Config.options.bar.weather.enable
sourceComponent: BarGroup {
implicitHeight: Appearance.sizes.baseBarHeight
WeatherBar {}
}
}
}
}
}
}
// Round decorators
Loader {
id: roundDecorators
anchors {
left: parent.left
right: parent.right
}
y: Appearance.sizes.barHeight
width: parent.width
height: Appearance.rounding.screenRounding
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
roundDecorators.y: 0
}
}
sourceComponent: Item {
implicitHeight: Appearance.rounding.screenRounding
RoundCorner {
id: leftCorner
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
size: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
}
}
}
RoundCorner {
id: rightCorner
anchors {
right: parent.right
top: !Config.options.bar.bottom ? parent.top : undefined
bottom: Config.options.bar.bottom ? parent.bottom : undefined
}
size: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopRight
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
rightCorner.corner: RoundCorner.CornerEnum.BottomRight
} }
} }
} }
@@ -0,0 +1,448 @@
import "./weather"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Item { // Bar content region
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
property real useShortenedForm: (Appearance.sizes.barHellaShortenScreenWidthThreshold >= screen?.width) ? 2 : (Appearance.sizes.barShortenScreenWidthThreshold >= screen?.width) ? 1 : 0
readonly property int centerSideModuleWidth: (useShortenedForm == 2) ? Appearance.sizes.barCenterSideModuleWidthHellaShortened : (useShortenedForm == 1) ? Appearance.sizes.barCenterSideModuleWidthShortened : Appearance.sizes.barCenterSideModuleWidth
component VerticalBarSeparator: Rectangle {
Layout.topMargin: Appearance.sizes.baseBarHeight / 3
Layout.bottomMargin: Appearance.sizes.baseBarHeight / 3
Layout.fillHeight: true
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
}
// Background shadow
Loader {
active: Config.options.bar.showBackground && Config.options.bar.cornerStyle === 1
anchors.fill: barBackground
sourceComponent: StyledRectangularShadow {
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
target: barBackground
}
}
// Background
Rectangle {
id: barBackground
anchors {
fill: parent
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
}
color: Config.options.bar.showBackground ? Appearance.colors.colLayer0 : "transparent"
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
border.color: Appearance.colors.colLayer0Border
}
MouseArea { // Left side | scroll to change brightness
id: barLeftSideMouseArea
anchors.left: parent.left
implicitHeight: Appearance.sizes.baseBarHeight
height: Appearance.sizes.barHeight
width: (root.width - middleSection.width) / 2
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barLeftSideMouseArea.hovered = true;
}
onExited: event => {
barLeftSideMouseArea.hovered = false;
barLeftSideMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
}
// Scroll to change brightness
WheelHandler {
onWheel: event => {
if (event.angleDelta.y < 0)
root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05);
else if (event.angleDelta.y > 0)
root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05);
// Store the mouse position and start tracking
barLeftSideMouseArea.lastScrollX = event.x;
barLeftSideMouseArea.lastScrollY = event.y;
barLeftSideMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barLeftSideMouseArea.trackingScroll) {
const dx = mouse.x - barLeftSideMouseArea.lastScrollX;
const dy = mouse.y - barLeftSideMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
GlobalStates.osdBrightnessOpen = false;
barLeftSideMouseArea.trackingScroll = false;
}
}
}
Item {
// Left section
anchors.fill: parent
implicitHeight: leftSectionRowLayout.implicitHeight
implicitWidth: leftSectionRowLayout.implicitWidth
ScrollHint {
reveal: barLeftSideMouseArea.hovered
icon: "light_mode"
tooltipText: Translation.tr("Scroll to change brightness")
side: "left"
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
}
RowLayout { // Content
id: leftSectionRowLayout
anchors.fill: parent
spacing: 10
RippleButton {
// Left sidebar button
Layout.alignment: Qt.AlignLeft | Qt.AlignVCenter
Layout.leftMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
property real buttonPadding: 5
implicitWidth: distroIcon.width + buttonPadding * 2
implicitHeight: distroIcon.height + buttonPadding * 2
buttonRadius: Appearance.rounding.full
colBackground: barLeftSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarLeftOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
CustomIcon {
id: distroIcon
anchors.centerIn: parent
width: 19.5
height: 19.5
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : "spark-symbolic"
colorize: true
color: Appearance.colors.colOnLayer0
}
}
ActiveWindow {
visible: root.useShortenedForm === 0
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: true
Layout.fillHeight: true
}
}
}
}
RowLayout { // Middle section
id: middleSection
anchors.centerIn: parent
spacing: Config.options?.bar.borderless ? 4 : 8
BarGroup {
id: leftCenterGroup
Layout.preferredWidth: root.centerSideModuleWidth
Layout.fillHeight: true
Resources {
alwaysShowAllResources: root.useShortenedForm === 2
Layout.fillWidth: root.useShortenedForm === 2
}
Media {
visible: root.useShortenedForm < 2
Layout.fillWidth: true
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
BarGroup {
id: middleCenterGroup
padding: workspacesWidget.widgetPadding
Layout.fillHeight: true
Workspaces {
id: workspacesWidget
Layout.fillHeight: true
MouseArea {
// Right-click to toggle overview
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
MouseArea {
id: rightCenterGroup
implicitWidth: rightCenterGroupContent.implicitWidth
implicitHeight: rightCenterGroupContent.implicitHeight
Layout.preferredWidth: root.centerSideModuleWidth
Layout.fillHeight: true
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
BarGroup {
id: rightCenterGroupContent
anchors.fill: parent
ClockWidget {
showDate: (Config.options.bar.verbose && root.useShortenedForm < 2)
Layout.alignment: Qt.AlignVCenter
Layout.fillWidth: true
}
UtilButtons {
visible: (Config.options.bar.verbose && root.useShortenedForm === 0)
Layout.alignment: Qt.AlignVCenter
}
BatteryIndicator {
visible: (root.useShortenedForm < 2 && UPower.displayDevice.isLaptopBattery)
Layout.alignment: Qt.AlignVCenter
}
}
}
VerticalBarSeparator {
visible: Config.options.bar.borderless && Config.options.bar.weather.enable
}
}
MouseArea { // Right side | scroll to change volume
id: barRightSideMouseArea
anchors.right: parent.right
implicitHeight: Appearance.sizes.baseBarHeight
height: Appearance.sizes.barHeight
width: (root.width - middleSection.width) / 2
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barRightSideMouseArea.hovered = true;
}
onExited: event => {
barRightSideMouseArea.hovered = false;
barRightSideMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
} else if (event.button === Qt.RightButton) {
MprisController.activePlayer.next();
}
}
// Scroll to change volume
WheelHandler {
onWheel: event => {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
if (event.angleDelta.y < 0)
Audio.sink.audio.volume -= step;
else if (event.angleDelta.y > 0)
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
// Store the mouse position and start tracking
barRightSideMouseArea.lastScrollX = event.x;
barRightSideMouseArea.lastScrollY = event.y;
barRightSideMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barRightSideMouseArea.trackingScroll) {
const dx = mouse.x - barRightSideMouseArea.lastScrollX;
const dy = mouse.y - barRightSideMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
GlobalStates.osdVolumeOpen = false;
barRightSideMouseArea.trackingScroll = false;
}
}
}
Item {
anchors.fill: parent
implicitHeight: rightSectionRowLayout.implicitHeight
implicitWidth: rightSectionRowLayout.implicitWidth
ScrollHint {
reveal: barRightSideMouseArea.hovered
icon: "volume_up"
tooltipText: Translation.tr("Scroll to change volume")
side: "right"
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
}
RowLayout {
id: rightSectionRowLayout
anchors.fill: parent
spacing: 5
layoutDirection: Qt.RightToLeft
RippleButton { // Right sidebar button
id: rightSidebarButton
Layout.alignment: Qt.AlignRight | Qt.AlignVCenter
Layout.rightMargin: Appearance.rounding.screenRounding
Layout.fillWidth: false
implicitWidth: indicatorsRowLayout.implicitWidth + 10 * 2
implicitHeight: indicatorsRowLayout.implicitHeight + 5 * 2
buttonRadius: Appearance.rounding.full
colBackground: barRightSideMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarRightOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
RowLayout {
id: indicatorsRowLayout
anchors.centerIn: parent
property real realSpacing: 15
spacing: 0
Revealer {
reveal: Audio.sink?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
MaterialSymbol {
text: "volume_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Revealer {
reveal: Audio.source?.audio?.muted ?? false
Layout.fillHeight: true
Layout.rightMargin: reveal ? indicatorsRowLayout.realSpacing : 0
Behavior on Layout.rightMargin {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
MaterialSymbol {
text: "mic_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Loader {
active: HyprlandXkb.layoutCodes.length > 1
visible: active
Layout.rightMargin: indicatorsRowLayout.realSpacing
sourceComponent: StyledText {
text: HyprlandXkb.currentLayoutCode
font.pixelSize: Appearance.font.pixelSize.small
color: rightSidebarButton.colText
}
}
MaterialSymbol {
Layout.rightMargin: indicatorsRowLayout.realSpacing
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
SysTray {
visible: root.useShortenedForm === 0
Layout.fillWidth: false
Layout.fillHeight: true
}
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
// Weather
Loader {
Layout.leftMargin: 8
Layout.fillHeight: true
active: Config.options.bar.weather.enable
sourceComponent: BarGroup {
implicitHeight: Appearance.sizes.baseBarHeight
WeatherBar {}
}
}
}
}
}
}
@@ -39,12 +39,13 @@ Item {
} }
CircularProgress { CircularProgress {
enableAnimation: false
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
lineWidth: 2 lineWidth: 2
value: percentage value: percentage
size: 26 implicitSize: 26
secondaryColor: (isLow && !isCharging) ? batteryLowBackground : Appearance.colors.colSecondaryContainer colSecondary: (isLow && !isCharging) ? batteryLowBackground : Appearance.colors.colSecondaryContainer
primaryColor: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer colPrimary: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
fill: (isLow && !isCharging) fill: (isLow && !isCharging)
MaterialSymbol { MaterialSymbol {
+5 -4
View File
@@ -37,7 +37,7 @@ Item {
} else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) { } else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
activePlayer.next(); activePlayer.next();
} else if (event.button === Qt.LeftButton) { } else if (event.button === Qt.LeftButton) {
Hyprland.dispatch("global quickshell:mediaControlsToggle") GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen
} }
} }
} }
@@ -53,9 +53,10 @@ Item {
Layout.leftMargin: rowLayout.spacing Layout.leftMargin: rowLayout.spacing
lineWidth: 2 lineWidth: 2
value: activePlayer?.position / activePlayer?.length value: activePlayer?.position / activePlayer?.length
size: 26 implicitSize: 26
secondaryColor: Appearance.colors.colSecondaryContainer colSecondary: Appearance.colors.colSecondaryContainer
primaryColor: Appearance.m3colors.m3onSecondaryContainer colPrimary: Appearance.m3colors.m3onSecondaryContainer
enableAnimation: false
MaterialSymbol { MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
@@ -21,9 +21,10 @@ Item {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
lineWidth: 2 lineWidth: 2
value: percentage value: percentage
size: 26 implicitSize: 26
secondaryColor: Appearance.colors.colSecondaryContainer colSecondary: Appearance.colors.colSecondaryContainer
primaryColor: Appearance.m3colors.m3onSecondaryContainer colPrimary: Appearance.m3colors.m3onSecondaryContainer
enableAnimation: false
MaterialSymbol { MaterialSymbol {
anchors.centerIn: parent anchors.centerIn: parent
@@ -8,8 +8,6 @@ import Quickshell.Services.SystemTray
Item { Item {
id: root id: root
required property var bar
height: parent.height height: parent.height
implicitWidth: rowLayout.implicitWidth implicitWidth: rowLayout.implicitWidth
Layout.leftMargin: Appearance.rounding.screenRounding Layout.leftMargin: Appearance.rounding.screenRounding
@@ -25,8 +23,6 @@ Item {
SysTrayItem { SysTrayItem {
required property SystemTrayItem modelData required property SystemTrayItem modelData
bar: root.bar
item: modelData item: modelData
} }
@@ -10,7 +10,7 @@ import Qt5Compat.GraphicalEffects
MouseArea { MouseArea {
id: root id: root
required property var bar property var bar: root.QsWindow.window
required property SystemTrayItem item required property SystemTrayItem item
property bool targetMenuOpen: false property bool targetMenuOpen: false
property int trayItemWidth: Appearance.font.pixelSize.larger property int trayItemWidth: Appearance.font.pixelSize.larger
@@ -59,12 +59,12 @@ MouseArea {
visible: false // There's already color overlay visible: false // There's already color overlay
anchors.fill: parent anchors.fill: parent
source: trayIcon source: trayIcon
desaturation: 1 // 1.0 means fully grayscale desaturation: 0.8 // 1.0 means fully grayscale
} }
ColorOverlay { ColorOverlay {
anchors.fill: desaturatedIcon anchors.fill: desaturatedIcon
source: desaturatedIcon source: desaturatedIcon
color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.6) color: ColorUtils.transparentize(Appearance.colors.colOnLayer0, 0.9)
} }
} }
} }
@@ -1,3 +1,4 @@
import qs
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import QtQuick import QtQuick
@@ -5,6 +6,7 @@ import QtQuick.Layouts
import Quickshell import Quickshell
import Quickshell.Hyprland import Quickshell.Hyprland
import Quickshell.Services.Pipewire import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
Item { Item {
id: root id: root
@@ -23,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton { sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.configPath("screenshot.qml")]) onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
MaterialSymbol { MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
fill: 1 fill: 1
@@ -55,7 +57,7 @@ Item {
visible: Config.options.bar.utilButtons.showKeyboardToggle visible: Config.options.bar.utilButtons.showKeyboardToggle
sourceComponent: CircleUtilButton { sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
onClicked: Hyprland.dispatch("global quickshell:oskToggle") onClicked: GlobalStates.oskOpen = !GlobalStates.oskOpen
MaterialSymbol { MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter horizontalAlignment: Qt.AlignHCenter
fill: 0 fill: 0
@@ -103,5 +105,38 @@ Item {
} }
} }
} }
Loader {
active: Config.options.bar.utilButtons.showPerformanceProfileToggle
visible: Config.options.bar.utilButtons.showPerformanceProfileToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: event => {
if (PowerProfiles.hasPerformanceProfile) {
switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: PowerProfiles.profile = PowerProfile.Balanced
break;
case PowerProfile.Balanced: PowerProfiles.profile = PowerProfile.Performance
break;
case PowerProfile.Performance: PowerProfiles.profile = PowerProfile.PowerSaver
break;
}
} else {
PowerProfiles.profile = PowerProfiles.profile == PowerProfile.Balanced ? PowerProfile.PowerSaver : PowerProfile.Balanced
}
}
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 0
text: switch(PowerProfiles.profile) {
case PowerProfile.PowerSaver: return "energy_savings_leaf"
case PowerProfile.Balanced: return "settings_slow_motion"
case PowerProfile.Performance: return "local_fire_department"
}
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer2
}
}
}
} }
} }
@@ -13,12 +13,12 @@ import Quickshell.Widgets
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
Item { Item {
required property var bar id: root
property bool borderless: Config.options.bar.borderless property bool borderless: Config.options.bar.borderless
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown) readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown)
property list<bool> workspaceOccupied: [] property list<bool> workspaceOccupied: []
property int widgetPadding: 4 property int widgetPadding: 4
property int workspaceButtonWidth: 26 property int workspaceButtonWidth: 26
@@ -26,7 +26,31 @@ Item {
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55 property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1 property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4 property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (monitor.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown
property bool showNumbers: false
Timer {
id: showNumbersTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.showNumbers = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showNumbersTimer.restart();
else {
showNumbersTimer.stop();
root.showNumbers = false;
}
}
function onSuperReleaseMightTriggerChanged() {
showNumbersTimer.stop()
}
}
// Function to update workspaceOccupied // Function to update workspaceOccupied
function updateWorkspaceOccupied() { function updateWorkspaceOccupied() {
@@ -87,8 +111,8 @@ Item {
implicitWidth: workspaceButtonWidth implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth implicitHeight: workspaceButtonWidth
radius: Appearance.rounding.full radius: Appearance.rounding.full
property var leftOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index)) property var leftOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index))
property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+2)) property var rightOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+2))
property var radiusLeft: leftOccupied ? 0 : Appearance.rounding.full property var radiusLeft: leftOccupied ? 0 : Appearance.rounding.full
property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full
@@ -98,7 +122,7 @@ Item {
bottomRightRadius: radiusRight bottomRightRadius: radiusRight
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4) color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor.activeWorkspace?.id === index+1)) ? 1 : 0 opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+1)) ? 1 : 0
Behavior on opacity { Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this) animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
@@ -176,9 +200,9 @@ Item {
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing") property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text StyledText { // Workspace number text
opacity: GlobalStates.workspaceShowNumbers opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || GlobalStates.workspaceShowNumbers)) || ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (GlobalStates.workspaceShowNumbers && !Config.options?.bar.workspaces.showAppIcons) || (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0 ) ? 1 : 0
z: 3 z: 3
@@ -188,7 +212,7 @@ Item {
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2) font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
text: `${button.workspaceValue}` text: `${button.workspaceValue}`
elide: Text.ElideRight elide: Text.ElideRight
color: (monitor.activeWorkspace?.id == button.workspaceValue) ? color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer : (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive) Appearance.colors.colOnLayer1Inactive)
@@ -200,7 +224,7 @@ Item {
Rectangle { // Dot instead of ws number Rectangle { // Dot instead of ws number
id: wsDot id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| GlobalStates.workspaceShowNumbers || root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow) || (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1 ) ? 0 : 1
visible: opacity > 0 visible: opacity > 0
@@ -208,7 +232,7 @@ Item {
width: workspaceButtonWidth * 0.18 width: workspaceButtonWidth * 0.18
height: width height: width
radius: width / 2 radius: width / 2
color: (monitor.activeWorkspace?.id == button.workspaceValue) ? color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary : Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer : (workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive) Appearance.colors.colOnLayer1Inactive)
@@ -222,20 +246,20 @@ Item {
width: workspaceButtonWidth width: workspaceButtonWidth
height: workspaceButtonWidth height: workspaceButtonWidth
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(workspaceButtonBackground.biggestWindow && !GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? (workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0 1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
visible: opacity > 0 visible: opacity > 0
IconImage { IconImage {
id: mainAppIcon id: mainAppIcon
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.right: parent.right anchors.right: parent.right
anchors.bottomMargin: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
anchors.rightMargin: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked (workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
source: workspaceButtonBackground.mainAppIconSource source: workspaceButtonBackground.mainAppIconSource
implicitSize: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
Behavior on opacity { Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -21,7 +21,7 @@ MouseArea {
MaterialSymbol { MaterialSymbol {
fill: 0 fill: 0
text: WeatherIcons.codeToName[Weather.data.wCode] text: WeatherIcons.codeToName[Weather.data?.wCode] ?? "question_mark"
iconSize: Appearance.font.pixelSize.large iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1 color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
@@ -31,7 +31,7 @@ MouseArea {
visible: true visible: true
font.pixelSize: Appearance.font.pixelSize.small font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1 color: Appearance.colors.colOnLayer1
text: Weather.data.temp text: Weather.data?.temp ?? "--°"
Layout.alignment: Qt.AlignVCenter Layout.alignment: Qt.AlignVCenter
} }
} }
@@ -14,7 +14,7 @@ Rectangle {
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
radius: Appearance.rounding.small radius: Appearance.rounding.small
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
clip: true clip: true
ColumnLayout { ColumnLayout {
@@ -74,7 +74,7 @@ Scope { // Scope
anchors.centerIn: parent anchors.centerIn: parent
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.windowRounding radius: Appearance.rounding.windowRounding
property real padding: 30 property real padding: 30
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2 implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
@@ -104,6 +104,7 @@ Singleton {
property color colOnLayer0: m3colors.m3onBackground property color colOnLayer0: m3colors.m3onBackground
property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency)) property color colLayer0Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.9, root.contentTransparency))
property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency)) property color colLayer0Active: ColorUtils.transparentize(ColorUtils.mix(colLayer0, colOnLayer0, 0.8, root.contentTransparency))
property color colLayer0Border: ColorUtils.mix(root.m3colors.m3outlineVariant, colLayer0, 0.4)
property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency); property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3surfaceContainerLow, m3colors.m3background, 0.8), root.contentTransparency);
property color colOnLayer1: m3colors.m3onSurfaceVariant; property color colOnLayer1: m3colors.m3onSurfaceVariant;
property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45); property color colOnLayer1Inactive: ColorUtils.mix(colOnLayer1, colLayer1, 0.45);
@@ -140,8 +141,8 @@ Singleton {
property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency) property color colSurfaceContainerHighest: ColorUtils.transparentize(m3colors.m3surfaceContainerHighest, root.contentTransparency)
property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95) property color colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85) property color colSurfaceContainerHighestActive: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.85)
property color colTooltip: m3colors.darkmode ? ColorUtils.mix(m3colors.m3background, "#3C4043", 0.5) : "#3C4043" // m3colors.m3inverseSurface in the specs, but the m3 website actually uses #3C4043 property color colTooltip: m3colors.m3inverseSurface
property color colOnTooltip: "#F8F9FA" // m3colors.m3inverseOnSurface in the specs, but the m3 website actually uses this color property color colOnTooltip: m3colors.m3inverseOnSurface
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5) property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7) property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
property color colOutlineVariant: m3colors.m3outlineVariant property color colOutlineVariant: m3colors.m3outlineVariant
@@ -265,7 +266,6 @@ Singleton {
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
}} }}
} }
property QtObject clickBounce: QtObject { property QtObject clickBounce: QtObject {
property int duration: 200 property int duration: 200
property int type: Easing.BezierSpline property int type: Easing.BezierSpline
@@ -278,7 +278,7 @@ Singleton {
}} }}
} }
property QtObject scroll: QtObject { property QtObject scroll: QtObject {
property int duration: 400 property int duration: 200
property int type: Easing.BezierSpline property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.standardDecel property list<real> bezierCurve: animationCurves.standardDecel
} }
@@ -8,6 +8,7 @@ Singleton {
id: root id: root
property string filePath: Directories.shellConfigPath property string filePath: Directories.shellConfigPath
property alias options: configOptionsJsonAdapter property alias options: configOptionsJsonAdapter
property bool ready: false
function setNestedValue(nestedKey, value) { function setNestedValue(nestedKey, value) {
let keys = nestedKey.split("."); let keys = nestedKey.split(".");
@@ -41,10 +42,10 @@ Singleton {
FileView { FileView {
path: root.filePath path: root.filePath
watchChanges: true watchChanges: true
onFileChanged: reload() onFileChanged: reload()
onAdapterUpdated: writeAdapter() onAdapterUpdated: writeAdapter()
onLoaded: root.ready = true
onLoadFailed: error => { onLoadFailed: error => {
if (error == FileViewError.FileNotFound) { if (error == FileViewError.FileNotFound) {
writeAdapter(); writeAdapter();
@@ -59,7 +60,22 @@ Singleton {
} }
property JsonObject ai: JsonObject { property JsonObject ai: JsonObject {
property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n\nThanks!\n\n## Tools\nMay or may not be available depending on the user's settings. If they're available, follow these guidelines:\n\n### Search\n- When user asks for information that might benefit from up-to-date information, use this to get search access\n\n### Shell configuration\n- Always fetch the config options to see the available keys before setting\n- Avoid unnecessarily asking the user to confirm the changes they explicitly asked for, just do it\n" property string systemPrompt: "## Style\n- Use casual tone, don't be formal! Make sure you answer precisely without hallucination and prefer bullet points over walls of text. You can have a friendly greeting at the beginning of the conversation, but don't repeat the user's question\n\n## Context (ignore when irrelevant)\n- You are a helpful and inspiring sidebar assistant on a {DISTRO} Linux system\n- Desktop environment: {DE}\n- Current date & time: {DATETIME}\n- Focused app: {WINDOWCLASS}\n\n## Presentation\n- Use Markdown features in your response: \n - **Bold** text to **highlight keywords** in your response\n - **Split long information into small sections** with h2 headers and a relevant emoji at the start of it (for example `## 🐧 Linux`). Bullet points are preferred over long paragraphs, unless you're offering writing support or instructed otherwise by the user.\n- Asked to compare different options? You should firstly use a table to compare the main aspects, then elaborate or include relevant comments from online forums *after* the table. Make sure to provide a final recommendation for the user's use case!\n- Use LaTeX formatting for mathematical and scientific notations whenever appropriate. Enclose all LaTeX '$$' delimiters. NEVER generate LaTeX code in a latex block unless the user explicitly asks for it. DO NOT use LaTeX for regular documents (resumes, letters, essays, CVs, etc.).\n"
property string tool: "functions" // search, functions, or none
property list<var> extraModels: [
{
"api_format": "openai", // Most of the time you want "openai". Use "gemini" for Google's models
"description": "This is a custom model. Edit the config to add more! | Anyway, this is DeepSeek R1 Distill LLaMA 70B",
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
"homepage": "https://openrouter.ai/deepseek/deepseek-r1-distill-llama-70b:free", // Not mandatory
"icon": "spark-symbolic", // Not mandatory
"key_get_link": "https://openrouter.ai/settings/keys", // Not mandatory
"key_id": "openrouter",
"model": "deepseek/deepseek-r1-distill-llama-70b:free",
"name": "Custom: DS R1 Dstl. LLaMA 70B",
"requires_key": true
}
]
} }
property JsonObject appearance: JsonObject { property JsonObject appearance: JsonObject {
@@ -99,6 +115,7 @@ Singleton {
property real clockX: -500 property real clockX: -500
property real clockY: -500 property real clockY: -500
property string wallpaperPath: "" property string wallpaperPath: ""
property string thumbnailPath: ""
property JsonObject parallax: JsonObject { property JsonObject parallax: JsonObject {
property bool enableWorkspace: true property bool enableWorkspace: true
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
@@ -107,6 +124,14 @@ Singleton {
} }
property JsonObject bar: JsonObject { property JsonObject bar: JsonObject {
property JsonObject autoHide: JsonObject {
property bool enable: false
property bool pushWindows: false
property JsonObject showWhenPressingSuper: JsonObject {
property bool enable: true
property int delay: 100
}
}
property bool bottom: false // Instead of top property bool bottom: false // Instead of top
property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle property int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle
property bool borderless: false // true for no grouping of items property bool borderless: false // true for no grouping of items
@@ -124,6 +149,7 @@ Singleton {
property bool showMicToggle: false property bool showMicToggle: false
property bool showKeyboardToggle: true property bool showKeyboardToggle: true
property bool showDarkModeToggle: true property bool showDarkModeToggle: true
property bool showPerformanceProfileToggle: false
} }
property JsonObject tray: JsonObject { property JsonObject tray: JsonObject {
property bool monochromeIcons: true property bool monochromeIcons: true
@@ -163,6 +189,14 @@ Singleton {
property list<string> ignoredAppRegexes: [] property list<string> ignoredAppRegexes: []
} }
property JsonObject interactions: JsonObject {
property JsonObject scrolling: JsonObject {
property int mouseScrollDeltaThreshold: 120 // delta >= this then it gets detected as mouse scroll rather than touchpad
property int mouseScrollFactor: 120
property int touchpadScrollFactor: 450
}
}
property JsonObject language: JsonObject { property JsonObject language: JsonObject {
property JsonObject translator: JsonObject { property JsonObject translator: JsonObject {
property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google property string engine: "auto" // Run `trans -list-engines` for available engines. auto should use google
@@ -171,6 +205,20 @@ Singleton {
} }
} }
property JsonObject light: JsonObject {
property JsonObject night: JsonObject {
property bool automatic: true
property string from: "19:00" // Format: "HH:mm", 24-hour time
property string to: "06:30" // Format: "HH:mm", 24-hour time
property int colorTemperature: 5000
}
}
property JsonObject media: JsonObject {
// Attempt to remove dupes (the aggregator playerctl one and browsers' native ones when there's plasma browser integration)
property bool filterDuplicatePlayers: true
}
property JsonObject networking: JsonObject { property JsonObject networking: JsonObject {
property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36" property string userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
} }
@@ -185,6 +233,7 @@ Singleton {
} }
property JsonObject overview: JsonObject { property JsonObject overview: JsonObject {
property bool enable: true
property real scale: 0.18 // Relative to screen size property real scale: 0.18 // Relative to screen size
property real rows: 2 property real rows: 2
property real columns: 5 property real columns: 5
@@ -207,6 +256,7 @@ Singleton {
} }
property JsonObject sidebar: JsonObject { property JsonObject sidebar: JsonObject {
property bool keepRightSidebarLoaded: true
property JsonObject translator: JsonObject { property JsonObject translator: JsonObject {
property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag. property int delay: 300 // Delay before sending request. Reduces (potential) rate limits and lag.
} }
@@ -15,8 +15,8 @@ Singleton {
readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0] readonly property string downloads: StandardPaths.standardLocations(StandardPaths.DownloadLocation)[0]
// Other dirs used by the shell, without "file://" // Other dirs used by the shell, without "file://"
property string assetsPath: Quickshell.configPath("assets") property string assetsPath: Quickshell.shellPath("assets")
property string scriptPath: Quickshell.configPath("scripts") property string scriptPath: Quickshell.shellPath("scripts")
property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`) property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`)
property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`) property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`)
property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`) property string booruPreviews: FileUtils.trimFileProtocol(`${Directories.cache}/media/boorus`)
@@ -32,7 +32,7 @@ Singleton {
property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`) property string cliphistDecode: FileUtils.trimFileProtocol(`/tmp/quickshell/media/cliphist`)
property string screenshotTemp: "/tmp/quickshell/media/screenshot" property string screenshotTemp: "/tmp/quickshell/media/screenshot"
property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`) property string wallpaperSwitchScriptPath: FileUtils.trimFileProtocol(`${Directories.scriptPath}/colors/switchwall.sh`)
property string defaultAiPrompts: Quickshell.configPath("defaults/ai/prompts") property string defaultAiPrompts: Quickshell.shellPath("defaults/ai/prompts")
property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`) property string userAiPrompts: FileUtils.trimFileProtocol(`${Directories.shellConfig}/ai/prompts`)
property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`) property string aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`)
// Cleanup on init // Cleanup on init
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import "./fuzzysort.js" as FuzzySort
/**
* Wrapper for FuzzySort to play nicely with Quickshell's imports
*/
Singleton {
function go(...args) {
return FuzzySort.go(...args)
}
function prepare(...args) {
return FuzzySort.prepare(...args)
}
}
@@ -0,0 +1,18 @@
pragma Singleton
import Quickshell
import "./levendist.js" as Levendist
/**
* Wrapper for levendist.js to play nicely with Quickshell's imports
*/
Singleton {
function computeScore(...args) {
return Levendist.computeScore(...args)
}
function computeTextMatchScore(...args) {
return Levendist.computeTextMatchScore(...args)
}
}
@@ -1,7 +1,5 @@
// From https://github.com/rafzby/circular-progressbar with modifications
// License: LGPL-3.0 - A copy can be found in `licenses` folder of repo
import QtQuick import QtQuick
import QtQuick.Shapes
import qs.modules.common import qs.modules.common
/** /**
@@ -10,86 +8,81 @@ import qs.modules.common
Item { Item {
id: root id: root
property int size: 30 property int implicitSize: 30
property int lineWidth: 2 property int lineWidth: 2
property real value: 0 property real value: 0
property color primaryColor: Appearance.m3colors.m3onSecondaryContainer property color colPrimary: Appearance.m3colors.m3onSecondaryContainer
property color secondaryColor: Appearance.colors.colSecondaryContainer property color colSecondary: Appearance.colors.colSecondaryContainer
property real gapAngle: Math.PI / 9 property real gapAngle: 360 / 18
property bool fill: false property bool fill: false
property int fillOverflow: 2 property int fillOverflow: 2
property int animationDuration: 1000 property bool enableAnimation: true
property int animationDuration: 800
property var easingType: Easing.OutCubic property var easingType: Easing.OutCubic
width: size implicitWidth: implicitSize
height: size implicitHeight: implicitSize
signal animationFinished(); property real degree: value * 360
property real centerX: root.width / 2
property real centerY: root.height / 2
property real arcRadius: root.implicitSize / 2 - root.lineWidth
property real startAngle: -90
Behavior on degree {
enabled: root.enableAnimation
NumberAnimation {
duration: root.animationDuration
easing.type: root.easingType
}
onValueChanged: {
canvas.degree = value * 360;
}
onPrimaryColorChanged: {
canvas.requestPaint();
}
onSecondaryColorChanged: {
canvas.requestPaint();
} }
Canvas { Loader {
id: canvas active: root.fill
property real degree: 0
anchors.fill: parent anchors.fill: parent
antialiasing: true
sourceComponent: Rectangle {
onDegreeChanged: { radius: 9999
requestPaint(); color: root.colSecondary
} }
}
onPaint: { Shape {
var ctx = getContext("2d"); anchors.fill: parent
var x = root.width / 2; layer.enabled: true
var y = root.height / 2; layer.smooth: true
var radius = root.size / 2 - root.lineWidth; preferredRendererType: Shape.CurveRenderer
var startAngle = (Math.PI / 180) * 270; ShapePath {
var fullAngle = (Math.PI / 180) * (270 + 360); id: secondaryPath
var progressAngle = (Math.PI / 180) * (270 + degree); strokeColor: root.colSecondary
var epsilon = 0.01; // Small angle in radians strokeWidth: root.lineWidth
capStyle: ShapePath.RoundCap
ctx.reset(); fillColor: "transparent"
if (root.fill) { PathAngleArc {
ctx.fillStyle = root.secondaryColor; centerX: root.centerX
ctx.beginPath(); centerY: root.centerY
ctx.arc(x, y, radius + fillOverflow, startAngle, fullAngle); radiusX: root.arcRadius
ctx.fill(); radiusY: root.arcRadius
startAngle: root.startAngle - root.gapAngle
sweepAngle: -(360 - root.degree - 2 * root.gapAngle)
} }
ctx.lineCap = 'round';
ctx.lineWidth = root.lineWidth;
// Secondary
ctx.beginPath();
ctx.arc(x, y, radius, progressAngle + gapAngle, fullAngle - gapAngle);
ctx.strokeStyle = root.secondaryColor;
ctx.stroke();
// Primary (value indication)
var endAngle = progressAngle + (value > 0 ? 0 : epsilon);
ctx.beginPath();
ctx.arc(x, y, radius, startAngle, endAngle);
ctx.strokeStyle = root.primaryColor;
ctx.stroke();
} }
ShapePath {
Behavior on degree { id: primaryPath
NumberAnimation { strokeColor: root.colPrimary
duration: root.animationDuration strokeWidth: root.lineWidth
easing.type: root.easingType capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathAngleArc {
centerX: root.centerX
centerY: root.centerY
radiusX: root.arcRadius
radiusY: root.arcRadius
startAngle: root.startAngle
sweepAngle: root.degree
} }
} }
} }
} }
@@ -3,7 +3,7 @@ import QtQuick.Layouts
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
Flickable { StyledFlickable {
id: root id: root
property real baseWidth: 550 property real baseWidth: 550
property bool forceWidth: false property bool forceWidth: false
@@ -25,4 +25,5 @@ Flickable {
} }
spacing: 20 spacing: 20
} }
} }
@@ -9,7 +9,7 @@ Item {
property bool colorize: false property bool colorize: false
property color color property color color
property string source: "" property string source: ""
property string iconFolder: Qt.resolvedUrl(Quickshell.configPath("assets/icons")) // The folder to check first property string iconFolder: Qt.resolvedUrl(Quickshell.shellPath("assets/icons")) // The folder to check first
width: 30 width: 30
height: 30 height: 30
@@ -93,13 +93,23 @@ Button {
root.down = false root.down = false
if (event.button != Qt.LeftButton) return; if (event.button != Qt.LeftButton) return;
if (root.releaseAction) root.releaseAction(); if (root.releaseAction) root.releaseAction();
root.click() // Because the MouseArea already consumed the event }
onClicked: (event) => {
if (event.button != Qt.LeftButton) return;
root.click()
} }
onCanceled: (event) => { onCanceled: (event) => {
root.down = false root.down = false
} }
onPressAndHold: () => {
altAction();
root.down = false;
root.clicked = false;
};
} }
background: Rectangle { background: Rectangle {
id: buttonBackground id: buttonBackground
topLeftRadius: root.leftRadius topLeftRadius: root.leftRadius
@@ -214,13 +214,13 @@ Item { // Notification item area
onLinkActivated: (link) => { onLinkActivated: (link) => {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarRightClose") GlobalStates.sidebarRightOpen = false
} }
PointingHandLinkHover {} PointingHandLinkHover {}
} }
Flickable { // Notification actions StyledFlickable { // Notification actions
id: actionsFlickable id: actionsFlickable
Layout.fillWidth: true Layout.fillWidth: true
implicitHeight: actionRowLayout.implicitHeight implicitHeight: actionRowLayout.implicitHeight
@@ -1,4 +1,5 @@
import QtQuick 2.9 import QtQuick
import QtQuick.Shapes
Item { Item {
id: root id: root
@@ -6,55 +7,57 @@ Item {
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight } enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight }
property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft
property int size: 25 property int implicitSize: 25
property color color: "#000000" property color color: "#000000"
onColorChanged: { implicitWidth: implicitSize
canvas.requestPaint(); implicitHeight: implicitSize
}
onCornerChanged: {
canvas.requestPaint();
}
implicitWidth: size
implicitHeight: size
Canvas {
id: canvas
Shape {
anchors.fill: parent anchors.fill: parent
antialiasing: true layer.enabled: true
layer.smooth: true
onPaint: { preferredRendererType: Shape.CurveRenderer
var ctx = getContext("2d");
var r = root.size; ShapePath {
ctx.clearRect(0, 0, canvas.width, canvas.height); id: shapePath
ctx.beginPath(); strokeWidth: 0
switch (root.corner) {
case RoundCorner.CornerEnum.TopLeft: fillColor: root.color
ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2); startX: switch (root.corner) {
ctx.lineTo(0, 0); case RoundCorner.CornerEnum.TopLeft: return 0;
break; case RoundCorner.CornerEnum.TopRight: return root.implicitSize;
case RoundCorner.CornerEnum.TopRight: case RoundCorner.CornerEnum.BottomLeft: return 0;
ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI); case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
ctx.lineTo(r, 0); }
break; startY: switch (root.corner) {
case RoundCorner.CornerEnum.BottomLeft: case RoundCorner.CornerEnum.TopLeft: return 0;
ctx.arc(r, 0, r, Math.PI / 2, Math.PI); case RoundCorner.CornerEnum.TopRight: return 0;
ctx.lineTo(0, r); case RoundCorner.CornerEnum.BottomLeft: return root.implicitSize;
break; case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
case RoundCorner.CornerEnum.BottomRight: }
ctx.arc(0, 0, r, 0, Math.PI / 2); PathAngleArc {
ctx.lineTo(r, r); moveToStart: false
break; centerX: root.implicitSize - shapePath.startX
centerY: root.implicitSize - shapePath.startY
radiusX: root.implicitSize
radiusY: root.implicitSize
startAngle: switch (root.corner) {
case RoundCorner.CornerEnum.TopLeft: return 180;
case RoundCorner.CornerEnum.TopRight: return -90;
case RoundCorner.CornerEnum.BottomLeft: return 90;
case RoundCorner.CornerEnum.BottomRight: return 0;
}
sweepAngle: 90
}
PathLine {
x: shapePath.startX
y: shapePath.startY
} }
ctx.closePath();
ctx.fillStyle = root.color;
ctx.fill();
} }
} }
Behavior on size { Behavior on implicitSize {
animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this)
} }
@@ -63,12 +63,13 @@ Item {
Layout.rightMargin: dialogPadding Layout.rightMargin: dialogPadding
} }
ListView { StyledListView {
id: choiceListView id: choiceListView
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
clip: true clip: true
currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1 currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
spacing: 6
model: ScriptModel { model: ScriptModel {
id: choiceModel id: choiceModel
@@ -0,0 +1,35 @@
import QtQuick
import qs.modules.common
Flickable {
id: root
maximumFlickVelocity: 3500
boundsBehavior: Flickable.DragOverBounds
property real touchpadScrollFactor: Config?.options.interactions.scrolling.touchpadScrollFactor ?? 100
property real mouseScrollFactor: Config?.options.interactions.scrolling.mouseScrollFactor ?? 50
property real mouseScrollDeltaThreshold: Config?.options.interactions.scrolling.mouseScrollDeltaThreshold ?? 120
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
const delta = wheelEvent.angleDelta.y / root.mouseScrollDeltaThreshold;
// The angleDelta.y of a touchpad is usually small and continuous,
// while that of a mouse wheel is typically in multiples of ±120.
var scrollFactor = Math.abs(wheelEvent.angleDelta.y) >= root.mouseScrollDeltaThreshold ? root.mouseScrollFactor : root.touchpadScrollFactor;
var targetY = root.contentY - delta * scrollFactor;
targetY = Math.max(0, Math.min(targetY, root.contentHeight - root.height));
root.contentY = targetY;
}
}
Behavior on contentY {
NumberAnimation {
id: scrollAnim
duration: Appearance.animation.scroll.duration
easing.type: Appearance.animation.scroll.type
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
}
}
}
@@ -15,11 +15,41 @@ ListView {
property real dragDistance: 0 property real dragDistance: 0
property bool popin: true property bool popin: true
property real touchpadScrollFactor: Config?.options.interactions.scrolling.touchpadScrollFactor ?? 100
property real mouseScrollFactor: Config?.options.interactions.scrolling.mouseScrollFactor ?? 50
property real mouseScrollDeltaThreshold: Config?.options.interactions.scrolling.mouseScrollDeltaThreshold ?? 120
function resetDrag() { function resetDrag() {
root.dragIndex = -1 root.dragIndex = -1
root.dragDistance = 0 root.dragDistance = 0
} }
maximumFlickVelocity: 3500
boundsBehavior: Flickable.DragOverBounds
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.NoButton
onWheel: function(wheelEvent) {
const delta = wheelEvent.angleDelta.y / root.mouseScrollDeltaThreshold;
// The angleDelta.y of a touchpad is usually small and continuous,
// while that of a mouse wheel is typically in multiples of ±120.
var scrollFactor = Math.abs(wheelEvent.angleDelta.y) >= root.mouseScrollDeltaThreshold ? root.mouseScrollFactor : root.touchpadScrollFactor;
var targetY = root.contentY - delta * scrollFactor;
targetY = Math.max(0, Math.min(targetY, root.contentHeight - root.height));
root.contentY = targetY;
}
}
Behavior on contentY {
NumberAnimation {
id: scrollAnim
duration: Appearance.animation.scroll.duration
easing.type: Appearance.animation.scroll.type
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
}
}
add: Transition { add: Transition {
animations: [ animations: [
Appearance?.animation.elementMove.numberAnimation.createObject(this, { Appearance?.animation.elementMove.numberAnimation.createObject(this, {
@@ -10,7 +10,7 @@ import Quickshell.Services.Pipewire
RadioButton { RadioButton {
id: root id: root
implicitHeight: 40 implicitHeight: contentItem.implicitHeight + 4 * 2
property string description property string description
property color activeColor: Appearance?.colors.colPrimary ?? "#685496" property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F" property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F"
@@ -20,6 +20,7 @@ RadioButton {
indicator: Item{} indicator: Item{}
contentItem: RowLayout { contentItem: RowLayout {
id: contentItem
Layout.fillWidth: true Layout.fillWidth: true
spacing: 12 spacing: 12
Rectangle { Rectangle {
+2 -2
View File
@@ -94,7 +94,7 @@ Scope { // Scope
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.large radius: Appearance.rounding.large
} }
@@ -129,7 +129,7 @@ Scope { // Scope
DockSeparator {} DockSeparator {}
DockButton { DockButton {
Layout.fillHeight: true Layout.fillHeight: true
onClicked: Hyprland.dispatch("global quickshell:overviewToggle") onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
contentItem: MaterialSymbol { contentItem: MaterialSymbol {
anchors.fill: parent anchors.fill: parent
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
@@ -26,8 +26,18 @@ Scope {
property list<real> visualizerPoints: [] property list<real> visualizerPoints: []
property bool hasPlasmaIntegration: false property bool hasPlasmaIntegration: false
Process {
id: plasmaIntegrationAvailabilityCheckProc
running: true
command: ["bash", "-c", "command -v plasma-browser-integration-host"]
onExited: (exitCode, exitStatus) => {
root.hasPlasmaIntegration = (exitCode === 0);
}
}
function isRealPlayer(player) { function isRealPlayer(player) {
// return true if (!Config.options.media.filterDuplicatePlayers) {
return true;
}
return ( return (
// Remove unecessary native buses from browsers if there's plasma integration // Remove unecessary native buses from browsers if there's plasma integration
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) && !(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
@@ -88,7 +98,12 @@ Scope {
Loader { Loader {
id: mediaControlsLoader id: mediaControlsLoader
active: false active: GlobalStates.mediaControlsOpen
onActiveChanged: {
if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) {
GlobalStates.mediaControlsOpen = false;
}
}
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: mediaControlsRoot id: mediaControlsRoot
@@ -160,11 +175,7 @@ Scope {
description: "Toggles media controls on press" description: "Toggles media controls on press"
onPressed: { onPressed: {
if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) { GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen;
return;
}
mediaControlsLoader.active = !mediaControlsLoader.active;
if(mediaControlsLoader.active) Notifications.timeoutAll();
} }
} }
GlobalShortcut { GlobalShortcut {
@@ -172,8 +183,7 @@ Scope {
description: "Opens media controls on press" description: "Opens media controls on press"
onPressed: { onPressed: {
mediaControlsLoader.active = true; GlobalStates.mediaControlsOpen = true;
Notifications.timeoutAll();
} }
} }
GlobalShortcut { GlobalShortcut {
@@ -181,7 +191,7 @@ Scope {
description: "Closes media controls on press" description: "Closes media controls on press"
onPressed: { onPressed: {
mediaControlsLoader.active = false; GlobalStates.mediaControlsOpen = false;
} }
} }
@@ -12,12 +12,11 @@ import Quickshell.Wayland
Scope { Scope {
id: root id: root
property bool showOsdValues: false
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen) property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen)
function triggerOsd() { function triggerOsd() {
showOsdValues = true GlobalStates.osdBrightnessOpen = true
osdTimeout.restart() osdTimeout.restart()
} }
@@ -27,7 +26,7 @@ Scope {
repeat: false repeat: false
running: false running: false
onTriggered: { onTriggered: {
showOsdValues = false GlobalStates.osdBrightnessOpen = false
} }
} }
@@ -35,7 +34,7 @@ Scope {
target: Audio.sink?.audio ?? null target: Audio.sink?.audio ?? null
function onVolumeChanged() { function onVolumeChanged() {
if (!Audio.ready) return if (!Audio.ready) return
root.showOsdValues = false GlobalStates.osdBrightnessOpen = false
} }
} }
@@ -49,7 +48,7 @@ Scope {
Loader { Loader {
id: osdLoader id: osdLoader
active: showOsdValues active: GlobalStates.osdBrightnessOpen
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: osdRoot id: osdRoot
@@ -91,7 +90,7 @@ Scope {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onEntered: root.showOsdValues = false onEntered: GlobalStates.osdBrightnessOpen = false
} }
Behavior on implicitHeight { Behavior on implicitHeight {
@@ -125,11 +124,11 @@ Scope {
} }
function hide() { function hide() {
showOsdValues = false GlobalStates.osdBrightnessOpen = false
} }
function toggle() { function toggle() {
showOsdValues = !showOsdValues GlobalStates.osdBrightnessOpen = !GlobalStates.osdBrightnessOpen
} }
} }
@@ -146,7 +145,7 @@ Scope {
description: "Hides brightness OSD on press" description: "Hides brightness OSD on press"
onPressed: { onPressed: {
root.showOsdValues = false GlobalStates.osdBrightnessOpen = false
} }
} }
@@ -12,12 +12,11 @@ import Quickshell.Hyprland
Scope { Scope {
id: root id: root
property bool showOsdValues: false
property string protectionMessage: "" property string protectionMessage: ""
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
function triggerOsd() { function triggerOsd() {
showOsdValues = true GlobalStates.osdVolumeOpen = true
osdTimeout.restart() osdTimeout.restart()
} }
@@ -27,7 +26,7 @@ Scope {
repeat: false repeat: false
running: false running: false
onTriggered: { onTriggered: {
root.showOsdValues = false GlobalStates.osdVolumeOpen = false
root.protectionMessage = "" root.protectionMessage = ""
} }
} }
@@ -35,7 +34,7 @@ Scope {
Connections { Connections {
target: Brightness target: Brightness
function onBrightnessChanged() { function onBrightnessChanged() {
showOsdValues = false GlobalStates.osdVolumeOpen = false
} }
} }
@@ -61,7 +60,7 @@ Scope {
Loader { Loader {
id: osdLoader id: osdLoader
active: showOsdValues active: GlobalStates.osdVolumeOpen
sourceComponent: PanelWindow { sourceComponent: PanelWindow {
id: osdRoot id: osdRoot
@@ -103,7 +102,7 @@ Scope {
MouseArea { MouseArea {
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
onEntered: root.showOsdValues = false onEntered: GlobalStates.osdVolumeOpen = false
} }
ColumnLayout { ColumnLayout {
@@ -177,11 +176,11 @@ Scope {
} }
function hide() { function hide() {
showOsdValues = false GlobalStates.osdVolumeOpen = false
} }
function toggle() { function toggle() {
showOsdValues = !showOsdValues GlobalStates.osdVolumeOpen = !GlobalStates.osdVolumeOpen
} }
} }
GlobalShortcut { GlobalShortcut {
@@ -197,7 +196,7 @@ Scope {
description: "Hides volume OSD on press" description: "Hides volume OSD on press"
onPressed: { onPressed: {
root.showOsdValues = false GlobalStates.osdVolumeOpen = false
} }
} }
@@ -49,7 +49,10 @@ Item {
Layout.topMargin: valueIndicatorVerticalPadding Layout.topMargin: valueIndicatorVerticalPadding
Layout.bottomMargin: valueIndicatorVerticalPadding Layout.bottomMargin: valueIndicatorVerticalPadding
MaterialSymbol { // Icon MaterialSymbol { // Icon
anchors.centerIn: parent anchors {
centerIn: parent
alignWhenCentered: !root.rotateIcon
}
color: Appearance.colors.colOnLayer0 color: Appearance.colors.colOnLayer0
renderType: Text.QtRendering renderType: Text.QtRendering
@@ -24,7 +24,7 @@ Scope { // Scope
Loader { Loader {
id: oskLoader id: oskLoader
active: false active: GlobalStates.oskOpen
onActiveChanged: { onActiveChanged: {
if (!oskLoader.active) { if (!oskLoader.active) {
Ydotool.releaseAllKeys(); Ydotool.releaseAllKeys();
@@ -124,15 +124,15 @@ Scope { // Scope
target: "osk" target: "osk"
function toggle(): void { function toggle(): void {
oskLoader.active = !oskLoader.active GlobalStates.oskOpen = !GlobalStates.oskOpen;
} }
function close(): void { function close(): void {
oskLoader.active = false GlobalStates.oskOpen = false
} }
function open(): void { function open(): void {
oskLoader.active = true GlobalStates.oskOpen = true
} }
} }
@@ -141,7 +141,7 @@ Scope { // Scope
description: "Toggles on screen keyboard on press" description: "Toggles on screen keyboard on press"
onPressed: { onPressed: {
oskLoader.active = !oskLoader.active; GlobalStates.oskOpen = !GlobalStates.oskOpen;
} }
} }
@@ -150,7 +150,7 @@ Scope { // Scope
description: "Opens on screen keyboard on press" description: "Opens on screen keyboard on press"
onPressed: { onPressed: {
oskLoader.active = true; GlobalStates.oskOpen = true
} }
} }
@@ -159,7 +159,7 @@ Scope { // Scope
description: "Closes on screen keyboard on press" description: "Closes on screen keyboard on press"
onPressed: { onPressed: {
oskLoader.active = false; GlobalStates.oskOpen = false
} }
} }
@@ -13,8 +13,10 @@ import Quickshell.Hyprland
Item { Item {
id: root id: root
property var activeLayoutName: Config.options?.osk.layout ?? Layouts.defaultLayout
property var layouts: Layouts.byName property var layouts: Layouts.byName
property var activeLayoutName: (layouts.hasOwnProperty(Config.options?.osk.layout))
? Config.options?.osk.layout
: Layouts.defaultLayout
property var currentLayout: layouts[activeLayoutName] property var currentLayout: layouts[activeLayoutName]
implicitWidth: keyRows.implicitWidth implicitWidth: keyRows.implicitWidth
@@ -1,11 +1,11 @@
// We're going to use ydotool // We're going to use ydotool
// See /usr/include/linux/input-event-codes.h for keycodes // See /usr/include/linux/input-event-codes.h for keycodes
const defaultLayout = "qwerty_full"; const defaultLayout = "English (US)";
const byName = { const byName = {
"qwerty_full": { "English (US)": {
name: "QWERTY - Full",
name_short: "US", name_short: "US",
description: "QWERTY - Full",
comment: "Like physical keyboard", comment: "Like physical keyboard",
// A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type) // A key looks like this: { k: "a", ks: "A", t: "normal" } (key, key-shift, type)
// key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand // key types are: normal, tab, caps, shift, control, fn (normal w/ half height), space, expand
@@ -113,9 +113,9 @@ const byName = {
] ]
] ]
}, },
"qwertz_full": { "German": {
name: "QWERTZ - Full",
name_short: "DE", name_short: "DE",
description: "QWERTZ - Full",
comment: "Keyboard layout commonly used in German-speaking countries", comment: "Keyboard layout commonly used in German-speaking countries",
keys: [ keys: [
[ [
@@ -214,5 +214,99 @@ const byName = {
{ keytype: "normal", label: "⇨", shape: "normal", keycode: 106 }, { keytype: "normal", label: "⇨", shape: "normal", keycode: 106 },
] ]
] ]
},
"Russian": {
name_short: "RU",
description: "ЙЦУКЕН - Full",
comment: "Standard Russian keyboard layout",
keys: [
[
{ keytype: "normal", label: "Esc", shape: "fn", keycode: 1 },
{ keytype: "normal", label: "F1", shape: "fn", keycode: 59 },
{ keytype: "normal", label: "F2", shape: "fn", keycode: 60 },
{ keytype: "normal", label: "F3", shape: "fn", keycode: 61 },
{ keytype: "normal", label: "F4", shape: "fn", keycode: 62 },
{ keytype: "normal", label: "F5", shape: "fn", keycode: 63 },
{ keytype: "normal", label: "F6", shape: "fn", keycode: 64 },
{ keytype: "normal", label: "F7", shape: "fn", keycode: 65 },
{ keytype: "normal", label: "F8", shape: "fn", keycode: 66 },
{ keytype: "normal", label: "F9", shape: "fn", keycode: 67 },
{ keytype: "normal", label: "F10", shape: "fn", keycode: 68 },
{ keytype: "normal", label: "F11", shape: "fn", keycode: 87 },
{ keytype: "normal", label: "F12", shape: "fn", keycode: 88 },
{ keytype: "normal", label: "PrtSc", shape: "fn", keycode: 99 },
{ keytype: "normal", label: "Del", shape: "fn", keycode: 111 }
],
[
{ keytype: "normal", label: "ё", labelShift: "Ё", shape: "normal", keycode: 41 },
{ keytype: "normal", label: "1", labelShift: "!", shape: "normal", keycode: 2 },
{ keytype: "normal", label: "2", labelShift: "\"", shape: "normal", keycode: 3 },
{ keytype: "normal", label: "3", labelShift: "№", shape: "normal", keycode: 4 },
{ keytype: "normal", label: "4", labelShift: ";", shape: "normal", keycode: 5 },
{ keytype: "normal", label: "5", labelShift: "%", shape: "normal", keycode: 6 },
{ keytype: "normal", label: "6", labelShift: ":", shape: "normal", keycode: 7 },
{ keytype: "normal", label: "7", labelShift: "?", shape: "normal", keycode: 8 },
{ keytype: "normal", label: "8", labelShift: "*", shape: "normal", keycode: 9 },
{ keytype: "normal", label: "9", labelShift: "(", shape: "normal", keycode: 10 },
{ keytype: "normal", label: "0", labelShift: ")", shape: "normal", keycode: 11 },
{ keytype: "normal", label: "-", labelShift: "_", shape: "normal", keycode: 12 },
{ keytype: "normal", label: "=", labelShift: "+", shape: "normal", keycode: 13 },
{ keytype: "normal", label: "Backspace", shape: "expand", keycode: 14 }
],
[
{ keytype: "normal", label: "Tab", shape: "tab", keycode: 15 },
{ keytype: "normal", label: "й", labelShift: "Й", shape: "normal", keycode: 16 },
{ keytype: "normal", label: "ц", labelShift: "Ц", shape: "normal", keycode: 17 },
{ keytype: "normal", label: "у", labelShift: "У", shape: "normal", keycode: 18 },
{ keytype: "normal", label: "к", labelShift: "К", shape: "normal", keycode: 19 },
{ keytype: "normal", label: "е", labelShift: "Е", shape: "normal", keycode: 20 },
{ keytype: "normal", label: "н", labelShift: "Н", shape: "normal", keycode: 21 },
{ keytype: "normal", label: "г", labelShift: "Г", shape: "normal", keycode: 22 },
{ keytype: "normal", label: "ш", labelShift: "Ш", shape: "normal", keycode: 23 },
{ keytype: "normal", label: "щ", labelShift: "Щ", shape: "normal", keycode: 24 },
{ keytype: "normal", label: "з", labelShift: "З", shape: "normal", keycode: 25 },
{ keytype: "normal", label: "х", labelShift: "Х", shape: "normal", keycode: 26 },
{ keytype: "normal", label: "ъ", labelShift: "Ъ", shape: "normal", keycode: 27 },
{ keytype: "normal", label: "\\", labelShift: "/", shape: "expand", keycode: 43 }
],
[
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "spacer", label: "", shape: "empty" },
{ keytype: "normal", label: "ф", labelShift: "Ф", shape: "normal", keycode: 30 },
{ keytype: "normal", label: "ы", labelShift: "Ы", shape: "normal", keycode: 31 },
{ keytype: "normal", label: "в", labelShift: "В", shape: "normal", keycode: 32 },
{ keytype: "normal", label: "а", labelShift: "А", shape: "normal", keycode: 33 },
{ keytype: "normal", label: "п", labelShift: "П", shape: "normal", keycode: 34 },
{ keytype: "normal", label: "р", labelShift: "Р", shape: "normal", keycode: 35 },
{ keytype: "normal", label: "о", labelShift: "О", shape: "normal", keycode: 36 },
{ keytype: "normal", label: "л", labelShift: "Л", shape: "normal", keycode: 37 },
{ keytype: "normal", label: "д", labelShift: "Д", shape: "normal", keycode: 38 },
{ keytype: "normal", label: "ж", labelShift: "Ж", shape: "normal", keycode: 39 },
{ keytype: "normal", label: "э", labelShift: "Э", shape: "normal", keycode: 40 },
{ keytype: "normal", label: "Enter", shape: "expand", keycode: 28 }
],
[
{ keytype: "modkey", label: "Shift", shape: "shift", keycode: 42 },
{ keytype: "normal", label: "я", labelShift: "Я", shape: "normal", keycode: 44 },
{ keytype: "normal", label: "ч", labelShift: "Ч", shape: "normal", keycode: 45 },
{ keytype: "normal", label: "с", labelShift: "С", shape: "normal", keycode: 46 },
{ keytype: "normal", label: "м", labelShift: "М", shape: "normal", keycode: 47 },
{ keytype: "normal", label: "и", labelShift: "И", shape: "normal", keycode: 48 },
{ keytype: "normal", label: "т", labelShift: "Т", shape: "normal", keycode: 49 },
{ keytype: "normal", label: "ь", labelShift: "Ь", shape: "normal", keycode: 50 },
{ keytype: "normal", label: "б", labelShift: "Б", shape: "normal", keycode: 51 },
{ keytype: "normal", label: "ю", labelShift: "Ю", shape: "normal", keycode: 52 },
{ keytype: "normal", label: ".", labelShift: ",", shape: "normal", keycode: 53 },
{ keytype: "modkey", label: "Shift", shape: "expand", keycode: 54 }
],
[
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 29 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 56 },
{ keytype: "normal", label: "Space", shape: "space", keycode: 57 },
{ keytype: "modkey", label: "Alt", shape: "normal", keycode: 100 },
{ keytype: "normal", label: "Menu", shape: "normal", keycode: 139 },
{ keytype: "modkey", label: "Ctrl", shape: "control", keycode: 97 }
]
]
} }
} }
@@ -21,7 +21,7 @@ Scope {
required property var modelData required property var modelData
property string searchingText: "" property string searchingText: ""
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen)
property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor?.id)
screen: modelData screen: modelData
visible: GlobalStates.overviewOpen visible: GlobalStates.overviewOpen
@@ -40,6 +40,8 @@ Scope {
anchors { anchors {
top: true top: true
bottom: true bottom: true
left: !(Config?.options.overview.enable ?? true)
right: !(Config?.options.overview.enable ?? true)
} }
HyprlandFocusGrab { HyprlandFocusGrab {
@@ -84,7 +86,7 @@ Scope {
function setSearchingText(text) { function setSearchingText(text) {
searchWidget.setSearchingText(text); searchWidget.setSearchingText(text);
searchWidget.focusFirstItemIfNeeded(); searchWidget.focusFirstItem();
} }
ColumnLayout { ColumnLayout {
@@ -122,7 +124,7 @@ Scope {
Loader { Loader {
id: overviewLoader id: overviewLoader
active: GlobalStates.overviewOpen active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget { sourceComponent: OverviewWidget {
panelWindow: root panelWindow: root
visible: (root.searchingText == "") visible: (root.searchingText == "")
@@ -20,7 +20,7 @@ Item {
property var windows: HyprlandData.windowList property var windows: HyprlandData.windowList
property var windowByAddress: HyprlandData.windowByAddress property var windowByAddress: HyprlandData.windowByAddress
property var windowAddresses: HyprlandData.addresses property var windowAddresses: HyprlandData.addresses
property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor?.id)
property real scale: Config.options.overview.scale property real scale: Config.options.overview.scale
property color activeBorderColor: Appearance.colors.colSecondary property color activeBorderColor: Appearance.colors.colSecondary
@@ -61,7 +61,7 @@ Item {
radius: Appearance.rounding.screenRounding * root.scale + padding radius: Appearance.rounding.screenRounding * root.scale + padding
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
ColumnLayout { // Workspaces ColumnLayout { // Workspaces
id: workspaceColumnLayout id: workspaceColumnLayout
@@ -149,14 +149,15 @@ Item {
const address = `0x${toplevel.HyprlandToplevel.address}` const address = `0x${toplevel.HyprlandToplevel.address}`
var win = windowByAddress[address] var win = windowByAddress[address]
const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) const inWorkspaceGroup = (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown)
const inMonitor = root.monitor.id === win.monitor return inWorkspaceGroup;
return inWorkspaceGroup && inMonitor;
}) })
} }
} }
delegate: OverviewWindow { delegate: OverviewWindow {
id: window id: window
required property var modelData required property var modelData
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors[monitorId]
property var address: `0x${modelData.HyprlandToplevel.address}` property var address: `0x${modelData.HyprlandToplevel.address}`
windowData: windowByAddress[address] windowData: windowByAddress[address]
toplevel: modelData toplevel: modelData
@@ -164,9 +165,7 @@ Item {
scale: root.scale scale: root.scale
availableWorkspaceWidth: root.workspaceImplicitWidth availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight availableWorkspaceHeight: root.workspaceImplicitHeight
widgetMonitorId: root.monitor.id
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors[monitorId]
property bool atInitPosition: (initX == x && initY == y) property bool atInitPosition: (initX == x && initY == y)
@@ -1,7 +1,6 @@
import qs import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import Qt5Compat.GraphicalEffects import Qt5Compat.GraphicalEffects
import QtQuick import QtQuick
@@ -22,6 +21,7 @@ Item { // Window
property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset property real initY: Math.max((windowData?.at[1] - (monitorData?.y ?? 0) - monitorData?.reserved[1]) * root.scale, 0) + yOffset
property real xOffset: 0 property real xOffset: 0
property real yOffset: 0 property real yOffset: 0
property int widgetMonitorId: 0
property var targetWindowWidth: windowData?.size[0] * scale property var targetWindowWidth: windowData?.size[0] * scale
property var targetWindowHeight: windowData?.size[1] * scale property var targetWindowHeight: windowData?.size[1] * scale
@@ -40,6 +40,7 @@ Item { // Window
y: initY y: initY
width: windowData?.size[0] * root.scale width: windowData?.size[0] * root.scale
height: windowData?.size[1] * root.scale height: windowData?.size[1] * root.scale
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
@@ -69,6 +70,7 @@ Item { // Window
captureSource: GlobalStates.overviewOpen ? root.toplevel : null captureSource: GlobalStates.overviewOpen ? root.toplevel : null
live: true live: true
// Color overlay for interactions
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
radius: Appearance.rounding.windowRounding * root.scale radius: Appearance.rounding.windowRounding * root.scale
@@ -90,7 +90,7 @@ RippleButton {
onClicked: { onClicked: {
root.itemExecute() root.itemExecute()
Hyprland.dispatch("global quickshell:overviewClose") GlobalStates.overviewOpen = false
} }
Keys.onPressed: (event) => { Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
@@ -58,7 +58,7 @@ Item { // Wrapper
{ {
action: "konachanwall", action: "konachanwall",
execute: () => { execute: () => {
Quickshell.execDetached([Quickshell.configPath("scripts/colors/random_konachan_wall.sh")]); Quickshell.execDetached([Quickshell.shellPath("scripts/colors/random_konachan_wall.sh")]);
} }
}, },
{ {
@@ -75,9 +75,8 @@ Item { // Wrapper
}, },
] ]
function focusFirstItemIfNeeded() { function focusFirstItem() {
if (searchInput.focus) appResults.currentIndex = 0;
appResults.currentIndex = 0; // Focus the first item
} }
Timer { Timer {
@@ -99,7 +98,7 @@ Item { // Wrapper
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {
root.mathResult = data; root.mathResult = data;
root.focusFirstItemIfNeeded(); root.focusFirstItem();
} }
} }
} }
@@ -164,7 +163,7 @@ Item { // Wrapper
radius: Appearance.rounding.large radius: Appearance.rounding.large
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
ColumnLayout { ColumnLayout {
id: columnLayout id: columnLayout
@@ -250,7 +249,7 @@ Item { // Wrapper
color: Appearance.colors.colOutlineVariant color: Appearance.colors.colOutlineVariant
} }
ListView { // App results StyledListView { // App results
id: appResults id: appResults
visible: root.showResults visible: root.showResults
Layout.fillWidth: true Layout.fillWidth: true
@@ -261,6 +260,8 @@ Item { // Wrapper
spacing: 2 spacing: 2
KeyNavigation.up: searchBar KeyNavigation.up: searchBar
highlightMoveDuration: 100 highlightMoveDuration: 100
add: null
remove: null
onFocusChanged: { onFocusChanged: {
if (focus) if (focus)
@@ -277,6 +278,9 @@ Item { // Wrapper
model: ScriptModel { model: ScriptModel {
id: model id: model
onValuesChanged: {
root.focusFirstItem();
}
values: { values: {
// Search results are handled here // Search results are handled here
////////////////// Skip? ////////////////// ////////////////// Skip? //////////////////
@@ -405,8 +409,6 @@ Item { // Wrapper
} }
} }
onModelChanged: root.focusFirstItemIfNeeded()
delegate: SearchItem { delegate: SearchItem {
// The selectable item for each search result // The selectable item for each search result
required property var modelData required property var modelData
@@ -13,7 +13,8 @@ Scope {
component CornerPanelWindow: PanelWindow { component CornerPanelWindow: PanelWindow {
id: cornerPanelWindow id: cornerPanelWindow
visible: (Config.options.appearance.fakeScreenRounding === 1 || (Config.options.appearance.fakeScreenRounding === 2 && !activeWindow?.fullscreen)) property bool fullscreen
visible: (Config.options.appearance.fakeScreenRounding === 1 || (Config.options.appearance.fakeScreenRounding === 2 && !fullscreen))
property var corner property var corner
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
@@ -35,7 +36,7 @@ Scope {
implicitHeight: cornerWidget.implicitHeight implicitHeight: cornerWidget.implicitHeight
RoundCorner { RoundCorner {
id: cornerWidget id: cornerWidget
size: Appearance.rounding.screenRounding implicitSize: Appearance.rounding.screenRounding
corner: cornerPanelWindow.corner corner: cornerPanelWindow.corner
} }
} }
@@ -44,22 +45,34 @@ Scope {
model: Quickshell.screens model: Quickshell.screens
Scope { Scope {
id: monitorScope
required property var modelData required property var modelData
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
// Hide when fullscreen
property list<HyprlandWorkspace> workspacesForMonitor: Hyprland.workspaces.values.filter(workspace=>workspace.monitor && workspace.monitor.name == monitor.name)
property var activeWorkspaceWithFullscreen: workspacesForMonitor.filter(workspace=>((workspace.toplevels.values.filter(window=>window.wayland.fullscreen)[0] != undefined) && workspace.active))[0]
property bool fullscreen: activeWorkspaceWithFullscreen != undefined
CornerPanelWindow { CornerPanelWindow {
screen: modelData screen: modelData
corner: RoundCorner.CornerEnum.TopLeft corner: RoundCorner.CornerEnum.TopLeft
fullscreen: monitorScope.fullscreen
} }
CornerPanelWindow { CornerPanelWindow {
screen: modelData screen: modelData
corner: RoundCorner.CornerEnum.TopRight corner: RoundCorner.CornerEnum.TopRight
fullscreen: monitorScope.fullscreen
} }
CornerPanelWindow { CornerPanelWindow {
screen: modelData screen: modelData
corner: RoundCorner.CornerEnum.BottomLeft corner: RoundCorner.CornerEnum.BottomLeft
fullscreen: monitorScope.fullscreen
} }
CornerPanelWindow { CornerPanelWindow {
screen: modelData screen: modelData
corner: RoundCorner.CornerEnum.BottomRight corner: RoundCorner.CornerEnum.BottomRight
fullscreen: monitorScope.fullscreen
} }
} }
} }
+100 -26
View File
@@ -14,6 +14,30 @@ import Quickshell.Hyprland
Scope { Scope {
id: root id: root
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name) property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
property bool packageManagerRunning: false
property bool downloadRunning: false
component DescriptionLabel: Rectangle {
id: descriptionLabel
property string text
property color textColor: Appearance.colors.colOnTooltip
color: Appearance.colors.colTooltip
clip: true
radius: Appearance.rounding.normal
implicitHeight: descriptionLabelText.implicitHeight + 10 * 2
implicitWidth: descriptionLabelText.implicitWidth + 15 * 2
Behavior on implicitWidth {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
StyledText {
id: descriptionLabelText
anchors.centerIn: parent
color: descriptionLabel.textColor
text: descriptionLabel.text
}
}
function closeAllWindows() { function closeAllWindows() {
HyprlandData.windowList.map(w => w.pid).forEach((pid) => { HyprlandData.windowList.map(w => w.pid).forEach((pid) => {
@@ -21,15 +45,44 @@ Scope {
}); });
} }
function detectRunningStuff() {
packageManagerRunning = false;
downloadRunning = false;
detectPackageManagerProc.running = false;
detectPackageManagerProc.running = true;
detectDownloadProc.running = false;
detectDownloadProc.running = true;
}
Process {
id: detectPackageManagerProc
command: ["pidof", "pacman", "yay", "paru", "dnf", "zypper", "apt", "apx", "xbps", "flatpak", "snap", "apk",
"yum", "epsi", "pikman"]
onExited: (exitCode, exitStatus) => {
root.packageManagerRunning = (exitCode === 0);
}
}
Process {
id: detectDownloadProc
command: ["bash", "-c", "pidof curl wget aria2c yt-dlp || ls ~/Downloads | grep -E '\.crdownload$|\.part$'"]
onExited: (exitCode, exitStatus) => {
root.downloadRunning = (exitCode === 0);
}
}
Loader { Loader {
id: sessionLoader id: sessionLoader
active: false active: GlobalStates.sessionOpen
onActiveChanged: {
if (sessionLoader.active) root.detectRunningStuff();
}
Connections { Connections {
target: GlobalStates target: GlobalStates
function onScreenLockedChanged() { function onScreenLockedChanged() {
if (GlobalStates.screenLocked) { if (GlobalStates.screenLocked) {
sessionLoader.active = false; GlobalStates.sessionOpen = false;
} }
} }
} }
@@ -40,9 +93,8 @@ Scope {
property string subtitle property string subtitle
function hide() { function hide() {
sessionLoader.active = false GlobalStates.sessionOpen = false;
} }
exclusionMode: ExclusionMode.Ignore exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:session" WlrLayershell.namespace: "quickshell:session"
@@ -68,6 +120,7 @@ Scope {
} }
ColumnLayout { // Content column ColumnLayout { // Content column
id: contentColumn
anchors.centerIn: parent anchors.centerIn: parent
spacing: 15 spacing: 15
@@ -182,27 +235,39 @@ Scope {
} }
} }
Rectangle { DescriptionLabel {
Layout.alignment: Qt.AlignHCenter Layout.alignment: Qt.AlignHCenter
radius: Appearance.rounding.normal text: sessionRoot.subtitle
implicitHeight: sessionSubtitle.implicitHeight + 10 * 2
implicitWidth: sessionSubtitle.implicitWidth + 15 * 2
color: Appearance.colors.colTooltip
clip: true
Behavior on implicitWidth {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
StyledText {
id: sessionSubtitle
anchors.centerIn: parent
color: Appearance.colors.colOnTooltip
text: sessionRoot.subtitle
}
} }
} }
RowLayout {
anchors {
top: contentColumn.bottom
topMargin: 10
horizontalCenter: contentColumn.horizontalCenter
}
spacing: 10
Loader {
active: root.packageManagerRunning
visible: active
sourceComponent: DescriptionLabel {
text: Translation.tr("Your package manager is running")
textColor: Appearance.m3colors.m3onErrorContainer
color: Appearance.m3colors.m3errorContainer
}
}
Loader {
active: root.downloadRunning
visible: active
sourceComponent: DescriptionLabel {
text: Translation.tr("There might be a download in progress")
textColor: Appearance.m3colors.m3onErrorContainer
color: Appearance.m3colors.m3errorContainer
}
}
}
} }
} }
@@ -210,15 +275,15 @@ Scope {
target: "session" target: "session"
function toggle(): void { function toggle(): void {
sessionLoader.active = !sessionLoader.active; GlobalStates.sessionOpen = !GlobalStates.sessionOpen;
} }
function close(): void { function close(): void {
sessionLoader.active = false; GlobalStates.sessionOpen = false
} }
function open(): void { function open(): void {
sessionLoader.active = true; GlobalStates.sessionOpen = true
} }
} }
@@ -227,7 +292,7 @@ Scope {
description: "Toggles session screen on press" description: "Toggles session screen on press"
onPressed: { onPressed: {
sessionLoader.active = !sessionLoader.active; GlobalStates.sessionOpen = !GlobalStates.sessionOpen;
} }
} }
@@ -236,7 +301,16 @@ Scope {
description: "Opens session screen on press" description: "Opens session screen on press"
onPressed: { onPressed: {
sessionLoader.active = true; GlobalStates.sessionOpen = true
}
}
GlobalShortcut {
name: "sessionClose"
description: "Closes session screen on press"
onPressed: {
GlobalStates.sessionOpen = false
} }
} }
@@ -95,7 +95,7 @@ ContentPage {
} }
ContentSubsection { ContentSubsection {
title: Translation.tr("Appearance") title: Translation.tr("Overall appearance")
ConfigRow { ConfigRow {
uniform: true uniform: true
ConfigSwitch { ConfigSwitch {
@@ -164,8 +164,11 @@ ContentPage {
} }
} }
ConfigSwitch { ConfigSwitch {
opacity: 0 text: Translation.tr("Performance Profile toggle")
enabled: false checked: Config.options.bar.utilButtons.showPerformanceProfileToggle
onCheckedChanged: {
Config.options.bar.utilButtons.showPerformanceProfileToggle = checked;
}
} }
} }
} }
@@ -184,13 +187,20 @@ ContentPage {
} }
} }
ConfigSwitch { ConfigSwitch {
text: Translation.tr('Always show numbers') text: Translation.tr('Tint app icons')
checked: Config.options.bar.workspaces.alwaysShowNumbers checked: Config.options.bar.workspaces.monochromeIcons
onCheckedChanged: { onCheckedChanged: {
Config.options.bar.workspaces.alwaysShowNumbers = checked; Config.options.bar.workspaces.monochromeIcons = checked;
} }
} }
} }
ConfigSwitch {
text: Translation.tr('Always show numbers')
checked: Config.options.bar.workspaces.alwaysShowNumbers
onCheckedChanged: {
Config.options.bar.workspaces.alwaysShowNumbers = checked;
}
}
ConfigSpinBox { ConfigSpinBox {
text: Translation.tr("Workspaces shown") text: Translation.tr("Workspaces shown")
value: Config.options.bar.workspaces.shown value: Config.options.bar.workspaces.shown
@@ -213,6 +223,18 @@ ContentPage {
} }
} }
ContentSubsection {
title: Translation.tr("Tray")
ConfigSwitch {
text: Translation.tr('Tint icons')
checked: Config.options.bar.tray.monochromeIcons
onCheckedChanged: {
Config.options.bar.tray.monochromeIcons = checked;
}
}
}
ContentSubsection { ContentSubsection {
title: Translation.tr("Weather") title: Translation.tr("Weather")
ConfigSwitch { ConfigSwitch {
@@ -304,6 +326,27 @@ ContentPage {
} }
} }
} }
ConfigSwitch {
text: Translation.tr("Tint app icons")
checked: Config.options.dock.monochromeIcons
onCheckedChanged: {
Config.options.dock.monochromeIcons = checked;
}
}
}
ContentSection {
title: Translation.tr("Sidebars")
ConfigSwitch {
text: Translation.tr('Keep right sidebar loaded')
checked: Config.options.sidebar.keepRightSidebarLoaded
onCheckedChanged: {
Config.options.sidebar.keepRightSidebarLoaded = checked;
}
StyledToolTip {
content: Translation.tr("When enabled keeps the content of the right sidebar loaded to reduce the delay when opening,\nat the cost of around 15MB of consistent RAM usage. Delay significance depends on your system's performance.\nUsing a custom kernel like linux-cachyos might help")
}
}
} }
ContentSection { ContentSection {
@@ -322,6 +365,13 @@ ContentPage {
ContentSection { ContentSection {
title: Translation.tr("Overview") title: Translation.tr("Overview")
ConfigSwitch {
text: Translation.tr("Enable")
checked: Config.options.overview.enable
onCheckedChanged: {
Config.options.overview.enable = checked;
}
}
ConfigSpinBox { ConfigSpinBox {
text: Translation.tr("Scale (%)") text: Translation.tr("Scale (%)")
value: Config.options.overview.scale * 100 value: Config.options.overview.scale * 100
@@ -2,9 +2,8 @@ import qs
import qs.services import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import "./aiChat/"
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import qs.modules.common.functions import qs.modules.common.functions
import "./aiChat/"
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
import QtQuick.Layouts import QtQuick.Layouts
@@ -46,6 +45,22 @@ Item {
Ai.setModel(args[0]); Ai.setModel(args[0]);
} }
}, },
{
name: "tool",
description: Translation.tr("Set the tool to use for the model."),
execute: (args) => {
// console.log(args)
if (args.length == 0 || args[0] == "get") {
Ai.addMessage(Translation.tr("Usage: %1tool TOOL_NAME").arg(root.commandPrefix), Ai.interfaceRole);
} else {
const tool = args[0];
const switched = Ai.setTool(tool);
if (switched) {
Ai.addMessage(Translation.tr("Tool set to: %1").arg(tool), Ai.interfaceRole);
}
}
}
},
{ {
name: "prompt", name: "prompt",
description: Translation.tr("Set the system prompt for the model."), description: Translation.tr("Set the system prompt for the model."),
@@ -74,7 +89,7 @@ Item {
execute: (args) => { execute: (args) => {
const joinedArgs = args.join(" ") const joinedArgs = args.join(" ")
if (joinedArgs.trim().length == 0) { if (joinedArgs.trim().length == 0) {
Ai.addMessage(`Usage: ${root.commandPrefix}save CHAT_NAME`, Ai.interfaceRole); Ai.addMessage(Translation.tr("Usage: %1save CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
return; return;
} }
Ai.saveChat(joinedArgs) Ai.saveChat(joinedArgs)
@@ -86,7 +101,7 @@ Item {
execute: (args) => { execute: (args) => {
const joinedArgs = args.join(" ") const joinedArgs = args.join(" ")
if (joinedArgs.trim().length == 0) { if (joinedArgs.trim().length == 0) {
Ai.addMessage(`Usage: ${root.commandPrefix}load CHAT_NAME`, Ai.interfaceRole); Ai.addMessage(Translation.tr("Usage: %1load CHAT_NAME").arg(root.commandPrefix), Ai.interfaceRole);
return; return;
} }
Ai.loadChat(joinedArgs) Ai.loadChat(joinedArgs)
@@ -126,7 +141,7 @@ Mowe uwu wem ipsum!
### Formatting ### Formatting
- *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com) - *Italic*, \`Monospace\`, **Bold**, [Link](https://example.com)
- Arch lincox icon <img src="${Quickshell.configPath("assets/icons/arch-symbolic.svg")}" height="${Appearance.font.pixelSize.small}"/> - Arch lincox icon <img src="${Quickshell.shellPath("assets/icons/arch-symbolic.svg")}" height="${Appearance.font.pixelSize.small}"/>
### Table ### Table
@@ -188,10 +203,76 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
} }
} }
component StatusItem: MouseArea {
id: statusItem
property string icon
property string statusText
property string description
hoverEnabled: true
implicitHeight: statusItemRowLayout.implicitHeight
implicitWidth: statusItemRowLayout.implicitWidth
RowLayout {
id: statusItemRowLayout
spacing: 0
MaterialSymbol {
text: statusItem.icon
iconSize: Appearance.font.pixelSize.huge
color: Appearance.colors.colSubtext
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.small
text: statusItem.statusText
color: Appearance.colors.colSubtext
}
}
StyledToolTip {
content: statusItem.description
extraVisibleCondition: false
alternativeVisibleCondition: statusItem.containsMouse
}
}
component StatusSeparator: Rectangle {
implicitWidth: 4
implicitHeight: 4
radius: implicitWidth / 2
color: Appearance.colors.colOutlineVariant
}
ColumnLayout { ColumnLayout {
id: columnLayout id: columnLayout
anchors.fill: parent anchors.fill: parent
RowLayout { // Status
Layout.alignment: Qt.AlignHCenter
spacing: 10
StatusItem {
icon: Ai.currentModelHasApiKey ? "key" : "key_off"
statusText: ""
description: Ai.currentModelHasApiKey ? Translation.tr("API key is set\nChange with /key YOUR_API_KEY") : Translation.tr("No API key\nSet it with /key YOUR_API_KEY")
}
StatusSeparator {}
StatusItem {
icon: "device_thermostat"
statusText: Ai.temperature.toFixed(1)
description: Translation.tr("Temperature\nChange with /temp VALUE")
}
StatusSeparator {
visible: Ai.tokenCount.total > 0
}
StatusItem {
visible: Ai.tokenCount.total > 0
icon: "token"
statusText: Ai.tokenCount.total
description: Translation.tr("Total token count\nInput: %1\nOutput: %2")
.arg(Ai.tokenCount.input)
.arg(Ai.tokenCount.output)
}
}
Item { // Messages Item { // Messages
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
@@ -201,6 +282,9 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
spacing: 10 spacing: 10
popin: false popin: false
touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4
mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4
property int lastResponseLength: 0 property int lastResponseLength: 0
clip: true clip: true
@@ -215,15 +299,6 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
add: null // Prevent function calls from being janky add: null // Prevent function calls from being janky
Behavior on contentY {
NumberAnimation {
id: scrollAnim
duration: Appearance.animation.scroll.duration
easing.type: Appearance.animation.scroll.type
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
}
}
model: ScriptModel { model: ScriptModel {
values: Ai.messageIDs.filter(id => { values: Ai.messageIDs.filter(id => {
const message = Ai.messageByID[id]; const message = Ai.messageByID[id];
@@ -457,6 +532,25 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
description: Translation.tr(`Load chat from %1`).arg(file.target), description: Translation.tr(`Load chat from %1`).arg(file.target),
} }
}) })
} else if (messageInputField.text.startsWith(`${root.commandPrefix}tool`)) {
root.suggestionQuery = messageInputField.text.split(" ")[1] ?? ""
const toolResults = Fuzzy.go(root.suggestionQuery, Ai.availableTools.map(tool => {
return {
name: Fuzzy.prepare(tool),
obj: tool,
}
}), {
all: true,
key: "name"
})
root.suggestionList = toolResults.map(tool => {
const toolName = tool.target
return {
name: `${messageInputField.text.trim().split(" ").length == 1 ? (root.commandPrefix + "tool ") : ""}${tool.target}`,
displayName: toolName,
description: Ai.toolDescriptions[toolName],
}
})
} else if(messageInputField.text.startsWith(root.commandPrefix)) { } else if(messageInputField.text.startsWith(root.commandPrefix)) {
root.suggestionQuery = messageInputField.text root.suggestionQuery = messageInputField.text
root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => { root.suggestionList = root.allCommands.filter(cmd => cmd.name.startsWith(messageInputField.text.substring(1))).map(cmd => {
@@ -535,60 +629,41 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
anchors.right: parent.right anchors.right: parent.right
anchors.bottom: parent.bottom anchors.bottom: parent.bottom
anchors.bottomMargin: 5 anchors.bottomMargin: 5
anchors.leftMargin: 5 anchors.leftMargin: 10
anchors.rightMargin: 5 anchors.rightMargin: 5
spacing: 5 spacing: 4
property var commandsShown: [ property var commandsShown: [
{ {
name: "model", name: "",
sendDirectly: false, sendDirectly: false,
}, dontAddSpace: true,
},
{ {
name: "clear", name: "clear",
sendDirectly: true, sendDirectly: true,
}, },
] ]
Item { ApiInputBoxIndicator { // Model indicator
implicitHeight: providerRowLayout.implicitHeight + 5 * 2 icon: "api"
implicitWidth: providerRowLayout.implicitWidth + 10 * 2 text: Ai.getModel().name
tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL")
RowLayout { .arg(Ai.getModel().name)
id: providerRowLayout .arg(root.commandPrefix)
anchors.centerIn: parent }
MaterialSymbol { ApiInputBoxIndicator { // Tool indicator
text: "api" icon: "service_toolbox"
iconSize: Appearance.font.pixelSize.large text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1)
} tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL")
StyledText { .arg(Ai.currentTool)
id: providerName .arg(root.commandPrefix)
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3onSurface
elide: Text.ElideRight
text: Ai.getModel().name
}
}
StyledToolTip {
id: toolTip
extraVisibleCondition: false
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
content: Translation.tr("Current model: %1\nSet it with %2model MODEL")
.arg(Ai.getModel().name)
.arg(root.commandPrefix)
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
ButtonGroup { ButtonGroup { // Command buttons
padding: 0 padding: 0
Repeater { // Command buttons Repeater { // Command buttons
@@ -600,7 +675,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
if(modelData.sendDirectly) { if(modelData.sendDirectly) {
root.handleInput(commandRepresentation) root.handleInput(commandRepresentation)
} else { } else {
messageInputField.text = commandRepresentation + " " messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ")
messageInputField.cursorPosition = messageInputField.text.length messageInputField.cursorPosition = messageInputField.text.length
messageInputField.forceActiveFocus() messageInputField.forceActiveFocus()
} }
@@ -3,7 +3,6 @@ import qs.services
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.modules.common.functions import qs.modules.common.functions
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "./anime/" import "./anime/"
import QtQuick import QtQuick
import QtQuick.Controls import QtQuick.Controls
@@ -138,6 +137,9 @@ Item {
anchors.fill: parent anchors.fill: parent
spacing: 10 spacing: 10
touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4
mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4
property int lastResponseLength: 0 property int lastResponseLength: 0
clip: true clip: true
@@ -150,15 +152,6 @@ Item {
} }
} }
Behavior on contentY {
NumberAnimation {
id: scrollAnim
duration: Appearance.animation.scroll.duration
easing.type: Appearance.animation.scroll.type
easing.bezierCurve: Appearance.animation.scroll.bezierCurve
}
}
model: ScriptModel { model: ScriptModel {
values: { values: {
if(root.responses.length > booruResponseListView.lastResponseLength) { if(root.responses.length > booruResponseListView.lastResponseLength) {
@@ -493,40 +486,12 @@ Item {
}, },
] ]
Item { ApiInputBoxIndicator { // Tool indicator
implicitHeight: providerRowLayout.implicitHeight + 5 * 2 icon: "api"
implicitWidth: providerRowLayout.implicitWidth + 10 * 2 text: Booru.providers[Booru.currentProvider].name
tooltipText: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER")
RowLayout { .arg(Booru.providers[Booru.currentProvider].url)
id: providerRowLayout .arg(root.commandPrefix)
anchors.centerIn: parent
MaterialSymbol {
text: "api"
iconSize: Appearance.font.pixelSize.large
}
StyledText {
id: providerName
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.m3colors.m3onSurface
text: Booru.providers[Booru.currentProvider].name
}
}
StyledToolTip {
id: toolTip
extraVisibleCondition: false
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
// content: Translation.tr("The current API used. Endpoint: ") + Booru.providers[Booru.currentProvider].url + Translation.tr("\nSet with /mode PROVIDER")
content: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER")
.arg(Booru.providers[Booru.currentProvider].url)
.arg(root.commandPrefix)
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
} }
StyledText { StyledText {
@@ -1,6 +1,5 @@
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs.services
import QtQuick import QtQuick
GroupButton { GroupButton {
@@ -0,0 +1,47 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
Item { // Model indicator
id: root
property string icon: "api"
property string text: ""
property string tooltipText: ""
implicitHeight: rowLayout.implicitHeight + 4 * 2
implicitWidth: rowLayout.implicitWidth + 4 * 2
RowLayout {
id: rowLayout
anchors.centerIn: parent
MaterialSymbol {
text: root.icon
iconSize: Appearance.font.pixelSize.normal
}
StyledText {
id: providerName
font.pixelSize: Appearance.font.pixelSize.smaller
color: Appearance.m3colors.m3onSurface
elide: Text.ElideRight
text: root.text
}
}
Loader {
active: root.tooltipText?.length > 0
anchors.fill: parent
sourceComponent: MouseArea {
id: mouseArea
hoverEnabled: true
StyledToolTip {
id: toolTip
extraVisibleCondition: false
alternativeVisibleCondition: mouseArea.containsMouse // Show tooltip when hovered
content: root.tooltipText
}
}
}
}
@@ -96,7 +96,7 @@ Scope { // Scope
height: parent.height - Appearance.sizes.hyprlandGapsOut * 2 height: parent.height - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
Behavior on width { Behavior on width {
@@ -263,7 +263,6 @@ Rectangle {
} }
Flow { // Annotations Flow { // Annotations
id: annotationFlowLayout
visible: root.messageData?.annotationSources?.length > 0 visible: root.messageData?.annotationSources?.length > 0
spacing: 5 spacing: 5
Layout.fillWidth: true Layout.fillWidth: true
@@ -274,12 +273,28 @@ Rectangle {
values: root.messageData?.annotationSources || [] values: root.messageData?.annotationSources || []
} }
delegate: AnnotationSourceButton { delegate: AnnotationSourceButton {
id: annotationButton required property var modelData
displayText: modelData.text displayText: modelData.text
url: modelData.url url: modelData.url
} }
} }
}
Flow { // Search queries
visible: root.messageData?.searchQueries?.length > 0
spacing: 5
Layout.fillWidth: true
Layout.alignment: Qt.AlignLeft
Repeater {
model: ScriptModel {
values: root.messageData?.searchQueries || []
}
delegate: SearchQueryButton {
required property var modelData
query: modelData
}
}
} }
} }
@@ -24,7 +24,7 @@ RippleButton {
onClicked: { onClicked: {
if (url) { if (url) {
Qt.openUrlExternally(url) Qt.openUrlExternally(url)
Hyprland.dispatch("global quickshell:sidebarLeftClose") GlobalStates.sidebarLeftOpen = false
} }
} }
@@ -12,12 +12,15 @@ import Quickshell
import org.kde.syntaxhighlighting import org.kde.syntaxhighlighting
ColumnLayout { ColumnLayout {
id: root
// These are needed on the parent loader // These are needed on the parent loader
property bool editing: parent?.editing ?? false property bool editing: parent?.editing ?? false
property bool renderMarkdown: parent?.renderMarkdown ?? true property bool renderMarkdown: parent?.renderMarkdown ?? true
property bool enableMouseSelection: parent?.enableMouseSelection ?? false property bool enableMouseSelection: parent?.enableMouseSelection ?? false
property var segmentContent: parent?.segmentContent ?? ({}) property var segmentContent: parent?.segmentContent ?? ({})
property var segmentLang: parent?.segmentLang ?? "txt" property var segmentLang: parent?.segmentLang ?? "txt"
property bool isCommandRequest: segmentLang === "command"
property var displayLang: (isCommandRequest ? "bash" : segmentLang)
property var messageData: parent?.messageData ?? {} property var messageData: parent?.messageData ?? {}
property real codeBlockBackgroundRounding: Appearance.rounding.small property real codeBlockBackgroundRounding: Appearance.rounding.small
@@ -56,7 +59,7 @@ ColumnLayout {
font.pixelSize: Appearance.font.pixelSize.small font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2 color: Appearance.colors.colOnLayer2
text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain" text: root.displayLang ? Repository.definitionForName(root.displayLang).name : "plain"
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
@@ -123,6 +126,7 @@ ColumnLayout {
Rectangle { // Line numbers Rectangle { // Line numbers
implicitWidth: 40 implicitWidth: 40
implicitHeight: lineNumberColumnLayout.implicitHeight
Layout.fillHeight: true Layout.fillHeight: true
Layout.fillWidth: false Layout.fillWidth: false
topLeftRadius: Appearance.rounding.unsharpen topLeftRadius: Appearance.rounding.unsharpen
@@ -133,10 +137,13 @@ ColumnLayout {
ColumnLayout { ColumnLayout {
id: lineNumberColumnLayout id: lineNumberColumnLayout
anchors.left: parent.left anchors {
anchors.right: parent.right left: parent.left
anchors.rightMargin: 5 right: parent.right
anchors.verticalCenter: parent.verticalCenter rightMargin: 5
top: parent.top
topMargin: 6
}
spacing: 0 spacing: 0
Repeater { Repeater {
@@ -162,82 +169,116 @@ ColumnLayout {
topRightRadius: Appearance.rounding.unsharpen topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: codeBlockBackgroundRounding bottomRightRadius: codeBlockBackgroundRounding
color: Appearance.colors.colLayer2 color: Appearance.colors.colLayer2
implicitHeight: codeTextArea.implicitHeight implicitHeight: codeColumnLayout.implicitHeight
ScrollView { ColumnLayout {
id: codeScrollView id: codeColumnLayout
Layout.fillWidth: true anchors.fill: parent
Layout.fillHeight: true spacing: 0
implicitWidth: parent.width ScrollView {
implicitHeight: codeTextArea.implicitHeight + 1 id: codeScrollView
contentWidth: codeTextArea.width - 1 Layout.fillWidth: true
// contentHeight: codeTextArea.contentHeight // Layout.fillHeight: true
clip: true implicitWidth: parent.width
ScrollBar.vertical.policy: ScrollBar.AlwaysOff implicitHeight: codeTextArea.implicitHeight + 1
contentWidth: codeTextArea.width - 1
ScrollBar.horizontal: ScrollBar { // contentHeight: codeTextArea.contentHeight
anchors.bottom: parent.bottom clip: true
anchors.left: parent.left ScrollBar.vertical.policy: ScrollBar.AlwaysOff
anchors.right: parent.right
padding: 5 ScrollBar.horizontal: ScrollBar {
policy: ScrollBar.AsNeeded anchors.bottom: parent.bottom
opacity: visualSize == 1 ? 0 : 1 anchors.left: parent.left
visible: opacity > 0 anchors.right: parent.right
padding: 5
policy: ScrollBar.AsNeeded
opacity: visualSize == 1 ? 0 : 1
visible: opacity > 0
Behavior on opacity { Behavior on opacity {
NumberAnimation { NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
contentItem: Rectangle {
implicitHeight: 6
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2Active
} }
} }
contentItem: Rectangle { TextArea { // Code
implicitHeight: 6 id: codeTextArea
radius: Appearance.rounding.small Layout.fillWidth: true
color: Appearance.colors.colLayer2Active readOnly: !editing
selectByMouse: enableMouseSelection || editing
renderType: Text.NativeRendering
font.family: Appearance.font.family.monospace
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text
font.pixelSize: Appearance.font.pixelSize.small
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer
selectionColor: Appearance.colors.colSecondaryContainer
// wrapMode: TextEdit.Wrap
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1
text: segmentContent
onTextChanged: {
segmentContent = text
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Tab) {
// Insert 4 spaces at cursor
const cursor = codeTextArea.cursorPosition;
codeTextArea.insert(cursor, " ");
codeTextArea.cursorPosition = cursor + 4;
event.accepted = true;
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
codeTextArea.copy();
event.accepted = true;
}
}
SyntaxHighlighter {
id: highlighter
textEdit: codeTextArea
repository: Repository
definition: Repository.definitionForName(root.displayLang || "plaintext")
theme: Appearance.syntaxHighlightingTheme
}
} }
} }
Loader {
TextArea { // Code active: root.isCommandRequest && root.messageData.functionPending
id: codeTextArea visible: active
Layout.fillWidth: true Layout.fillWidth: true
readOnly: !editing Layout.margins: 6
selectByMouse: enableMouseSelection || editing Layout.topMargin: 0
renderType: Text.NativeRendering sourceComponent: RowLayout {
font.family: Appearance.font.family.monospace Item { Layout.fillWidth: true }
font.hintingPreference: Font.PreferNoHinting // Prevent weird bold text ButtonGroup {
font.pixelSize: Appearance.font.pixelSize.small GroupButton {
selectedTextColor: Appearance.m3colors.m3onSecondaryContainer contentItem: StyledText {
selectionColor: Appearance.colors.colSecondaryContainer text: Translation.tr("Reject")
// wrapMode: TextEdit.Wrap font.pixelSize: Appearance.font.pixelSize.small
color: messageData.thinking ? Appearance.colors.colSubtext : Appearance.colors.colOnLayer1 color: Appearance.colors.colOnLayer2
}
text: segmentContent onClicked: Ai.rejectCommand(root.messageData)
onTextChanged: { }
segmentContent = text GroupButton {
} toggled: true
contentItem: StyledText {
Keys.onPressed: (event) => { text: Translation.tr("Approve")
if (event.key === Qt.Key_Tab) { font.pixelSize: Appearance.font.pixelSize.small
// Insert 4 spaces at cursor color: Appearance.colors.colOnPrimary
const cursor = codeTextArea.cursorPosition; }
codeTextArea.insert(cursor, " "); onClicked: Ai.approveCommand(root.messageData)
codeTextArea.cursorPosition = cursor + 4; }
event.accepted = true;
} else if ((event.key === Qt.Key_C) && event.modifiers == Qt.ControlModifier) {
codeTextArea.copy();
event.accepted = true;
} }
} }
SyntaxHighlighter {
id: highlighter
textEdit: codeTextArea
repository: Repository
definition: Repository.definitionForName(segmentLang || "plaintext")
theme: Appearance.syntaxHighlightingTheme
}
} }
} }
@@ -128,7 +128,7 @@ ColumnLayout {
onLinkActivated: (link) => { onLinkActivated: (link) => {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose") GlobalStates.sidebarLeftOpen = false
} }
MouseArea { // Pointing hand for links MouseArea { // Pointing hand for links
@@ -92,7 +92,7 @@ Item {
id: thinkBlockLanguage id: thinkBlockLanguage
Layout.fillWidth: false Layout.fillWidth: false
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
text: root.completed ? Translation.tr("Chain of Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4)) text: root.completed ? Translation.tr("Thought") : (Translation.tr("Thinking") + ".".repeat(Math.random() * 4))
} }
Item { Layout.fillWidth: true } Item { Layout.fillWidth: true }
RippleButton { // Expand button RippleButton { // Expand button
@@ -0,0 +1,53 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import qs.modules.common.functions
import QtQuick
import QtQuick.Layouts
import Quickshell.Hyprland
RippleButton {
id: root
property string query
implicitHeight: 30
leftPadding: 6
rightPadding: 10
buttonRadius: Appearance.rounding.verysmall
colBackground: Appearance.colors.colSurfaceContainerHighest
colBackgroundHover: Appearance.colors.colSurfaceContainerHighestHover
colRipple: Appearance.colors.colSurfaceContainerHighestActive
PointingHandInteraction {}
onClicked: {
let url = Config.options.search.engineBaseUrl + root.query;
for (let site of (Config?.options?.search.excludedSites ?? [])) {
url += ` -site:${site}`;
}
Qt.openUrlExternally(url);
GlobalStates.sidebarLeftOpen = false;
}
contentItem: Item {
anchors.centerIn: parent
implicitWidth: rowLayout.implicitWidth
implicitHeight: rowLayout.implicitHeight
RowLayout {
id: rowLayout
anchors.centerIn: parent
spacing: 5
MaterialSymbol {
text: "search"
iconSize: 20
color: Appearance.m3colors.m3onSurface
}
StyledText {
id: text
horizontalAlignment: Text.AlignHCenter
text: root.query
color: Appearance.m3colors.m3onSurface
}
}
}
}
@@ -63,13 +63,17 @@ Button {
anchors.fill: parent anchors.fill: parent
width: root.rowHeight * modelData.aspect_ratio width: root.rowHeight * modelData.aspect_ratio
height: root.rowHeight height: root.rowHeight
visible: opacity > 0
opacity: status === Image.Ready ? 1 : 0
fillMode: Image.PreserveAspectFit fillMode: Image.PreserveAspectFit
source: modelData.preview_url source: modelData.preview_url
sourceSize.width: root.rowHeight * modelData.aspect_ratio sourceSize.width: root.rowHeight * modelData.aspect_ratio
sourceSize.height: root.rowHeight sourceSize.height: root.rowHeight
visible: opacity > 0
opacity: status === Image.Ready ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
layer.enabled: true layer.enabled: true
layer.effect: OpacityMask { layer.effect: OpacityMask {
maskSource: Rectangle { maskSource: Rectangle {
@@ -78,10 +82,6 @@ Button {
radius: imageRadius radius: imageRadius
} }
} }
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
} }
RippleButton { RippleButton {
@@ -97,7 +97,7 @@ Rectangle {
} }
} }
Flickable { // Tag strip StyledFlickable { // Tag strip
id: tagsFlickable id: tagsFlickable
visible: root.responseData.tags.length > 0 visible: root.responseData.tags.length > 0
Layout.alignment: Qt.AlignLeft Layout.alignment: Qt.AlignLeft
@@ -158,7 +158,7 @@ Rectangle {
textFormat: Text.MarkdownText textFormat: Text.MarkdownText
onLinkActivated: (link) => { onLinkActivated: (link) => {
Qt.openUrlExternally(link) Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose") GlobalStates.sidebarLeftOpen = false
} }
PointingHandLinkHover {} PointingHandLinkHover {}
} }
@@ -17,7 +17,7 @@ Scope {
id: root id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12 property int sidebarPadding: 12
property string settingsQmlPath: Quickshell.configPath("settings.qml") property string settingsQmlPath: Quickshell.shellPath("settings.qml")
PanelWindow { PanelWindow {
id: sidebarRoot id: sidebarRoot
@@ -51,15 +51,10 @@ Scope {
Loader { Loader {
id: sidebarContentLoader id: sidebarContentLoader
active: GlobalStates.sidebarRightOpen active: GlobalStates.sidebarRightOpen || Config?.options.sidebar.keepRightSidebarLoaded
anchors { anchors {
top: parent.top fill: parent
bottom: parent.bottom margins: Appearance.sizes.hyprlandGapsOut
right: parent.right
left: parent.left
topMargin: Appearance.sizes.hyprlandGapsOut
rightMargin: Appearance.sizes.hyprlandGapsOut
bottomMargin: Appearance.sizes.hyprlandGapsOut
leftMargin: Appearance.sizes.elevationMargin leftMargin: Appearance.sizes.elevationMargin
} }
width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
@@ -87,7 +82,7 @@ Scope {
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2 implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0 color: Appearance.colors.colLayer0
border.width: 1 border.width: 1
border.color: Appearance.m3colors.m3outlineVariant border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1 radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
ColumnLayout { ColumnLayout {
@@ -102,26 +97,19 @@ Scope {
Layout.topMargin: 5 Layout.topMargin: 5
Layout.bottomMargin: 0 Layout.bottomMargin: 0
Item { CustomIcon {
implicitWidth: distroIcon.width id: distroIcon
implicitHeight: distroIcon.height width: 25
CustomIcon { height: 25
id: distroIcon source: SystemInfo.distroIcon
width: 25 colorize: true
height: 25 color: Appearance.colors.colOnLayer0
source: SystemInfo.distroIcon
}
ColorOverlay {
anchors.fill: distroIcon
source: distroIcon
color: Appearance.colors.colOnLayer0
}
} }
StyledText { StyledText {
font.pixelSize: Appearance.font.pixelSize.normal font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0 color: Appearance.colors.colOnLayer0
text: Translation.tr("Uptime: %1").arg(DateTime.uptime) text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText textFormat: Text.MarkdownText
} }
@@ -145,7 +133,7 @@ Scope {
toggled: false toggled: false
buttonIcon: "settings" buttonIcon: "settings"
onClicked: { onClicked: {
Hyprland.dispatch("global quickshell:sidebarRightClose") GlobalStates.sidebarRightOpen = false
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath]) Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
} }
StyledToolTip { StyledToolTip {
@@ -156,7 +144,7 @@ Scope {
toggled: false toggled: false
buttonIcon: "power_settings_new" buttonIcon: "power_settings_new"
onClicked: { onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen") GlobalStates.sessionOpen = true
} }
StyledToolTip { StyledToolTip {
content: Translation.tr("Session") content: Translation.tr("Session")
@@ -16,7 +16,7 @@ QuickToggleButton {
} }
altAction: () => { altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`]) Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
Hyprland.dispatch("global quickshell:sidebarRightClose") GlobalStates.sidebarRightOpen = false
} }
Process { Process {
id: toggleBluetooth id: toggleBluetooth
@@ -22,7 +22,7 @@ QuickToggleButton {
altAction: () => { altAction: () => {
Quickshell.execDetached(["easyeffects"]) Quickshell.execDetached(["easyeffects"])
Hyprland.dispatch("global quickshell:sidebarRightClose") GlobalStates.sidebarRightOpen = false
} }
Process { Process {
@@ -17,7 +17,7 @@ QuickToggleButton {
} }
altAction: () => { altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`]) Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`])
Hyprland.dispatch("global quickshell:sidebarRightClose") GlobalStates.sidebarRightOpen = false
} }
Process { Process {
id: toggleNetwork id: toggleNetwork
@@ -1,41 +1,28 @@
import QtQuick
import qs.modules.common import qs.modules.common
import qs.modules.common.widgets import qs.modules.common.widgets
import qs import qs
import qs.services
import Quickshell.Io import Quickshell.Io
QuickToggleButton { QuickToggleButton {
id: nightLightButton id: nightLightButton
property bool enabled: false property bool enabled: Hyprsunset.active
toggled: enabled toggled: enabled
buttonIcon: "nightlight" buttonIcon: Config.options.light.night.automatic ? "night_sight_auto" : "bedtime"
onClicked: { onClicked: {
nightLightButton.enabled = !nightLightButton.enabled Hyprsunset.toggle()
if (enabled) {
nightLightOn.startDetached()
}
else {
nightLightOff.startDetached()
}
} }
Process {
id: nightLightOn altAction: () => {
command: ["gammastep"] Config.options.light.night.automatic = !Config.options.light.night.automatic
} }
Process {
id: nightLightOff Component.onCompleted: {
command: ["pkill", "gammastep"] Hyprsunset.fetchState()
}
Process {
id: updateNightLightState
running: true
command: ["pidof", "gammastep"]
stdout: SplitParser {
onRead: (data) => { // if not empty then set toggled to true
nightLightButton.enabled = data.length > 0
}
}
} }
StyledToolTip { StyledToolTip {
content: Translation.tr("Night Light") content: Translation.tr("Night Light | Right-click to toggle Auto mode")
} }
} }
@@ -16,7 +16,7 @@ Item {
property int todoListItemPadding: 8 property int todoListItemPadding: 8
property int listBottomPadding: 80 property int listBottomPadding: 80
Flickable { StyledFlickable {
id: flickable id: flickable
anchors.fill: parent anchors.fill: parent
contentHeight: columnLayout.height contentHeight: columnLayout.height
@@ -40,7 +40,7 @@ Item {
Item { Item {
Layout.fillWidth: true Layout.fillWidth: true
Layout.fillHeight: true Layout.fillHeight: true
ListView { StyledListView {
id: listView id: listView
model: root.appPwNodes model: root.appPwNodes
clip: true clip: true
@@ -187,7 +187,7 @@ Item {
Layout.rightMargin: dialogMargins Layout.rightMargin: dialogMargins
} }
Flickable { StyledFlickable {
id: dialogFlickable id: dialogFlickable
Layout.fillWidth: true Layout.fillWidth: true
clip: true clip: true
+1
View File
@@ -1,6 +1,7 @@
//@ pragma UseQApplication //@ pragma UseQApplication
//@ pragma Env QS_NO_RELOAD_POPUP=1 //@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic //@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
// Adjust this to make it smaller or larger // Adjust this to make it smaller or larger
//@ pragma Env QT_SCALE_FACTOR=1 //@ pragma Env QT_SCALE_FACTOR=1
+61
View File
@@ -0,0 +1,61 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1754725699,
"narHash": "sha256-iAcj9T/Y+3DBy2J0N+yF9XQQQ8IEb5swLFzs23CdP88=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "85dbfc7aaf52ecb755f87e577ddbe6dbbdbc1054",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}
@@ -0,0 +1,82 @@
{
description = "A flake that provides a runnable switchwall script.";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
};
outputs = {
self,
nixpkgs,
flake-utils,
}:
flake-utils.lib.eachDefaultSystem (
system: let
pkgs = nixpkgs.legacyPackages.${system};
pythonWithPackages = pkgs.python3.withPackages (ps:
with ps; [
dbus-python
materialyoucolor
pillow
]);
switchwallScript = pkgs.stdenv.mkDerivation {
pname = "switchwall";
version = "1.0";
src = ./.;
buildInputs = [
pkgs.makeWrapper
pkgs.jq
pkgs.imagemagick
pkgs.ffmpeg
pkgs.mpvpaper
pkgs.libnotify
pkgs.hyprpicker
pkgs.matugen
pkgs.bash
pkgs.pipewire
pkgs.dbus
pkgs.kdePackages.plasma-desktop
# pkgs.kdePackages.plasma-framework
pkgs.kdePackages.kdialog
pythonWithPackages
];
nativeBuildInputs = [pkgs.qt6.wrapQtAppsHook];
installPhase = ''
mkdir -p $out/bin
# CHANGE HERE: Copy to 'switchwall' instead of 'switchwall.sh'
cp switchwall.sh $out/bin/switchwall
chmod +x $out/bin/switchwall
# UPDATE HERE: Wrap the new filename
wrapProgram $out/bin/switchwall \
--prefix PATH : "${pkgs.lib.makeBinPath [
pkgs.jq
pkgs.imagemagick
pkgs.ffmpeg
pkgs.mpvpaper
pkgs.kdePackages.kdialog
pkgs.libnotify
pkgs.hyprpicker
pkgs.matugen
pythonWithPackages
pkgs.bash
pkgs.pipewire
pkgs.kdePackages.plasma-desktop
]}"
'';
};
in {
packages.default = switchwallScript;
apps.default = flake-utils.lib.mkApp {
drv = switchwallScript;
};
}
);
}
+6
View File
@@ -0,0 +1,6 @@
#!/usr/bin/env bash
cd "$(dirname "$0")"
# Execute the switchwall.sh script within the Nix environment.
nix develop --command ./switchwall.sh "$@"
@@ -63,9 +63,9 @@ post_process() {
# echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/" # echo "Error: least_busy_region.py script not found in $MATUGEN_DIR/scripts/"
# else # else
# "$MATUGEN_DIR/scripts/least_busy_region.py" \ # "$MATUGEN_DIR/scripts/least_busy_region.py" \
# --screen-width "$screen_width" --screen-height "$screen_height" \ # --screen-width "$screen_width" --screen-height "$screen_height" \
# --width 300 --height 200 \ # --width 300 --height 200 \
# "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json # "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
# fi # fi
} }
@@ -85,18 +85,18 @@ check_and_prompt_upscale() {
fi fi
if [[ "$img_width" -lt "$min_width_desired" || "$img_height" -lt "$min_height_desired" ]]; then if [[ "$img_width" -lt "$min_width_desired" || "$img_height" -lt "$min_height_desired" ]]; then
action=$(notify-send "Upscale?" \ action=$(notify-send "Upscale?" \
"Image resolution (${img_width}x${img_height}) is lower than screen resolution (${min_width_desired}x${min_height_desired})" \ "Image resolution (${img_width}x${img_height}) is lower than screen resolution (${min_width_desired}x${min_height_desired})" \
-A "open_upscayl=Open Upscayl"\ -A "open_upscayl=Open Upscayl"\
-a "Wallpaper switcher") -a "Wallpaper switcher")
if [[ "$action" == "open_upscayl" ]]; then if [[ "$action" == "open_upscayl" ]]; then
if command -v upscayl &>/dev/null; then if command -v upscayl &>/dev/null; then
nohup upscayl > /dev/null 2>&1 & nohup upscayl > /dev/null 2>&1 &
else else
action2=$(notify-send \ action2=$(notify-send \
-a "Wallpaper switcher" \ -a "Wallpaper switcher" \
-c "im.error" \ -c "im.error" \
-A "install_upscayl=Install Upscayl (Arch)" \ -A "install_upscayl=Install Upscayl (Arch)" \
"Install Upscayl?" \ "Install Upscayl?" \
"yay -S upscayl-bin") "yay -S upscayl-bin")
if [[ "$action2" == "install_upscayl" ]]; then if [[ "$action2" == "install_upscayl" ]]; then
kitty -1 yay -S upscayl-bin kitty -1 yay -S upscayl-bin
@@ -110,15 +110,15 @@ check_and_prompt_upscale() {
fi fi
} }
THUMBNAIL_DIR="/tmp/mpvpaper_thumbnails"
CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom" CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom"
RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts" RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts"
RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh" RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh"
VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5" THUMBNAIL_DIR="$RESTORE_SCRIPT_DIR/mpvpaper_thumbnails"
VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5 load-scripts=no"
is_video() { is_video() {
local extension="${1##*.}" local extension="${1##*.}"
[[ "$extension" == "mp4" || "$extension" == "mkv" || "$extension" == "webm" ]] && return 0 || return 1 [[ "$extension" == "mp4" || "$extension" == "webm" || "$extension" == "mkv" || "$extension" == "avi" || "$extension" == "mov" ]] && return 0 || return 1
} }
kill_existing_mpvpaper() { kill_existing_mpvpaper() {
@@ -135,7 +135,7 @@ create_restore_script() {
pkill -f -9 mpvpaper pkill -f -9 mpvpaper
for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do
mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" --mpv-options '--load-scripts=no' & mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" &
sleep 0.1 sleep 0.1
done done
EOF EOF
@@ -158,6 +158,13 @@ set_wallpaper_path() {
fi fi
} }
set_thumbnail_path() {
local path="$1"
if [ -f "$SHELL_CONFIG_FILE" ]; then
jq --arg path "$path" '.background.thumbnailPath = $path' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE"
fi
}
switch() { switch() {
imgpath="$1" imgpath="$1"
mode_flag="$2" mode_flag="$2"
@@ -197,10 +204,10 @@ switch() {
echo "Missing deps: ${missing_deps[*]}" echo "Missing deps: ${missing_deps[*]}"
echo "Arch: sudo pacman -S ${missing_deps[*]}" echo "Arch: sudo pacman -S ${missing_deps[*]}"
action=$(notify-send \ action=$(notify-send \
-a "Wallpaper switcher" \ -a "Wallpaper switcher" \
-c "im.error" \ -c "im.error" \
-A "install_arch=Install (Arch)" \ -A "install_arch=Install (Arch)" \
"Can't switch to video wallpaper" \ "Can't switch to video wallpaper" \
"Missing dependencies: ${missing_deps[*]}") "Missing dependencies: ${missing_deps[*]}")
if [[ "$action" == "install_arch" ]]; then if [[ "$action" == "install_arch" ]]; then
kitty -1 sudo pacman -S "${missing_deps[*]}" kitty -1 sudo pacman -S "${missing_deps[*]}"
@@ -218,7 +225,7 @@ switch() {
local video_path="$imgpath" local video_path="$imgpath"
monitors=$(hyprctl monitors -j | jq -r '.[] | .name') monitors=$(hyprctl monitors -j | jq -r '.[] | .name')
for monitor in $monitors; do for monitor in $monitors; do
mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" --mpv-options '--load-scripts=no' & mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" &
sleep 0.1 sleep 0.1
done done
@@ -226,6 +233,9 @@ switch() {
thumbnail="$THUMBNAIL_DIR/$(basename "$imgpath").jpg" thumbnail="$THUMBNAIL_DIR/$(basename "$imgpath").jpg"
ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null
# Set thumbnail path
set_thumbnail_path "$thumbnail"
if [ -f "$thumbnail" ]; then if [ -f "$thumbnail" ]; then
matugen_args=(image "$thumbnail") matugen_args=(image "$thumbnail")
generate_colors_material_args=(--path "$thumbnail") generate_colors_material_args=(--path "$thumbnail")
+370 -361
View File
@@ -6,25 +6,55 @@ import qs.modules.common
import qs import qs
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
import Quickshell.Wayland
import QtQuick import QtQuick
import "./ai/"
/** /**
* Basic service to handle LLM chats. Supports Google's and OpenAI's API formats. * Basic service to handle LLM chats. Supports Google's and OpenAI's API formats.
* Supports Gemini and OpenAI models.
* Limitations:
* - For now functions only work with Gemini API format
*/ */
Singleton { Singleton {
id: root id: root
property Component aiMessageComponent: AiMessageData {}
property Component aiModelComponent: AiModel {}
property Component geminiApiStrategy: GeminiApiStrategy {}
property Component openaiApiStrategy: OpenAiApiStrategy {}
property Component mistralApiStrategy: MistralApiStrategy {}
readonly property string interfaceRole: "interface" readonly property string interfaceRole: "interface"
readonly property string apiKeyEnvVarName: "API_KEY" readonly property string apiKeyEnvVarName: "API_KEY"
property Component aiMessageComponent: AiMessageData {}
property string systemPrompt: Config.options?.ai?.systemPrompt ?? "" property string systemPrompt: {
let prompt = Config.options?.ai?.systemPrompt ?? "";
for (let key in root.promptSubstitutions) {
// prompt = prompt.replaceAll(key, root.promptSubstitutions[key]);
// QML/JS doesn't support replaceAll, so use split/join
prompt = prompt.split(key).join(root.promptSubstitutions[key]);
}
return prompt;
}
// property var messages: [] // property var messages: []
property var messageIDs: [] property var messageIDs: []
property var messageByID: ({}) property var messageByID: ({})
readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {} readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}
readonly property var apiKeysLoaded: KeyringStorage.loaded readonly property var apiKeysLoaded: KeyringStorage.loaded
readonly property bool currentModelHasApiKey: {
const model = models[currentModelId];
if (!model || !model.requires_key) return true;
if (!apiKeysLoaded) return false;
const key = apiKeys[model.key_id];
return (key?.length > 0);
}
property var postResponseHook property var postResponseHook
property real temperature: Persistent.states?.ai?.temperature ?? 0.5 property real temperature: Persistent.states?.ai?.temperature ?? 0.5
property QtObject tokenCount: QtObject {
property int input: -1
property int output: -1
property int total: -1
}
function idForMessage(message) { function idForMessage(message) {
// Generate a unique ID using timestamp and random value // Generate a unique ID using timestamp and random value
@@ -32,7 +62,7 @@ Singleton {
} }
function safeModelName(modelName) { function safeModelName(modelName) {
return modelName.replace(/:/g, "_").replace(/\./g, "_") return modelName.replace(/:/g, "_").replace(/ /g, "-").replace(/\//g, "-")
} }
property list<var> defaultPrompts: [] property list<var> defaultPrompts: []
@@ -40,6 +70,171 @@ Singleton {
property list<var> promptFiles: [...defaultPrompts, ...userPrompts] property list<var> promptFiles: [...defaultPrompts, ...userPrompts]
property list<var> savedChats: [] property list<var> savedChats: []
property var promptSubstitutions: {
"{DISTRO}": SystemInfo.distroName,
"{DATETIME}": `${DateTime.time}, ${DateTime.collapsedCalendarFormat}`,
"{WINDOWCLASS}": ToplevelManager.activeToplevel?.appId ?? "Unknown",
"{DE}": `${SystemInfo.desktopEnvironment} (${SystemInfo.windowingSystem})`
}
// Gemini: https://ai.google.dev/gemini-api/docs/function-calling
// OpenAI: https://platform.openai.com/docs/guides/function-calling
property string currentTool: Config?.options.ai.tool ?? "search"
property var tools: {
"gemini": {
"functions": [{"functionDeclarations": [
{
"name": "switch_to_search_mode",
"description": "Search the web",
},
{
"name": "get_shell_config",
"description": "Get the desktop shell config file contents",
},
{
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
},
"value": {
"type": "string",
"description": "The value to set, e.g. `true`"
}
},
"required": ["key", "value"]
}
},
{
"name": "run_shell_command",
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run",
},
},
"required": ["command"]
}
},
]}],
"search": [{
"google_search": {}
}],
"none": []
},
"openai": {
"functions": [
{
"name": "switch_to_search_mode",
"description": "Search the web",
},
{
"name": "get_shell_config",
"description": "Get the desktop shell config file contents",
},
{
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
},
"value": {
"type": "string",
"description": "The value to set, e.g. `true`"
}
},
"required": ["key", "value"]
}
},
{
"name": "run_shell_command",
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run",
},
},
"required": ["command"]
}
},
],
"search": [],
"none": [],
},
"mistral": {
"functions": [
{
"type": "function",
"function": {
"name": "get_shell_config",
"description": "Get the desktop shell config file contents",
"parameters": {}
},
},
{
"type": "function",
"function": {
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
},
"value": {
"type": "string",
"description": "The value to set, e.g. `true`"
}
},
"required": ["key", "value"]
}
}
},
{
"type": "function",
"function": {
"name": "run_shell_command",
"description": "Run a shell command in bash and get its output. Use this only for quick commands that don't require user interaction. For commands that require interaction, ask the user to run manually instead.",
"parameters": {
"type": "object",
"properties": {
"command": {
"type": "string",
"description": "The bash command to run",
},
},
"required": ["command"]
}
},
},
],
"search": [],
"none": [],
}
}
property list<var> availableTools: Object.keys(root.tools[models[currentModelId]?.api_format])
property var toolDescriptions: {
"functions": Translation.tr("Commands, edit configs, search.\nTakes an extra turn to switch to search mode if that's needed"),
"search": Translation.tr("Gives the model search capabilities (immediately)"),
"none": Translation.tr("Disable tools")
}
// Model properties: // Model properties:
// - name: Name of the model // - name: Name of the model
// - icon: Icon name of the model // - icon: Icon name of the model
@@ -51,13 +246,12 @@ Singleton {
// - key_get_link: Link to get an API key // - key_get_link: Link to get an API key
// - key_get_description: Description of pricing and how to get an API key // - key_get_description: Description of pricing and how to get an API key
// - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai". // - api_format: The API format of the model. Can be "openai" or "gemini". Default is "openai".
// - tools: List of tools that the model can use. Each tool is an object with the tool name as the key and an empty object as the value.
// - extraParams: Extra parameters to be passed to the model. This is a JSON object. // - extraParams: Extra parameters to be passed to the model. This is a JSON object.
property var models: { property var models: {
"gemini-2.0-flash-search": { "gemini-2.0-flash": aiModelComponent.createObject(this, {
"name": "Gemini 2.0 Flash (Search)", "name": "Gemini 2.0 Flash",
"icon": "google-gemini-symbolic", "icon": "google-gemini-symbolic",
"description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), "description": Translation.tr("Online | Google's model\nFast, can perform searches for up-to-date information"),
"homepage": "https://aistudio.google.com", "homepage": "https://aistudio.google.com",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
"model": "gemini-2.0-flash", "model": "gemini-2.0-flash",
@@ -66,133 +260,60 @@ Singleton {
"key_get_link": "https://aistudio.google.com/app/apikey", "key_get_link": "https://aistudio.google.com/app/apikey",
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
"api_format": "gemini", "api_format": "gemini",
"tools": [ }),
{ "gemini-2.5-flash": aiModelComponent.createObject(this, {
"google_search": {} "name": "Gemini 2.5 Flash",
},
]
},
"gemini-2.0-flash-tools": {
"name": "Gemini 2.0 Flash (Tools)",
"icon": "google-gemini-symbolic", "icon": "google-gemini-symbolic",
"description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), "description": Translation.tr("Online | Google's model\nNewer model that's slower than its predecessor but should deliver higher quality answers"),
"homepage": "https://aistudio.google.com", "homepage": "https://aistudio.google.com",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent",
"model": "gemini-2.0-flash", "model": "gemini-2.5-flash",
"requires_key": true, "requires_key": true,
"key_id": "gemini", "key_id": "gemini",
"key_get_link": "https://aistudio.google.com/app/apikey", "key_get_link": "https://aistudio.google.com/app/apikey",
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
"api_format": "gemini", "api_format": "gemini",
"tools": [ }),
{ "gemini-2.5-flash-pro": aiModelComponent.createObject(this, {
"functionDeclarations": [ "name": "Gemini 2.5 Pro",
{
"name": "switch_to_search_mode",
"description": "Search the web",
},
{
"name": "get_shell_config",
"description": "Get the desktop shell config file contents",
},
{
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
},
"value": {
"type": "string",
"description": "The value to set, e.g. `true`"
}
},
"required": ["key", "value"]
}
},
]
}
]
},
"gemini-2.5-flash-search": {
"name": "Gemini 2.5 Flash (Search)",
"icon": "google-gemini-symbolic", "icon": "google-gemini-symbolic",
"description": Translation.tr("Online | Google's model\nGives up-to-date information with search."), "description": Translation.tr("Online | Google's model\nGoogle's state-of-the-art multipurpose model that excels at coding and complex reasoning tasks."),
"homepage": "https://aistudio.google.com", "homepage": "https://aistudio.google.com",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
"model": "gemini-2.5-flash-preview-05-20", "model": "gemini-2.5-pro",
"requires_key": true, "requires_key": true,
"key_id": "gemini", "key_id": "gemini",
"key_get_link": "https://aistudio.google.com/app/apikey", "key_get_link": "https://aistudio.google.com/app/apikey",
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
"api_format": "gemini", "api_format": "gemini",
"tools": [ }),
{ "gemini-2.5-flash-lite": aiModelComponent.createObject(this, {
"google_search": ({}) "name": "Gemini 2.5 Flash-Lite",
},
]
},
"gemini-2.5-flash-tools": {
"name": "Gemini 2.5 Flash (Tools)",
"icon": "google-gemini-symbolic", "icon": "google-gemini-symbolic",
"description": Translation.tr("Experimental | Online | Google's model\nCan do a little more but doesn't search quickly"), "description": Translation.tr("Online | Google's model\nA Gemini 2.5 Flash model optimized for cost-efficiency and high throughput."),
"homepage": "https://aistudio.google.com", "homepage": "https://aistudio.google.com",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent", "endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent",
"model": "gemini-2.5-flash-preview-05-20", "model": "gemini-2.5-flash-lite",
"requires_key": true, "requires_key": true,
"key_id": "gemini", "key_id": "gemini",
"key_get_link": "https://aistudio.google.com/app/apikey", "key_get_link": "https://aistudio.google.com/app/apikey",
"key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"), "key_get_description": Translation.tr("**Pricing**: free. Data used for training.\n\n**Instructions**: Log into Google account, allow AI Studio to create Google Cloud project or whatever it asks, go back and click Get API key"),
"api_format": "gemini", "api_format": "gemini",
"tools": [ }),
{ "mistral-medium-3": aiModelComponent.createObject(this, {
"functionDeclarations": [ "name": "Mistral Medium 3",
{ "icon": "mistral-symbolic",
"name": "switch_to_search_mode", "description": Translation.tr("Online | %1's model | Delivers fast, responsive and well-formatted answers. Disadvantages: not very eager to do stuff; might make up unknown function calls").arg("Mistral"),
"description": "Search the web", "homepage": "https://mistral.ai/news/mistral-medium-3",
}, "endpoint": "https://api.mistral.ai/v1/chat/completions",
{ "model": "mistral-medium-2505",
"name": "get_shell_config",
"description": "Get the desktop shell config file contents",
},
{
"name": "set_shell_config",
"description": "Set a field in the desktop graphical shell config file. Must only be used after `get_shell_config`.",
"parameters": {
"type": "object",
"properties": {
"key": {
"type": "string",
"description": "The key to set, e.g. `bar.borderless`. MUST NOT BE GUESSED, use `get_shell_config` to see what keys are available before setting.",
},
"value": {
"type": "string",
"description": "The value to set, e.g. `true`"
}
},
"required": ["key", "value"]
}
},
]
}
]
},
"openrouter-llama4-maverick": {
"name": "Llama 4 Maverick",
"icon": "ollama-symbolic",
"description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("Meta"),
"homepage": "https://openrouter.ai/meta-llama/llama-4-maverick:free",
"endpoint": "https://openrouter.ai/api/v1/chat/completions",
"model": "meta-llama/llama-4-maverick:free",
"requires_key": true, "requires_key": true,
"key_id": "openrouter", "key_id": "mistral",
"key_get_link": "https://openrouter.ai/settings/keys", "key_get_link": "https://console.mistral.ai/api-keys",
"key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), "key_get_description": Translation.tr("**Instructions**: Log into Mistral account, go to Keys on the sidebar, click Create new key"),
}, "api_format": "mistral",
"openrouter-deepseek-r1": { }),
"openrouter-deepseek-r1": aiModelComponent.createObject(this, {
"name": "DeepSeek R1", "name": "DeepSeek R1",
"icon": "deepseek-symbolic", "icon": "deepseek-symbolic",
"description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("DeepSeek"), "description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("DeepSeek"),
@@ -203,11 +324,29 @@ Singleton {
"key_id": "openrouter", "key_id": "openrouter",
"key_get_link": "https://openrouter.ai/settings/keys", "key_get_link": "https://openrouter.ai/settings/keys",
"key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"), "key_get_description": Translation.tr("**Pricing**: free. Data use policy varies depending on your OpenRouter account settings.\n\n**Instructions**: Log into OpenRouter account, go to Keys on the topright menu, click Create API Key"),
}, }),
} }
property var modelList: Object.keys(root.models) property var modelList: Object.keys(root.models)
property var currentModelId: Persistent.states?.ai?.model || modelList[0] property var currentModelId: Persistent.states?.ai?.model || modelList[0]
property var apiStrategies: {
"openai": openaiApiStrategy.createObject(this),
"gemini": geminiApiStrategy.createObject(this),
"mistral": mistralApiStrategy.createObject(this),
}
property ApiStrategy currentApiStrategy: apiStrategies[models[currentModelId]?.api_format || "openai"]
Connections {
target: Config
function onReadyChanged() {
if (!Config.ready) return;
(Config?.options.ai?.extraModels ?? []).forEach(model => {
const safeModelName = root.safeModelName(model["model"]);
root.addModel(safeModelName, model)
});
}
}
Component.onCompleted: { Component.onCompleted: {
setModel(currentModelId, false, false); // Do necessary setup for model setModel(currentModelId, false, false); // Do necessary setup for model
} }
@@ -233,6 +372,10 @@ Singleton {
return result; return result;
} }
function addModel(modelName, data) {
root.models[modelName] = aiModelComponent.createObject(this, data);
}
Process { Process {
id: getOllamaModels id: getOllamaModels
running: true running: true
@@ -245,14 +388,15 @@ Singleton {
root.modelList = [...root.modelList, ...dataJson]; root.modelList = [...root.modelList, ...dataJson];
dataJson.forEach(model => { dataJson.forEach(model => {
const safeModelName = root.safeModelName(model); const safeModelName = root.safeModelName(model);
root.models[safeModelName] = { root.addModel(safeModelName, {
"name": guessModelName(model), "name": guessModelName(model),
"icon": guessModelLogo(model), "icon": guessModelLogo(model),
"description": Translation.tr("Local Ollama model | %1").arg(model), "description": Translation.tr("Local Ollama model | %1").arg(model),
"homepage": `https://ollama.com/library/${model}`, "homepage": `https://ollama.com/library/${model}`,
"endpoint": "http://localhost:11434/v1/chat/completions", "endpoint": "http://localhost:11434/v1/chat/completions",
"model": model, "model": model,
} "requires_key": false,
})
}); });
root.modelList = Object.keys(root.models); root.modelList = Object.keys(root.models);
@@ -350,8 +494,8 @@ Singleton {
function addApiKeyAdvice(model) { function addApiKeyAdvice(model) {
root.addMessage( root.addMessage(
Translation.tr('To set an API key, pass it with the command\n\nTo view the key, pass "get" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3') Translation.tr('To set an API key, pass it with the %4 command\n\nTo view the key, pass "get" with the command<br/>\n\n### For %1:\n\n**Link**: %2\n\n%3')
.arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("<i>No further instruction provided</i>")), .arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("<i>No further instruction provided</i>")).arg("/key"),
Ai.interfaceRole Ai.interfaceRole
); );
} }
@@ -387,6 +531,15 @@ Singleton {
if (feedback) root.addMessage(Translation.tr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```" if (feedback) root.addMessage(Translation.tr("Invalid model. Supported: \n```\n") + modelList.join("\n```\n```\n"), Ai.interfaceRole) + "\n```"
} }
} }
function setTool(tool) {
if (!root.tools[models[currentModelId]?.api_format] || !(tool in root.tools[models[currentModelId]?.api_format])) {
root.addMessage(Translation.tr("Invalid tool. Supported tools:\n- %1").arg(root.availableTools.join("\n- ")), root.interfaceRole);
return false;
}
Config.options.ai.tool = tool;
return true;
}
function getTemperature() { function getTemperature() {
return root.temperature; return root.temperature;
@@ -438,24 +591,16 @@ Singleton {
function clearMessages() { function clearMessages() {
root.messageIDs = []; root.messageIDs = [];
root.messageByID = ({}); root.messageByID = ({});
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
} }
Process { Process {
id: requester id: requester
property var baseCommand: ["bash", "-c"] property list<string> baseCommand: ["bash", "-c"]
property var message property AiMessageData message
property bool isReasoning property ApiStrategy currentStrategy
property string apiFormat: "openai"
property string geminiBuffer: ""
function buildGeminiEndpoint(model) {
// console.log("ENDPOINT: " + model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`)
return model.endpoint + `?key=\$\{${root.apiKeyEnvVarName}\}`;
}
function buildOpenAIEndpoint(model) {
return model.endpoint;
}
function markDone() { function markDone() {
requester.message.done = true; requester.message.done = true;
@@ -466,82 +611,20 @@ Singleton {
root.saveChat("lastSession") root.saveChat("lastSession")
} }
function buildGeminiRequestData(model, messages) {
let baseData = {
"contents": messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
const geminiApiRoleName = (message.role === "assistant") ? "model" : message.role;
const usingSearch = model.tools[0].google_search != undefined
if (!usingSearch && message.functionCall != undefined && message.functionCall.length > 0) {
return {
"role": geminiApiRoleName,
"parts": [{
functionCall: {
"name": message.functionName,
}
}]
}
}
if (!usingSearch && message.functionResponse != undefined && message.functionResponse.length > 0) {
return {
"role": geminiApiRoleName,
"parts": [{
functionResponse: {
"name": message.functionName,
"response": { "content": message.functionResponse }
}
}]
}
}
return {
"role": geminiApiRoleName,
"parts": [{
text: message.rawContent,
}]
}
}),
"tools": [
...model.tools,
],
"system_instruction": {
"parts": [{ text: root.systemPrompt }]
},
"generationConfig": {
// "temperature": root.temperature,
},
};
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
}
function buildOpenAIRequestData(model, messages) {
let baseData = {
"model": model.model,
"messages": [
{role: "system", content: root.systemPrompt},
...messages.filter(message => (message.role != Ai.interfaceRole)).map(message => {
return {
"role": message.role,
"content": message.rawContent,
}
}),
],
"stream": true,
// "temperature": root.temperature,
};
return model.extraParams ? Object.assign({}, baseData, model.extraParams) : baseData;
}
function makeRequest() { function makeRequest() {
const model = models[currentModelId]; const model = models[currentModelId];
requester.apiFormat = model.api_format ?? "openai"; requester.currentStrategy = root.currentApiStrategy;
requester.currentStrategy.reset(); // Reset strategy state
/* Put API key in environment variable */ /* Put API key in environment variable */
if (model.requires_key) requester.environment[`${root.apiKeyEnvVarName}`] = root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : "" if (model.requires_key) requester.environment[`${root.apiKeyEnvVarName}`] = root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : ""
/* Build endpoint, request data */ /* Build endpoint, request data */
const endpoint = (apiFormat === "gemini") ? buildGeminiEndpoint(model) : buildOpenAIEndpoint(model); const endpoint = root.currentApiStrategy.buildEndpoint(model);
const messageArray = root.messageIDs.map(id => root.messageByID[id]); const messageArray = root.messageIDs.map(id => root.messageByID[id]);
const data = (apiFormat === "gemini") ? buildGeminiRequestData(model, messageArray) : buildOpenAIRequestData(model, messageArray); const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole);
// console.log("REQUEST DATA: ", JSON.stringify(data, null, 2)); const data = root.currentApiStrategy.buildRequestData(model, filteredMessageArray, root.systemPrompt, root.temperature, root.tools[model.api_format][root.currentTool]);
// console.log("[Ai] Request data: ", JSON.stringify(data, null, 2));
let requestHeaders = { let requestHeaders = {
"Content-Type": "application/json", "Content-Type": "application/json",
@@ -569,153 +652,46 @@ Singleton {
// console.log("Request headers: ", JSON.stringify(requestHeaders)); // console.log("Request headers: ", JSON.stringify(requestHeaders));
// console.log("Header string: ", headerString); // console.log("Header string: ", headerString);
/* Get authorization header from strategy */
const authHeader = requester.currentStrategy.buildAuthorizationHeader(root.apiKeyEnvVarName);
/* Create command string */ /* Create command string */
const requestCommandString = `curl --no-buffer "${endpoint}"` const requestCommandString = `curl --no-buffer "${endpoint}"`
+ ` ${headerString}` + ` ${headerString}`
+ ((apiFormat == "gemini") ? "" : ` -H "Authorization: Bearer \$\{${root.apiKeyEnvVarName}\}"`) + (authHeader ? ` ${authHeader}` : "")
+ ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'` + ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
// console.log("Request command: ", requestCommandString);
/* Send the request */
requester.command = baseCommand.concat([requestCommandString]); requester.command = baseCommand.concat([requestCommandString]);
/* Reset vars and make the request */
requester.isReasoning = false
requester.running = true requester.running = true
} }
function parseGeminiBuffer() {
// console.log("BUFFER DATA: ", requester.geminiBuffer);
try {
if (requester.geminiBuffer.length === 0) return;
const dataJson = JSON.parse(requester.geminiBuffer);
if (!dataJson.candidates) return;
if (dataJson.candidates[0]?.finishReason) {
requester.markDone();
}
// Function call handling
if (dataJson.candidates[0]?.content?.parts[0]?.functionCall) {
const functionCall = dataJson.candidates[0]?.content?.parts[0]?.functionCall;
requester.message.functionName = functionCall.name;
requester.message.functionCall = functionCall.name;
const newContent = `\n\n[[ Function: ${functionCall.name}(${JSON.stringify(functionCall.args, null, 2)}) ]]\n`
requester.message.rawContent += newContent;
requester.message.content += newContent;
root.handleGeminiFunctionCall(functionCall.name, functionCall.args);
return
}
// Normal text response
const responseContent = dataJson.candidates[0]?.content?.parts[0]?.text
requester.message.rawContent += responseContent;
requester.message.content += responseContent;
const annotationSources = dataJson.candidates[0]?.groundingMetadata?.groundingChunks?.map(chunk => {
return {
"type": "url_citation",
"text": chunk?.web?.title,
"url": chunk?.web?.uri,
}
}) ?? [];
const annotations = dataJson.candidates[0]?.groundingMetadata?.groundingSupports?.map(citation => {
return {
"type": "url_citation",
"start_index": citation.segment?.startIndex,
"end_index": citation.segment?.endIndex,
"text": citation?.segment.text,
"url": annotationSources[citation.groundingChunkIndices[0]]?.url,
"sources": citation.groundingChunkIndices
}
});
requester.message.annotationSources = annotationSources;
requester.message.annotations = annotations;
// console.log(JSON.stringify(requester.message, null, 2));
} catch (e) {
console.log("[AI] Gemini: Could not parse buffer: ", e);
requester.message.rawContent += requester.geminiBuffer;
requester.message.content += requester.geminiBuffer
} finally {
requester.geminiBuffer = "";
}
}
function handleGeminiResponseLine(line) {
if (line.startsWith("[")) {
requester.geminiBuffer += line.slice(1).trim();
} else if (line == "]") {
requester.geminiBuffer += line.slice(0, -1).trim();
parseGeminiBuffer();
} else if (line.startsWith(",")) { // end of one entry
parseGeminiBuffer();
} else {
requester.geminiBuffer += line.trim();
}
}
function handleOpenAIResponseLine(line) {
// Remove 'data: ' prefix if present and trim whitespace
let cleanData = line.trim();
if (cleanData.startsWith("data:")) {
cleanData = cleanData.slice(5).trim();
}
// console.log("Clean data: ", cleanData);
if (!cleanData || cleanData.startsWith(":")) return;
if (cleanData === "[DONE]") {
requester.markDone();
return;
}
const dataJson = JSON.parse(cleanData);
let newContent = "";
const responseContent = dataJson.choices[0]?.delta?.content || dataJson.message?.content;
const responseReasoning = dataJson.choices[0]?.delta?.reasoning || dataJson.choices[0]?.delta?.reasoning_content;
if (responseContent && responseContent.length > 0) {
if (requester.isReasoning) {
requester.isReasoning = false;
const endBlock = "\n\n</think>\n\n";
requester.message.content += endBlock;
requester.message.rawContent += endBlock;
}
newContent = dataJson.choices[0]?.delta?.content || dataJson.message.content;
} else if (responseReasoning && responseReasoning.length > 0) {
// console.log("Reasoning content: ", dataJson.choices[0].delta.reasoning);
if (!requester.isReasoning) {
requester.isReasoning = true;
const startBlock = "\n\n<think>\n\n";
requester.message.rawContent += startBlock;
requester.message.content += startBlock;
}
newContent = dataJson.choices[0].delta.reasoning || dataJson.choices[0].delta.reasoning_content;
}
requester.message.content += newContent;
if (dataJson.done) {
requester.markDone();
}
}
stdout: SplitParser { stdout: SplitParser {
onRead: data => { onRead: data => {
// console.log("RAW DATA: ", data);
if (data.length === 0) return; if (data.length === 0) return;
if (requester.message.thinking) requester.message.thinking = false;
// console.log("[Ai] Raw response line: ", data);
// Handle response line // Handle response line
if (requester.message.thinking) requester.message.thinking = false;
try { try {
if (requester.apiFormat === "gemini") { const result = requester.currentStrategy.parseResponseLine(data, requester.message);
requester.handleGeminiResponseLine(data); // console.log("[Ai] Parsed response result: ", JSON.stringify(result, null, 2));
if (result.functionCall) {
requester.message.functionCall = result.functionCall;
root.handleFunctionCall(result.functionCall.name, result.functionCall.args, requester.message);
} }
else if (requester.apiFormat === "openai") { if (result.tokenUsage) {
requester.handleOpenAIResponseLine(data); root.tokenCount.input = result.tokenUsage.input;
root.tokenCount.output = result.tokenUsage.output;
root.tokenCount.total = result.tokenUsage.total;
} }
else { if (result.finished) {
console.log("Unknown API format: ", requester.apiFormat); requester.markDone();
requester.message.rawContent += data;
requester.message.content += data;
} }
} catch (e) { } catch (e) {
console.log("[AI] Could not parse response from stream: ", e); console.log("[AI] Could not parse response: ", e);
requester.message.rawContent += data; requester.message.rawContent += data;
requester.message.content += data; requester.message.content += data;
} }
@@ -723,18 +699,15 @@ Singleton {
} }
onExited: (exitCode, exitStatus) => { onExited: (exitCode, exitStatus) => {
if (requester.apiFormat == "gemini") requester.parseGeminiBuffer(); const result = requester.currentStrategy.onRequestFinished(requester.message);
else requester.markDone();
if (result.finished) {
try { // to parse full response into json for error handling requester.markDone();
// console.log("Full response: ", requester.message.content + "]"); } else if (!requester.message.done) {
const parsedResponse = JSON.parse(requester.message.rawContent + "]"); requester.markDone();
requester.message.rawContent = `\`\`\`json\n${JSON.stringify(parsedResponse, null, 2)}\n\`\`\``;
requester.message.content = requester.message.rawContent;
} catch (e) {
// console.log("[AI] Could not parse response on exit: ", e);
} }
// Handle error responses
if (requester.message.content.includes("API key not valid")) { if (requester.message.content.includes("API key not valid")) {
root.addApiKeyAdvice(models[requester.message.model]); root.addApiKeyAdvice(models[requester.message.model]);
} }
@@ -747,45 +720,72 @@ Singleton {
requester.makeRequest(); requester.makeRequest();
} }
function addFunctionOutputMessage(name, output) { function createFunctionOutputMessage(name, output, includeOutputInChat = true) {
const aiMessage = aiMessageComponent.createObject(root, { return aiMessageComponent.createObject(root, {
"role": "user", "role": "user",
"content": `[[ Output of ${name} ]]`, "content": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
"rawContent": `[[ Output of ${name} ]]`, "rawContent": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
"functionName": name, "functionName": name,
"functionResponse": output, "functionResponse": output,
"thinking": false, "thinking": false,
"done": true, "done": true,
"visibleToUser": false, // "visibleToUser": false,
}); });
// console.log("Adding function output message: ", JSON.stringify(aiMessage)); }
function addFunctionOutputMessage(name, output) {
const aiMessage = createFunctionOutputMessage(name, output);
const id = idForMessage(aiMessage); const id = idForMessage(aiMessage);
root.messageIDs = [...root.messageIDs, id]; root.messageIDs = [...root.messageIDs, id];
root.messageByID[id] = aiMessage; root.messageByID[id] = aiMessage;
} }
function buildGeminiFunctionOutput(name, output) { function rejectCommand(message: AiMessageData) {
const functionResponsePart = { if (!message.functionPending) return;
"name": name, message.functionPending = false; // User decided, no more "thinking"
"response": { "content": output } addFunctionOutputMessage(message.functionName, Translation.tr("Command rejected by user"))
}
function approveCommand(message: AiMessageData) {
if (!message.functionPending) return;
message.functionPending = false; // User decided, no more "thinking"
const responseMessage = createFunctionOutputMessage(message.functionName, "", false);
const id = idForMessage(responseMessage);
root.messageIDs = [...root.messageIDs, id];
root.messageByID[id] = responseMessage;
commandExecutionProc.message = responseMessage;
commandExecutionProc.baseMessageContent = responseMessage.content;
commandExecutionProc.shellCommand = message.functionCall.args.command;
commandExecutionProc.running = true; // Start the command execution
}
Process {
id: commandExecutionProc
property string shellCommand: ""
property AiMessageData message
property string baseMessageContent: ""
command: ["bash", "-c", shellCommand]
stdout: SplitParser {
onRead: (output) => {
commandExecutionProc.message.functionResponse += output + "\n\n";
const updatedContent = commandExecutionProc.baseMessageContent + `\n\n<think>\n<tt>${commandExecutionProc.message.functionResponse}</tt>\n</think>`;
commandExecutionProc.message.rawContent = updatedContent;
commandExecutionProc.message.content = updatedContent;
}
} }
return { onExited: (exitCode, exitStatus) => {
"role": "user", commandExecutionProc.message.functionResponse += `[[ Command exited with code ${exitCode} (${exitStatus}) ]]\n`;
"parts": [{ requester.makeRequest(); // Continue
functionResponse: functionResponsePart,
}]
} }
} }
function handleGeminiFunctionCall(name, args) { function handleFunctionCall(name, args: var, message: AiMessageData) {
if (name === "switch_to_search_mode") { if (name === "switch_to_search_mode") {
if (root.currentModelId === "gemini-2.5-flash-tools") { const modelId = root.currentModelId;
root.setModel("gemini-2.5-flash-search", false); root.currentTool = "search"
root.postResponseHook = () => root.setModel("gemini-2.5-flash-tools", false); root.postResponseHook = () => { root.currentTool = "functions" }
} else if (root.currentModelId === "gemini-2.0-flash-tools") {
root.setModel("gemini-2.0-flash-search", false);
root.postResponseHook = () => root.setModel("gemini-2.0-flash-tools", false);
}
addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request.")) addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request."))
requester.makeRequest(); requester.makeRequest();
} else if (name === "get_shell_config") { } else if (name === "get_shell_config") {
@@ -800,6 +800,15 @@ Singleton {
const key = args.key; const key = args.key;
const value = args.value; const value = args.value;
Config.setNestedValue(key, value); Config.setNestedValue(key, value);
} else if (name === "run_shell_command") {
if (!args.command || args.command.length === 0) {
addFunctionOutputMessage(name, Translation.tr("Invalid arguments. Must provide `command`."));
return;
}
const contentToAppend = `\n\n**Command execution request**\n\n\`\`\`command\n${args.command}\n\`\`\``;
message.rawContent += contentToAppend;
message.content += contentToAppend;
message.functionPending = true; // Use thinking to indicate the command is waiting for approval
} }
else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant"); else root.addMessage(Translation.tr("Unknown function call: %1").arg(name), "assistant");
} }
+1 -2
View File
@@ -1,8 +1,7 @@
pragma Singleton pragma Singleton
import qs.modules.common import qs.modules.common
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy import qs.modules.common.functions
import "root:/modules/common/functions/levendist.js" as Levendist
import Quickshell import Quickshell
/** /**
@@ -1,7 +1,7 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
// From https://github.com/caelestia-dots/shell/ (`quickshell` branch) with modifications. // From https://github.com/caelestia-dots/shell with modifications.
// License: GPLv3 // License: GPLv3
import Quickshell import Quickshell
@@ -1,8 +1,6 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "root:/modules/common/functions/levendist.js" as Levendist
import qs.modules.common import qs.modules.common
import qs.modules.common.functions import qs.modules.common.functions
import QtQuick import QtQuick
+4 -5
View File
@@ -9,16 +9,15 @@ pragma ComponentBehavior: Bound
* A nice wrapper for date and time strings. * A nice wrapper for date and time strings.
*/ */
Singleton { Singleton {
property var clock: SystemClock {
id: clock
precision: SystemClock.Minutes
}
property string time: Qt.locale().toString(clock.date, Config.options?.time.format ?? "hh:mm") property string time: Qt.locale().toString(clock.date, Config.options?.time.format ?? "hh:mm")
property string date: Qt.locale().toString(clock.date, Config.options?.time.dateFormat ?? "dddd, dd/MM") property string date: Qt.locale().toString(clock.date, Config.options?.time.dateFormat ?? "dddd, dd/MM")
property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dd MMMM yyyy") property string collapsedCalendarFormat: Qt.locale().toString(clock.date, "dd MMMM yyyy")
property string uptime: "0h, 0m" property string uptime: "0h, 0m"
SystemClock {
id: clock
precision: SystemClock.Minutes
}
Timer { Timer {
interval: 10 interval: 10
running: true running: true
+1 -2
View File
@@ -1,9 +1,8 @@
pragma Singleton pragma Singleton
pragma ComponentBehavior: Bound pragma ComponentBehavior: Bound
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "root:/modules/common/functions/levendist.js" as Levendist
import qs.modules.common import qs.modules.common
import qs.modules.common.functions
import QtQuick import QtQuick
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@@ -12,7 +12,7 @@ Singleton {
property string firstRunNotifSummary: "Welcome!" property string firstRunNotifSummary: "Welcome!"
property string firstRunNotifBody: "Hit Super+/ for a list of keybinds" property string firstRunNotifBody: "Hit Super+/ for a list of keybinds"
property string defaultWallpaperPath: FileUtils.trimFileProtocol(`${Directories.assetsPath}/images/default_wallpaper.png`) property string defaultWallpaperPath: FileUtils.trimFileProtocol(`${Directories.assetsPath}/images/default_wallpaper.png`)
property string welcomeQmlPath: FileUtils.trimFileProtocol(Quickshell.configPath("welcome.qml")) property string welcomeQmlPath: FileUtils.trimFileProtocol(Quickshell.shellPath("welcome.qml"))
function load() { function load() {
firstRunFileView.reload() firstRunFileView.reload()
+25 -20
View File
@@ -69,10 +69,11 @@ Singleton {
Process { Process {
id: getClients id: getClients
command: ["bash", "-c", "hyprctl clients -j | jq -c"] command: ["bash", "-c", "hyprctl clients -j"]
stdout: SplitParser { stdout: StdioCollector {
onRead: data => { id: clientsCollector
root.windowList = JSON.parse(data); onStreamFinished: {
root.windowList = JSON.parse(clientsCollector.text)
let tempWinByAddress = {}; let tempWinByAddress = {};
for (var i = 0; i < root.windowList.length; ++i) { for (var i = 0; i < root.windowList.length; ++i) {
var win = root.windowList[i]; var win = root.windowList[i];
@@ -86,30 +87,33 @@ Singleton {
Process { Process {
id: getMonitors id: getMonitors
command: ["bash", "-c", "hyprctl monitors -j | jq -c"] command: ["bash", "-c", "hyprctl monitors -j"]
stdout: SplitParser { stdout: StdioCollector {
onRead: data => { id: monitorsCollector
root.monitors = JSON.parse(data); onStreamFinished: {
root.monitors = JSON.parse(monitorsCollector.text);
} }
} }
} }
Process { Process {
id: getLayers id: getLayers
command: ["bash", "-c", "hyprctl layers -j | jq -c"] command: ["bash", "-c", "hyprctl layers -j"]
stdout: SplitParser { stdout: StdioCollector {
onRead: data => { id: layersCollector
root.layers = JSON.parse(data); onStreamFinished: {
root.layers = JSON.parse(layersCollector.text);
} }
} }
} }
Process { Process {
id: getWorkspaces id: getWorkspaces
command: ["bash", "-c", "hyprctl workspaces -j | jq -c"] command: ["bash", "-c", "hyprctl workspaces -j"]
stdout: SplitParser { stdout: StdioCollector {
onRead: data => { id: workspacesCollector
root.workspaces = JSON.parse(data); onStreamFinished: {
root.workspaces = JSON.parse(workspacesCollector.text);
let tempWorkspaceById = {}; let tempWorkspaceById = {};
for (var i = 0; i < root.workspaces.length; ++i) { for (var i = 0; i < root.workspaces.length; ++i) {
var ws = root.workspaces[i]; var ws = root.workspaces[i];
@@ -123,10 +127,11 @@ Singleton {
Process { Process {
id: getActiveWorkspace id: getActiveWorkspace
command: ["bash", "-c", "hyprctl activeworkspace -j | jq -c"] command: ["bash", "-c", "hyprctl activeworkspace -j"]
stdout: SplitParser { stdout: StdioCollector {
onRead: data => { id: activeWorkspaceCollector
root.activeWorkspace = JSON.parse(data); onStreamFinished: {
root.activeWorkspace = JSON.parse(activeWorkspaceCollector.text);
} }
} }
} }
@@ -0,0 +1,108 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Hyprland
import qs.modules.common
/**
* Exposes the active Hyprland Xkb keyboard layout name and code for indicators.
*/
Singleton {
id: root
// You can read these
property list<string> layoutCodes: []
property var cachedLayoutCodes: ({})
property string currentLayoutName: ""
property string currentLayoutCode: ""
// For the service
property var baseLayoutFilePath: "/usr/share/X11/xkb/rules/base.lst"
property bool needsLayoutRefresh: false
// Update the layout code according to the layout name (Hyprland gives the name not the code)
onCurrentLayoutNameChanged: root.updateLayoutCode()
function updateLayoutCode() {
if (cachedLayoutCodes.hasOwnProperty(currentLayoutName)) {
root.currentLayoutCode = cachedLayoutCodes[currentLayoutName];
} else {
getLayoutProc.running = true;
}
}
// Get the layout code from the base.lst file by grabbing the line with the current layout name
Process {
id: getLayoutProc
command: ["cat", root.baseLayoutFilePath]
stdout: StdioCollector {
id: layoutCollector
onStreamFinished: {
const lines = layoutCollector.text.split("\n");
const targetDescription = root.currentLayoutName;
const foundLine = lines.find(line => {
// Skip comment lines and empty lines
if (!line.trim() || line.trim().startsWith('!'))
return false;
// Match: key + whitespace + description
const match = line.match(/^\s*(\S+)\s+(.+)$/);
if (match && match[2] === targetDescription) {
root.cachedLayoutCodes[match[2]] = match[1];
root.currentLayoutCode = match[1];
return true;
}
});
// console.log("[HyprlandXkb] Found line:", foundLine);
// console.log("[HyprlandXkb] Layout:", root.currentLayoutName, "| Code:", root.currentLayoutCode);
// console.log("[HyprlandXkb] Cached layout codes:", JSON.stringify(root.cachedLayoutCodes, null, 2));
}
}
}
// Find out available layouts and current active layout. Should only be necessary on init
Process {
id: fetchLayoutsProc
running: true
command: ["hyprctl", "-j", "devices"]
stdout: StdioCollector {
id: devicesCollector
onStreamFinished: {
const parsedOutput = JSON.parse(devicesCollector.text);
const hyprlandKeyboard = parsedOutput["keyboards"].find(kb => kb.main === true);
root.layoutCodes = hyprlandKeyboard["layout"].split(",");
root.currentLayoutName = hyprlandKeyboard["active_keymap"];
// console.log("[HyprlandXkb] Fetched | Layouts (multiple: " + (root.layouts.length > 1) + "): "
// + root.layouts.join(", ") + " | Active: " + root.currentLayoutName);
}
}
}
// Update the layout name when it changes
Connections {
target: Hyprland
function onRawEvent(event) {
if (event.name === "activelayout") {
if (root.needsLayoutRefresh) {
root.needsLayoutRefresh = false;
fetchLayoutsProc.running = true;
}
// If there's only one layout, the updated layout is always the same
if (root.layoutCodes.length <= 1) return;
// Update when layout might have changed
const dataString = event.data;
root.currentLayoutName = dataString.split(",")[1];
// Update layout for on-screen keyboard (osk)
Config.options.osk.layout = root.currentLayoutName;
} else if (event.name == "configreloaded") {
// Mark layout code list to be updated when config is reloaded
root.needsLayoutRefresh = true;
}
}
}
}
@@ -0,0 +1,117 @@
pragma Singleton
import QtQuick
import qs.modules.common
import Quickshell
import Quickshell.Io
/**
* Simple hyprsunset service with automatic mode.
* In theory we don't need this because hyprsunset has a config file, but it somehow doesn't work.
* It should also be possible to control it via hyprctl, but it doesn't work consistently either so we're just killing and launching.
*/
Singleton {
id: root
property var manualActive
property string from: Config.options?.light?.night?.from ?? "19:00" // Default to 7 PM
property string to: Config.options?.light?.night?.to ?? "06:30" // Default to 6:30 AM
property bool automatic: Config.options?.light?.night?.automatic && (Config?.ready ?? true)
property int colorTemperature: Config.options?.light?.night?.colorTemperature ?? 5000 // Default color temperature
property bool shouldBeOn
property bool firstEvaluation: true
property bool active: false
property int fromHour: Number(from.split(":")[0])
property int fromMinute: Number(from.split(":")[1])
property int toHour: Number(to.split(":")[0])
property int toMinute: Number(to.split(":")[1])
property int clockHour: DateTime.clock.hours
property int clockMinute: DateTime.clock.minutes
function isNoLater(hour1, minute1, hour2, minute2) {
if (hour1 < hour2)
return true;
if (hour1 === hour2 && minute1 < minute2)
return true;
return false;
}
onClockMinuteChanged: reEvaluate()
onAutomaticChanged: {
root.manualActive = undefined;
root.firstEvaluation = true;
reEvaluate();
}
function reEvaluate() {
const toHourIsNextDay = !isNoLater(fromHour, fromMinute, toHour, toMinute);
const toHourWrapped = toHourIsNextDay ? toHour + 24 : toHour;
const toMinuteWrapped = toMinute;
root.shouldBeOn = isNoLater(fromHour, fromMinute, clockHour, clockMinute) && isNoLater(clockHour, clockMinute, toHourWrapped, toMinuteWrapped);
if (firstEvaluation) {
firstEvaluation = false;
root.ensureState();
}
}
onShouldBeOnChanged: ensureState()
function ensureState() {
// console.log("[Hyprsunset] Ensuring state:", root.shouldBeOn, "Automatic mode:", root.automatic);
if (!root.automatic || root.manualActive !== undefined)
return;
if (root.shouldBeOn) {
root.enable();
} else {
root.disable();
}
}
function load() { } // Dummy to force init
function enable() {
root.active = true;
// console.log("[Hyprsunset] Enabling");
Quickshell.execDetached(["bash", "-c", `pidof hyprsunset || hyprsunset --temperature ${root.colorTemperature}`]);
}
function disable() {
root.active = false;
// console.log("[Hyprsunset] Disabling");
Quickshell.execDetached(["bash", "-c", `pkill hyprsunset`]);
}
function fetchState() {
fetchProc.running = true;
}
Process {
id: fetchProc
running: true
command: ["bash", "-c", "hyprctl hyprsunset temperature"]
stdout: StdioCollector {
id: stateCollector
onStreamFinished: {
const output = stateCollector.text.trim();
if (output.length == 0 || output.startsWith("Couldn't"))
root.active = false;
else
root.active = (output != "6500");
// console.log("[Hyprsunset] Fetched state:", output, "->", root.active);
}
}
}
function toggle() {
if (root.manualActive === undefined)
root.manualActive = root.active;
root.manualActive = !root.manualActive;
if (root.manualActive) {
root.enable();
} else {
root.disable();
}
}
}

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