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
165 changed files with 6386 additions and 4973 deletions
+3
View File
@@ -1,2 +1,5 @@
# You can make apps auto-start here
# 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
# 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
# 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
# Window/layer rules: https://wiki.hyprland.org/Configuring/Window-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 #########
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
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 &
# Input method
+1 -1
View File
@@ -138,7 +138,7 @@ misc {
swallow_regex = (foot|kitty|allacritty|Alacritty)
new_window_takes_over_fullscreen = 2
allow_session_lock_restore = true
session_lock_xray = true
# session_lock_xray = true
initial_workspace_tracking = false
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, O, global, quickshell:sidebarLeftToggle # [hidden]
bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar
bindd = Super, Slash, Toggle cheatsheet, global, quickshell:cheatsheetToggle # Toggle cheatsheet
bindd = Super, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
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, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls
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 = 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
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+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
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
bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard
# 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]
##! Session
bindd = Super, L, Lock, exec, loginctl lock-session # Lock
bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep
# bindd = Super, L, Lock, exec, loginctl lock-session # Lock
# bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
# 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
##! Screen
@@ -201,10 +201,10 @@ bindl= ,XF86AudioPlay, exec, playerctl play-pause # [hidden]
bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden]
##! 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, T, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] Kitty (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 = Super, E, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "dolphin" "nautilus" "nemo" "thunar" "kitty -1 fish -c yazi" # File manager
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 "$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 "$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" "$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, 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
@@ -1,7 +1,7 @@
#!/usr/bin/env bash
for cmd in "$@"; do
[[ -z "$cmd" ]] && continue
eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue
eval "$cmd" &
exit
done
exit 1
+11 -5
View File
@@ -8,11 +8,6 @@ $font_material_symbols = Material Symbols Rounded
background {
color = rgba(181818FF)
# path = {{ SWWW_WALL }}
# path = screenshot
# blur_size = 15
# blur_passes = 4
}
input-field {
monitor =
@@ -30,6 +25,17 @@ input-field {
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
monitor =
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
}
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
monitor =
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 sidebarLeftOpen: 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 workspaceShowNumbers: false
property bool superReleaseMightTrigger: true
property bool screenLocked: 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
onScreenZoomChanged: {
@@ -26,31 +32,15 @@ Singleton {
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 {
name: "workspaceNumber"
description: "Hold to show workspace numbers, release to show icons"
onPressed: {
workspaceShowNumbersTimer.start()
root.superDown = true
}
onReleased: {
workspaceShowNumbersTimer.stop()
workspaceShowNumbers = false
root.superDown = 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
- 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
- Use Markdown features 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.).
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! 💕
## 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~ ✨
- 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
## 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
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.
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
@@ -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.Hyprland
Scope {
Variants {
id: root
readonly property bool fixedClockPosition: Config.options.background.fixedClockPosition
readonly property real fixedClockX: Config.options.background.clockX
readonly property real fixedClockY: Config.options.background.clockY
model: Quickshell.screens
Variants {
model: Quickshell.screens
PanelWindow {
id: bgRoot
PanelWindow {
id: bgRoot
required property var modelData
required property var modelData
// Workspaces
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
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)
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))
// 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]
visible: !(activeWorkspaceWithFullscreen != undefined)
// Layer props
screen: modelData
exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: GlobalStates.screenLocked ? WlrLayer.Top : WlrLayer.Bottom
// WlrLayershell.layer: WlrLayer.Bottom
WlrLayershell.namespace: "quickshell:background"
// Workspaces
property HyprlandMonitor monitor: Hyprland.monitorFor(modelData)
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)
property int firstWorkspaceId: relevantWindows[0]?.workspace.id || 1
property int lastWorkspaceId: relevantWindows[relevantWindows.length - 1]?.workspace.id || 10
// 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 {
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()
}
left: wallpaper.left
top: wallpaper.top
leftMargin: ((root.fixedClockPosition ? root.fixedClockX : bgRoot.clockX * bgRoot.effectiveWallpaperScale) - implicitWidth / 2) - (wallpaper.effectiveValue * bgRoot.movableXSpace)
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)
}
}
// 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.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
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
}
}
// Wallpaper
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 {
RowLayout {
anchors {
horizontalCenter: parent.horizontalCenter
bottom: parent.bottom
bottomMargin: 30
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 && !GlobalStates.screenLockContainsCharacters) ? 1 : 0
scale: opacity
opacity: GlobalStates.screenLocked ? 1 : 0
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
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 {
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 QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
Item {
id: root
required property var bar
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
property string activeWindowAddress: `0x${activeWindow?.HyprlandToplevel?.address}`
property bool focusingThisMonitor: HyprlandData.activeWorkspace.monitor == monitor.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor.id]?.activeWorkspace.id)
property bool focusingThisMonitor: HyprlandData.activeWorkspace?.monitor == monitor?.name
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(HyprlandData.monitors[root.monitor?.id]?.activeWorkspace.id)
implicitWidth: colLayout.implicitWidth
@@ -45,7 +45,7 @@ Item {
elide: Text.ElideRight
text: root.focusingThisMonitor && root.activeWindow?.activated && root.biggestWindow ?
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
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 {
// For each monitor
model: {
@@ -47,8 +39,30 @@ Scope {
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
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
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"
implicitHeight: Appearance.sizes.barHeight + Appearance.rounding.screenRounding
mask: Region {
@@ -63,501 +77,116 @@ Scope {
right: true
}
Item { // Bar content region
id: barContent
anchors {
right: parent.right
left: parent.left
top: parent.top
bottom: undefined
}
implicitHeight: Appearance.sizes.barHeight
height: Appearance.sizes.barHeight
MouseArea {
id: hoverRegion
hoverEnabled: true
anchors.fill: parent
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
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
BarContent {
id: barContent
implicitHeight: Appearance.sizes.barHeight
anchors {
fill: parent
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
right: parent.right
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
id: barLeftSideMouseArea
anchors.left: parent.left
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 => {
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;
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: barContent
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: parent.bottom
}
}
}
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: {
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
}
PropertyChanges {
target: barContent
anchors.topMargin: 0
anchors.bottomMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight + 1 : 0
}
}
}
RowLayout { // Middle section
id: middleSection
anchors.centerIn: parent
spacing: Config.options?.bar.borderless ? 4 : 8
// Round decorators
Loader {
id: roundDecorators
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 {
id: leftCenterGroup
Layout.preferredWidth: barRoot.centerSideModuleWidth
Layout.fillHeight: true
Resources {
alwaysShowAllResources: barRoot.useShortenedForm === 2
Layout.fillWidth: barRoot.useShortenedForm === 2
}
Media {
visible: barRoot.useShortenedForm < 2
Layout.fillWidth: true
states: State {
name: "bottom"
when: Config.options.bar.bottom
AnchorChanges {
target: roundDecorators
anchors {
right: parent.right
left: parent.left
top: undefined
bottom: barContent.top
}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
sourceComponent: Item {
implicitHeight: Appearance.rounding.screenRounding
RoundCorner {
id: leftCorner
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
BarGroup {
id: middleCenterGroup
padding: workspacesWidget.widgetPadding
Layout.fillHeight: true
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
Workspaces {
id: workspacesWidget
bar: barRoot
Layout.fillHeight: true
MouseArea {
// Right-click to toggle overview
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
Hyprland.dispatch('global quickshell:overviewToggle');
}
corner: RoundCorner.CornerEnum.TopLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
leftCorner.corner: RoundCorner.CornerEnum.BottomLeft
}
}
}
}
VerticalBarSeparator {
visible: Config.options?.bar.borderless
}
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
RoundCorner {
id: rightCorner
anchors {
right: parent.right
top: !Config.options.bar.bottom ? parent.top : undefined
bottom: Config.options.bar.bottom ? parent.bottom : undefined
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
UtilButtons {
visible: (Config.options.bar.verbose && barRoot.useShortenedForm === 0)
Layout.alignment: Qt.AlignVCenter
}
BatteryIndicator {
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)
corner: RoundCorner.CornerEnum.TopRight
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
rightCorner.corner: RoundCorner.CornerEnum.BottomRight
}
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 {
enableAnimation: false
Layout.alignment: Qt.AlignVCenter
lineWidth: 2
value: percentage
size: 26
secondaryColor: (isLow && !isCharging) ? batteryLowBackground : Appearance.colors.colSecondaryContainer
primaryColor: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
implicitSize: 26
colSecondary: (isLow && !isCharging) ? batteryLowBackground : Appearance.colors.colSecondaryContainer
colPrimary: (isLow && !isCharging) ? batteryLowOnBackground : Appearance.m3colors.m3onSecondaryContainer
fill: (isLow && !isCharging)
MaterialSymbol {
+5 -4
View File
@@ -37,7 +37,7 @@ Item {
} else if (event.button === Qt.ForwardButton || event.button === Qt.RightButton) {
activePlayer.next();
} else if (event.button === Qt.LeftButton) {
Hyprland.dispatch("global quickshell:mediaControlsToggle")
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen
}
}
}
@@ -53,9 +53,10 @@ Item {
Layout.leftMargin: rowLayout.spacing
lineWidth: 2
value: activePlayer?.position / activePlayer?.length
size: 26
secondaryColor: Appearance.colors.colSecondaryContainer
primaryColor: Appearance.m3colors.m3onSecondaryContainer
implicitSize: 26
colSecondary: Appearance.colors.colSecondaryContainer
colPrimary: Appearance.m3colors.m3onSecondaryContainer
enableAnimation: false
MaterialSymbol {
anchors.centerIn: parent
@@ -21,9 +21,10 @@ Item {
Layout.alignment: Qt.AlignVCenter
lineWidth: 2
value: percentage
size: 26
secondaryColor: Appearance.colors.colSecondaryContainer
primaryColor: Appearance.m3colors.m3onSecondaryContainer
implicitSize: 26
colSecondary: Appearance.colors.colSecondaryContainer
colPrimary: Appearance.m3colors.m3onSecondaryContainer
enableAnimation: false
MaterialSymbol {
anchors.centerIn: parent
@@ -8,8 +8,6 @@ import Quickshell.Services.SystemTray
Item {
id: root
required property var bar
height: parent.height
implicitWidth: rowLayout.implicitWidth
Layout.leftMargin: Appearance.rounding.screenRounding
@@ -25,8 +23,6 @@ Item {
SysTrayItem {
required property SystemTrayItem modelData
bar: root.bar
item: modelData
}
@@ -10,7 +10,7 @@ import Qt5Compat.GraphicalEffects
MouseArea {
id: root
required property var bar
property var bar: root.QsWindow.window
required property SystemTrayItem item
property bool targetMenuOpen: false
property int trayItemWidth: Appearance.font.pixelSize.larger
@@ -59,12 +59,12 @@ MouseArea {
visible: false // There's already color overlay
anchors.fill: parent
source: trayIcon
desaturation: 1 // 1.0 means fully grayscale
desaturation: 0.8 // 1.0 means fully grayscale
}
ColorOverlay {
anchors.fill: 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.widgets
import QtQuick
@@ -5,6 +6,7 @@ import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import Quickshell.Services.Pipewire
import Quickshell.Services.UPower
Item {
id: root
@@ -23,7 +25,7 @@ Item {
visible: Config.options.bar.utilButtons.showScreenSnip
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.configPath("screenshot.qml")])
onClicked: Quickshell.execDetached(["qs", "-p", Quickshell.shellPath("screenshot.qml")])
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
fill: 1
@@ -55,7 +57,7 @@ Item {
visible: Config.options.bar.utilButtons.showKeyboardToggle
sourceComponent: CircleUtilButton {
Layout.alignment: Qt.AlignVCenter
onClicked: Hyprland.dispatch("global quickshell:oskToggle")
onClicked: GlobalStates.oskOpen = !GlobalStates.oskOpen
MaterialSymbol {
horizontalAlignment: Qt.AlignHCenter
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
Item {
required property var bar
id: root
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 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 int widgetPadding: 4
property int workspaceButtonWidth: 26
@@ -26,7 +26,31 @@ Item {
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1
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 updateWorkspaceOccupied() {
@@ -87,8 +111,8 @@ Item {
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
radius: Appearance.rounding.full
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 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 radiusLeft: leftOccupied ? 0 : Appearance.rounding.full
property var radiusRight: rightOccupied ? 0 : Appearance.rounding.full
@@ -98,7 +122,7 @@ Item {
bottomRightRadius: radiusRight
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 {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
@@ -176,9 +200,9 @@ Item {
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text
opacity: GlobalStates.workspaceShowNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || GlobalStates.workspaceShowNumbers))
|| (GlobalStates.workspaceShowNumbers && !Config.options?.bar.workspaces.showAppIcons)
opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0
z: 3
@@ -188,7 +212,7 @@ Item {
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
text: `${button.workspaceValue}`
elide: Text.ElideRight
color: (monitor.activeWorkspace?.id == button.workspaceValue) ?
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
@@ -200,7 +224,7 @@ Item {
Rectangle { // Dot instead of ws number
id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| GlobalStates.workspaceShowNumbers
|| root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1
visible: opacity > 0
@@ -208,7 +232,7 @@ Item {
width: workspaceButtonWidth * 0.18
height: width
radius: width / 2
color: (monitor.activeWorkspace?.id == button.workspaceValue) ?
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
@@ -222,20 +246,20 @@ Item {
width: workspaceButtonWidth
height: workspaceButtonWidth
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
visible: opacity > 0
IconImage {
id: mainAppIcon
anchors.bottom: parent.bottom
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
anchors.rightMargin: (!GlobalStates.workspaceShowNumbers && Config.options?.bar.workspaces.showAppIcons) ?
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
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 {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
@@ -21,7 +21,7 @@ MouseArea {
MaterialSymbol {
fill: 0
text: WeatherIcons.codeToName[Weather.data.wCode]
text: WeatherIcons.codeToName[Weather.data?.wCode] ?? "question_mark"
iconSize: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
Layout.alignment: Qt.AlignVCenter
@@ -31,7 +31,7 @@ MouseArea {
visible: true
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer1
text: Weather.data.temp
text: Weather.data?.temp ?? "--°"
Layout.alignment: Qt.AlignVCenter
}
}
@@ -14,7 +14,7 @@ Rectangle {
color: Appearance.colors.colLayer0
radius: Appearance.rounding.small
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
clip: true
ColumnLayout {
@@ -74,7 +74,7 @@ Scope { // Scope
anchors.centerIn: parent
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.windowRounding
property real padding: 30
implicitWidth: cheatsheetColumnLayout.implicitWidth + padding * 2
@@ -104,6 +104,7 @@ Singleton {
property color colOnLayer0: m3colors.m3onBackground
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 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 colOnLayer1: m3colors.m3onSurfaceVariant;
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 colSurfaceContainerHighestHover: ColorUtils.mix(m3colors.m3surfaceContainerHighest, m3colors.m3onSurface, 0.95)
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 colOnTooltip: "#F8F9FA" // m3colors.m3inverseOnSurface in the specs, but the m3 website actually uses this color
property color colTooltip: m3colors.m3inverseSurface
property color colOnTooltip: m3colors.m3inverseOnSurface
property color colScrim: ColorUtils.transparentize(m3colors.m3scrim, 0.5)
property color colShadow: ColorUtils.transparentize(m3colors.m3shadow, 0.7)
property color colOutlineVariant: m3colors.m3outlineVariant
@@ -265,7 +266,6 @@ Singleton {
easing.bezierCurve: root.animation.elementMoveFast.bezierCurve
}}
}
property QtObject clickBounce: QtObject {
property int duration: 200
property int type: Easing.BezierSpline
@@ -278,7 +278,7 @@ Singleton {
}}
}
property QtObject scroll: QtObject {
property int duration: 400
property int duration: 200
property int type: Easing.BezierSpline
property list<real> bezierCurve: animationCurves.standardDecel
}
@@ -8,6 +8,7 @@ Singleton {
id: root
property string filePath: Directories.shellConfigPath
property alias options: configOptionsJsonAdapter
property bool ready: false
function setNestedValue(nestedKey, value) {
let keys = nestedKey.split(".");
@@ -41,10 +42,10 @@ Singleton {
FileView {
path: root.filePath
watchChanges: true
onFileChanged: reload()
onAdapterUpdated: writeAdapter()
onLoaded: root.ready = true
onLoadFailed: error => {
if (error == FileViewError.FileNotFound) {
writeAdapter();
@@ -59,7 +60,22 @@ Singleton {
}
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 {
@@ -99,6 +115,7 @@ Singleton {
property real clockX: -500
property real clockY: -500
property string wallpaperPath: ""
property string thumbnailPath: ""
property JsonObject parallax: JsonObject {
property bool enableWorkspace: true
property real workspaceZoom: 1.07 // Relative to your screen, not wallpaper size
@@ -107,6 +124,14 @@ Singleton {
}
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 int cornerStyle: 0 // 0: Hug | 1: Float | 2: Plain rectangle
property bool borderless: false // true for no grouping of items
@@ -124,6 +149,7 @@ Singleton {
property bool showMicToggle: false
property bool showKeyboardToggle: true
property bool showDarkModeToggle: true
property bool showPerformanceProfileToggle: false
}
property JsonObject tray: JsonObject {
property bool monochromeIcons: true
@@ -163,6 +189,14 @@ Singleton {
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 translator: JsonObject {
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 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 bool enable: true
property real scale: 0.18 // Relative to screen size
property real rows: 2
property real columns: 5
@@ -207,6 +256,7 @@ Singleton {
}
property JsonObject sidebar: JsonObject {
property bool keepRightSidebarLoaded: true
property JsonObject translator: JsonObject {
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]
// Other dirs used by the shell, without "file://"
property string assetsPath: Quickshell.configPath("assets")
property string scriptPath: Quickshell.configPath("scripts")
property string assetsPath: Quickshell.shellPath("assets")
property string scriptPath: Quickshell.shellPath("scripts")
property string favicons: FileUtils.trimFileProtocol(`${Directories.cache}/media/favicons`)
property string coverArt: FileUtils.trimFileProtocol(`${Directories.cache}/media/coverart`)
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 screenshotTemp: "/tmp/quickshell/media/screenshot"
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 aiChats: FileUtils.trimFileProtocol(`${Directories.state}/user/ai/chats`)
// 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.Shapes
import qs.modules.common
/**
@@ -10,86 +8,81 @@ import qs.modules.common
Item {
id: root
property int size: 30
property int implicitSize: 30
property int lineWidth: 2
property real value: 0
property color primaryColor: Appearance.m3colors.m3onSecondaryContainer
property color secondaryColor: Appearance.colors.colSecondaryContainer
property real gapAngle: Math.PI / 9
property color colPrimary: Appearance.m3colors.m3onSecondaryContainer
property color colSecondary: Appearance.colors.colSecondaryContainer
property real gapAngle: 360 / 18
property bool fill: false
property int fillOverflow: 2
property int animationDuration: 1000
property bool enableAnimation: true
property int animationDuration: 800
property var easingType: Easing.OutCubic
width: size
height: size
implicitWidth: implicitSize
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 {
id: canvas
property real degree: 0
Loader {
active: root.fill
anchors.fill: parent
antialiasing: true
onDegreeChanged: {
requestPaint();
sourceComponent: Rectangle {
radius: 9999
color: root.colSecondary
}
}
onPaint: {
var ctx = getContext("2d");
var x = root.width / 2;
var y = root.height / 2;
var radius = root.size / 2 - root.lineWidth;
var startAngle = (Math.PI / 180) * 270;
var fullAngle = (Math.PI / 180) * (270 + 360);
var progressAngle = (Math.PI / 180) * (270 + degree);
var epsilon = 0.01; // Small angle in radians
ctx.reset();
if (root.fill) {
ctx.fillStyle = root.secondaryColor;
ctx.beginPath();
ctx.arc(x, y, radius + fillOverflow, startAngle, fullAngle);
ctx.fill();
Shape {
anchors.fill: parent
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: secondaryPath
strokeColor: root.colSecondary
strokeWidth: root.lineWidth
capStyle: ShapePath.RoundCap
fillColor: "transparent"
PathAngleArc {
centerX: root.centerX
centerY: root.centerY
radiusX: root.arcRadius
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();
}
Behavior on degree {
NumberAnimation {
duration: root.animationDuration
easing.type: root.easingType
ShapePath {
id: primaryPath
strokeColor: root.colPrimary
strokeWidth: root.lineWidth
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.widgets
Flickable {
StyledFlickable {
id: root
property real baseWidth: 550
property bool forceWidth: false
@@ -25,4 +25,5 @@ Flickable {
}
spacing: 20
}
}
@@ -9,7 +9,7 @@ Item {
property bool colorize: false
property color color
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
height: 30
@@ -93,13 +93,23 @@ Button {
root.down = false
if (event.button != Qt.LeftButton) return;
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) => {
root.down = false
}
onPressAndHold: () => {
altAction();
root.down = false;
root.clicked = false;
};
}
background: Rectangle {
id: buttonBackground
topLeftRadius: root.leftRadius
@@ -214,13 +214,13 @@ Item { // Notification item area
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarRightClose")
GlobalStates.sidebarRightOpen = false
}
PointingHandLinkHover {}
}
Flickable { // Notification actions
StyledFlickable { // Notification actions
id: actionsFlickable
Layout.fillWidth: true
implicitHeight: actionRowLayout.implicitHeight
@@ -1,4 +1,5 @@
import QtQuick 2.9
import QtQuick
import QtQuick.Shapes
Item {
id: root
@@ -6,55 +7,57 @@ Item {
enum CornerEnum { TopLeft, TopRight, BottomLeft, BottomRight }
property var corner: RoundCorner.CornerEnum.TopLeft // Default to TopLeft
property int size: 25
property int implicitSize: 25
property color color: "#000000"
onColorChanged: {
canvas.requestPaint();
}
onCornerChanged: {
canvas.requestPaint();
}
implicitWidth: size
implicitHeight: size
Canvas {
id: canvas
implicitWidth: implicitSize
implicitHeight: implicitSize
Shape {
anchors.fill: parent
antialiasing: true
onPaint: {
var ctx = getContext("2d");
var r = root.size;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
switch (root.corner) {
case RoundCorner.CornerEnum.TopLeft:
ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2);
ctx.lineTo(0, 0);
break;
case RoundCorner.CornerEnum.TopRight:
ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI);
ctx.lineTo(r, 0);
break;
case RoundCorner.CornerEnum.BottomLeft:
ctx.arc(r, 0, r, Math.PI / 2, Math.PI);
ctx.lineTo(0, r);
break;
case RoundCorner.CornerEnum.BottomRight:
ctx.arc(0, 0, r, 0, Math.PI / 2);
ctx.lineTo(r, r);
break;
layer.enabled: true
layer.smooth: true
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: shapePath
strokeWidth: 0
fillColor: root.color
startX: switch (root.corner) {
case RoundCorner.CornerEnum.TopLeft: return 0;
case RoundCorner.CornerEnum.TopRight: return root.implicitSize;
case RoundCorner.CornerEnum.BottomLeft: return 0;
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
}
startY: switch (root.corner) {
case RoundCorner.CornerEnum.TopLeft: return 0;
case RoundCorner.CornerEnum.TopRight: return 0;
case RoundCorner.CornerEnum.BottomLeft: return root.implicitSize;
case RoundCorner.CornerEnum.BottomRight: return root.implicitSize;
}
PathAngleArc {
moveToStart: false
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)
}
@@ -63,12 +63,13 @@ Item {
Layout.rightMargin: dialogPadding
}
ListView {
StyledListView {
id: choiceListView
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
currentIndex: root.defaultChoice !== undefined ? root.items.indexOf(root.defaultChoice) : -1
spacing: 6
model: ScriptModel {
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 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() {
root.dragIndex = -1
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 {
animations: [
Appearance?.animation.elementMove.numberAnimation.createObject(this, {
@@ -10,7 +10,7 @@ import Quickshell.Services.Pipewire
RadioButton {
id: root
implicitHeight: 40
implicitHeight: contentItem.implicitHeight + 4 * 2
property string description
property color activeColor: Appearance?.colors.colPrimary ?? "#685496"
property color inactiveColor: Appearance?.m3colors.m3onSurfaceVariant ?? "#45464F"
@@ -20,6 +20,7 @@ RadioButton {
indicator: Item{}
contentItem: RowLayout {
id: contentItem
Layout.fillWidth: true
spacing: 12
Rectangle {
+2 -2
View File
@@ -94,7 +94,7 @@ Scope { // Scope
anchors.bottomMargin: Appearance.sizes.hyprlandGapsOut
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.large
}
@@ -129,7 +129,7 @@ Scope { // Scope
DockSeparator {}
DockButton {
Layout.fillHeight: true
onClicked: Hyprland.dispatch("global quickshell:overviewToggle")
onClicked: GlobalStates.overviewOpen = !GlobalStates.overviewOpen
contentItem: MaterialSymbol {
anchors.fill: parent
horizontalAlignment: Text.AlignHCenter
@@ -26,8 +26,18 @@ Scope {
property list<real> visualizerPoints: []
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) {
// return true
if (!Config.options.media.filterDuplicatePlayers) {
return true;
}
return (
// Remove unecessary native buses from browsers if there's plasma integration
!(hasPlasmaIntegration && player.dbusName.startsWith('org.mpris.MediaPlayer2.firefox')) &&
@@ -88,7 +98,12 @@ Scope {
Loader {
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 {
id: mediaControlsRoot
@@ -160,11 +175,7 @@ Scope {
description: "Toggles media controls on press"
onPressed: {
if (!mediaControlsLoader.active && Mpris.players.values.filter(player => isRealPlayer(player)).length === 0) {
return;
}
mediaControlsLoader.active = !mediaControlsLoader.active;
if(mediaControlsLoader.active) Notifications.timeoutAll();
GlobalStates.mediaControlsOpen = !GlobalStates.mediaControlsOpen;
}
}
GlobalShortcut {
@@ -172,8 +183,7 @@ Scope {
description: "Opens media controls on press"
onPressed: {
mediaControlsLoader.active = true;
Notifications.timeoutAll();
GlobalStates.mediaControlsOpen = true;
}
}
GlobalShortcut {
@@ -181,7 +191,7 @@ Scope {
description: "Closes media controls on press"
onPressed: {
mediaControlsLoader.active = false;
GlobalStates.mediaControlsOpen = false;
}
}
@@ -12,12 +12,11 @@ import Quickshell.Wayland
Scope {
id: root
property bool showOsdValues: false
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
property var brightnessMonitor: Brightness.getMonitorForScreen(focusedScreen)
function triggerOsd() {
showOsdValues = true
GlobalStates.osdBrightnessOpen = true
osdTimeout.restart()
}
@@ -27,7 +26,7 @@ Scope {
repeat: false
running: false
onTriggered: {
showOsdValues = false
GlobalStates.osdBrightnessOpen = false
}
}
@@ -35,7 +34,7 @@ Scope {
target: Audio.sink?.audio ?? null
function onVolumeChanged() {
if (!Audio.ready) return
root.showOsdValues = false
GlobalStates.osdBrightnessOpen = false
}
}
@@ -49,7 +48,7 @@ Scope {
Loader {
id: osdLoader
active: showOsdValues
active: GlobalStates.osdBrightnessOpen
sourceComponent: PanelWindow {
id: osdRoot
@@ -91,7 +90,7 @@ Scope {
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: root.showOsdValues = false
onEntered: GlobalStates.osdBrightnessOpen = false
}
Behavior on implicitHeight {
@@ -125,11 +124,11 @@ Scope {
}
function hide() {
showOsdValues = false
GlobalStates.osdBrightnessOpen = false
}
function toggle() {
showOsdValues = !showOsdValues
GlobalStates.osdBrightnessOpen = !GlobalStates.osdBrightnessOpen
}
}
@@ -146,7 +145,7 @@ Scope {
description: "Hides brightness OSD on press"
onPressed: {
root.showOsdValues = false
GlobalStates.osdBrightnessOpen = false
}
}
@@ -12,12 +12,11 @@ import Quickshell.Hyprland
Scope {
id: root
property bool showOsdValues: false
property string protectionMessage: ""
property var focusedScreen: Quickshell.screens.find(s => s.name === Hyprland.focusedMonitor?.name)
function triggerOsd() {
showOsdValues = true
GlobalStates.osdVolumeOpen = true
osdTimeout.restart()
}
@@ -27,7 +26,7 @@ Scope {
repeat: false
running: false
onTriggered: {
root.showOsdValues = false
GlobalStates.osdVolumeOpen = false
root.protectionMessage = ""
}
}
@@ -35,7 +34,7 @@ Scope {
Connections {
target: Brightness
function onBrightnessChanged() {
showOsdValues = false
GlobalStates.osdVolumeOpen = false
}
}
@@ -61,7 +60,7 @@ Scope {
Loader {
id: osdLoader
active: showOsdValues
active: GlobalStates.osdVolumeOpen
sourceComponent: PanelWindow {
id: osdRoot
@@ -103,7 +102,7 @@ Scope {
MouseArea {
anchors.fill: parent
hoverEnabled: true
onEntered: root.showOsdValues = false
onEntered: GlobalStates.osdVolumeOpen = false
}
ColumnLayout {
@@ -177,11 +176,11 @@ Scope {
}
function hide() {
showOsdValues = false
GlobalStates.osdVolumeOpen = false
}
function toggle() {
showOsdValues = !showOsdValues
GlobalStates.osdVolumeOpen = !GlobalStates.osdVolumeOpen
}
}
GlobalShortcut {
@@ -197,7 +196,7 @@ Scope {
description: "Hides volume OSD on press"
onPressed: {
root.showOsdValues = false
GlobalStates.osdVolumeOpen = false
}
}
@@ -49,7 +49,10 @@ Item {
Layout.topMargin: valueIndicatorVerticalPadding
Layout.bottomMargin: valueIndicatorVerticalPadding
MaterialSymbol { // Icon
anchors.centerIn: parent
anchors {
centerIn: parent
alignWhenCentered: !root.rotateIcon
}
color: Appearance.colors.colOnLayer0
renderType: Text.QtRendering
@@ -24,7 +24,7 @@ Scope { // Scope
Loader {
id: oskLoader
active: false
active: GlobalStates.oskOpen
onActiveChanged: {
if (!oskLoader.active) {
Ydotool.releaseAllKeys();
@@ -124,15 +124,15 @@ Scope { // Scope
target: "osk"
function toggle(): void {
oskLoader.active = !oskLoader.active
GlobalStates.oskOpen = !GlobalStates.oskOpen;
}
function close(): void {
oskLoader.active = false
GlobalStates.oskOpen = false
}
function open(): void {
oskLoader.active = true
GlobalStates.oskOpen = true
}
}
@@ -141,7 +141,7 @@ Scope { // Scope
description: "Toggles on screen keyboard on press"
onPressed: {
oskLoader.active = !oskLoader.active;
GlobalStates.oskOpen = !GlobalStates.oskOpen;
}
}
@@ -150,7 +150,7 @@ Scope { // Scope
description: "Opens on screen keyboard on press"
onPressed: {
oskLoader.active = true;
GlobalStates.oskOpen = true
}
}
@@ -159,7 +159,7 @@ Scope { // Scope
description: "Closes on screen keyboard on press"
onPressed: {
oskLoader.active = false;
GlobalStates.oskOpen = false
}
}
@@ -13,8 +13,10 @@ import Quickshell.Hyprland
Item {
id: root
property var activeLayoutName: Config.options?.osk.layout ?? Layouts.defaultLayout
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]
implicitWidth: keyRows.implicitWidth
@@ -1,11 +1,11 @@
// We're going to use ydotool
// See /usr/include/linux/input-event-codes.h for keycodes
const defaultLayout = "qwerty_full";
const defaultLayout = "English (US)";
const byName = {
"qwerty_full": {
name: "QWERTY - Full",
"English (US)": {
name_short: "US",
description: "QWERTY - Full",
comment: "Like physical keyboard",
// 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
@@ -113,9 +113,9 @@ const byName = {
]
]
},
"qwertz_full": {
name: "QWERTZ - Full",
"German": {
name_short: "DE",
description: "QWERTZ - Full",
comment: "Keyboard layout commonly used in German-speaking countries",
keys: [
[
@@ -214,5 +214,99 @@ const byName = {
{ 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
property string searchingText: ""
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
visible: GlobalStates.overviewOpen
@@ -40,6 +40,8 @@ Scope {
anchors {
top: true
bottom: true
left: !(Config?.options.overview.enable ?? true)
right: !(Config?.options.overview.enable ?? true)
}
HyprlandFocusGrab {
@@ -84,7 +86,7 @@ Scope {
function setSearchingText(text) {
searchWidget.setSearchingText(text);
searchWidget.focusFirstItemIfNeeded();
searchWidget.focusFirstItem();
}
ColumnLayout {
@@ -122,7 +124,7 @@ Scope {
Loader {
id: overviewLoader
active: GlobalStates.overviewOpen
active: GlobalStates.overviewOpen && (Config?.options.overview.enable ?? true)
sourceComponent: OverviewWidget {
panelWindow: root
visible: (root.searchingText == "")
@@ -20,7 +20,7 @@ Item {
property var windows: HyprlandData.windowList
property var windowByAddress: HyprlandData.windowByAddress
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 color activeBorderColor: Appearance.colors.colSecondary
@@ -61,7 +61,7 @@ Item {
radius: Appearance.rounding.screenRounding * root.scale + padding
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
ColumnLayout { // Workspaces
id: workspaceColumnLayout
@@ -149,14 +149,15 @@ Item {
const address = `0x${toplevel.HyprlandToplevel.address}`
var win = windowByAddress[address]
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 && inMonitor;
return inWorkspaceGroup;
})
}
}
delegate: OverviewWindow {
id: window
required property var modelData
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors[monitorId]
property var address: `0x${modelData.HyprlandToplevel.address}`
windowData: windowByAddress[address]
toplevel: modelData
@@ -164,9 +165,7 @@ Item {
scale: root.scale
availableWorkspaceWidth: root.workspaceImplicitWidth
availableWorkspaceHeight: root.workspaceImplicitHeight
property int monitorId: windowData?.monitor
property var monitor: HyprlandData.monitors[monitorId]
widgetMonitorId: root.monitor.id
property bool atInitPosition: (initX == x && initY == y)
@@ -1,7 +1,6 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import Qt5Compat.GraphicalEffects
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 xOffset: 0
property real yOffset: 0
property int widgetMonitorId: 0
property var targetWindowWidth: windowData?.size[0] * scale
property var targetWindowHeight: windowData?.size[1] * scale
@@ -40,6 +40,7 @@ Item { // Window
y: initY
width: windowData?.size[0] * root.scale
height: windowData?.size[1] * root.scale
opacity: windowData.monitor == widgetMonitorId ? 1 : 0.4
layer.enabled: true
layer.effect: OpacityMask {
@@ -69,6 +70,7 @@ Item { // Window
captureSource: GlobalStates.overviewOpen ? root.toplevel : null
live: true
// Color overlay for interactions
Rectangle {
anchors.fill: parent
radius: Appearance.rounding.windowRounding * root.scale
@@ -90,7 +90,7 @@ RippleButton {
onClicked: {
root.itemExecute()
Hyprland.dispatch("global quickshell:overviewClose")
GlobalStates.overviewOpen = false
}
Keys.onPressed: (event) => {
if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) {
@@ -58,7 +58,7 @@ Item { // Wrapper
{
action: "konachanwall",
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() {
if (searchInput.focus)
appResults.currentIndex = 0; // Focus the first item
function focusFirstItem() {
appResults.currentIndex = 0;
}
Timer {
@@ -99,7 +98,7 @@ Item { // Wrapper
stdout: SplitParser {
onRead: data => {
root.mathResult = data;
root.focusFirstItemIfNeeded();
root.focusFirstItem();
}
}
}
@@ -164,7 +163,7 @@ Item { // Wrapper
radius: Appearance.rounding.large
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
ColumnLayout {
id: columnLayout
@@ -250,7 +249,7 @@ Item { // Wrapper
color: Appearance.colors.colOutlineVariant
}
ListView { // App results
StyledListView { // App results
id: appResults
visible: root.showResults
Layout.fillWidth: true
@@ -261,6 +260,8 @@ Item { // Wrapper
spacing: 2
KeyNavigation.up: searchBar
highlightMoveDuration: 100
add: null
remove: null
onFocusChanged: {
if (focus)
@@ -277,6 +278,9 @@ Item { // Wrapper
model: ScriptModel {
id: model
onValuesChanged: {
root.focusFirstItem();
}
values: {
// Search results are handled here
////////////////// Skip? //////////////////
@@ -405,8 +409,6 @@ Item { // Wrapper
}
}
onModelChanged: root.focusFirstItemIfNeeded()
delegate: SearchItem {
// The selectable item for each search result
required property var modelData
@@ -13,7 +13,8 @@ Scope {
component CornerPanelWindow: PanelWindow {
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
exclusionMode: ExclusionMode.Ignore
@@ -35,7 +36,7 @@ Scope {
implicitHeight: cornerWidget.implicitHeight
RoundCorner {
id: cornerWidget
size: Appearance.rounding.screenRounding
implicitSize: Appearance.rounding.screenRounding
corner: cornerPanelWindow.corner
}
}
@@ -44,22 +45,34 @@ Scope {
model: Quickshell.screens
Scope {
id: monitorScope
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 {
screen: modelData
corner: RoundCorner.CornerEnum.TopLeft
fullscreen: monitorScope.fullscreen
}
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.TopRight
fullscreen: monitorScope.fullscreen
}
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.BottomLeft
fullscreen: monitorScope.fullscreen
}
CornerPanelWindow {
screen: modelData
corner: RoundCorner.CornerEnum.BottomRight
fullscreen: monitorScope.fullscreen
}
}
}
+100 -26
View File
@@ -14,6 +14,30 @@ import Quickshell.Hyprland
Scope {
id: root
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() {
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 {
id: sessionLoader
active: false
active: GlobalStates.sessionOpen
onActiveChanged: {
if (sessionLoader.active) root.detectRunningStuff();
}
Connections {
target: GlobalStates
function onScreenLockedChanged() {
if (GlobalStates.screenLocked) {
sessionLoader.active = false;
GlobalStates.sessionOpen = false;
}
}
}
@@ -40,9 +93,8 @@ Scope {
property string subtitle
function hide() {
sessionLoader.active = false
GlobalStates.sessionOpen = false;
}
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:session"
@@ -68,6 +120,7 @@ Scope {
}
ColumnLayout { // Content column
id: contentColumn
anchors.centerIn: parent
spacing: 15
@@ -182,27 +235,39 @@ Scope {
}
}
Rectangle {
DescriptionLabel {
Layout.alignment: Qt.AlignHCenter
radius: Appearance.rounding.normal
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
}
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"
function toggle(): void {
sessionLoader.active = !sessionLoader.active;
GlobalStates.sessionOpen = !GlobalStates.sessionOpen;
}
function close(): void {
sessionLoader.active = false;
GlobalStates.sessionOpen = false
}
function open(): void {
sessionLoader.active = true;
GlobalStates.sessionOpen = true
}
}
@@ -227,7 +292,7 @@ Scope {
description: "Toggles session screen on press"
onPressed: {
sessionLoader.active = !sessionLoader.active;
GlobalStates.sessionOpen = !GlobalStates.sessionOpen;
}
}
@@ -236,7 +301,16 @@ Scope {
description: "Opens session screen on press"
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 {
title: Translation.tr("Appearance")
title: Translation.tr("Overall appearance")
ConfigRow {
uniform: true
ConfigSwitch {
@@ -164,8 +164,11 @@ ContentPage {
}
}
ConfigSwitch {
opacity: 0
enabled: false
text: Translation.tr("Performance Profile toggle")
checked: Config.options.bar.utilButtons.showPerformanceProfileToggle
onCheckedChanged: {
Config.options.bar.utilButtons.showPerformanceProfileToggle = checked;
}
}
}
}
@@ -184,13 +187,20 @@ ContentPage {
}
}
ConfigSwitch {
text: Translation.tr('Always show numbers')
checked: Config.options.bar.workspaces.alwaysShowNumbers
text: Translation.tr('Tint app icons')
checked: Config.options.bar.workspaces.monochromeIcons
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 {
text: Translation.tr("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 {
title: Translation.tr("Weather")
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 {
@@ -322,6 +365,13 @@ ContentPage {
ContentSection {
title: Translation.tr("Overview")
ConfigSwitch {
text: Translation.tr("Enable")
checked: Config.options.overview.enable
onCheckedChanged: {
Config.options.overview.enable = checked;
}
}
ConfigSpinBox {
text: Translation.tr("Scale (%)")
value: Config.options.overview.scale * 100
@@ -2,9 +2,8 @@ import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import "./aiChat/"
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import qs.modules.common.functions
import "./aiChat/"
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
@@ -46,6 +45,22 @@ Item {
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",
description: Translation.tr("Set the system prompt for the model."),
@@ -74,7 +89,7 @@ Item {
execute: (args) => {
const joinedArgs = args.join(" ")
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;
}
Ai.saveChat(joinedArgs)
@@ -86,7 +101,7 @@ Item {
execute: (args) => {
const joinedArgs = args.join(" ")
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;
}
Ai.loadChat(joinedArgs)
@@ -126,7 +141,7 @@ Mowe uwu wem ipsum!
### Formatting
- *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
@@ -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 {
id: columnLayout
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
Layout.fillWidth: true
Layout.fillHeight: true
@@ -201,6 +282,9 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
spacing: 10
popin: false
touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4
mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4
property int lastResponseLength: 0
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
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 {
values: Ai.messageIDs.filter(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),
}
})
} 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)) {
root.suggestionQuery = messageInputField.text
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.bottom: parent.bottom
anchors.bottomMargin: 5
anchors.leftMargin: 5
anchors.leftMargin: 10
anchors.rightMargin: 5
spacing: 5
spacing: 4
property var commandsShown: [
{
name: "model",
name: "",
sendDirectly: false,
},
dontAddSpace: true,
},
{
name: "clear",
sendDirectly: true,
},
]
Item {
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
RowLayout {
id: providerRowLayout
anchors.centerIn: parent
ApiInputBoxIndicator { // Model indicator
icon: "api"
text: Ai.getModel().name
tooltipText: Translation.tr("Current model: %1\nSet it with %2model MODEL")
.arg(Ai.getModel().name)
.arg(root.commandPrefix)
}
MaterialSymbol {
text: "api"
iconSize: Appearance.font.pixelSize.large
}
StyledText {
id: providerName
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
}
ApiInputBoxIndicator { // Tool indicator
icon: "service_toolbox"
text: Ai.currentTool.charAt(0).toUpperCase() + Ai.currentTool.slice(1)
tooltipText: Translation.tr("Current tool: %1\nSet it with %2tool TOOL")
.arg(Ai.currentTool)
.arg(root.commandPrefix)
}
Item { Layout.fillWidth: true }
ButtonGroup {
ButtonGroup { // Command buttons
padding: 0
Repeater { // Command buttons
@@ -600,7 +675,7 @@ Inline w/ backslash and round brackets \\(e^{i\\pi} + 1 = 0\\)
if(modelData.sendDirectly) {
root.handleInput(commandRepresentation)
} else {
messageInputField.text = commandRepresentation + " "
messageInputField.text = commandRepresentation + (modelData.dontAddSpace ? "" : " ")
messageInputField.cursorPosition = messageInputField.text.length
messageInputField.forceActiveFocus()
}
@@ -3,7 +3,6 @@ import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "./anime/"
import QtQuick
import QtQuick.Controls
@@ -138,6 +137,9 @@ Item {
anchors.fill: parent
spacing: 10
touchpadScrollFactor: Config.options.interactions.scrolling.touchpadScrollFactor * 1.4
mouseScrollFactor: Config.options.interactions.scrolling.mouseScrollFactor * 1.4
property int lastResponseLength: 0
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 {
values: {
if(root.responses.length > booruResponseListView.lastResponseLength) {
@@ -493,40 +486,12 @@ Item {
},
]
Item {
implicitHeight: providerRowLayout.implicitHeight + 5 * 2
implicitWidth: providerRowLayout.implicitWidth + 10 * 2
RowLayout {
id: providerRowLayout
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
}
ApiInputBoxIndicator { // Tool indicator
icon: "api"
text: Booru.providers[Booru.currentProvider].name
tooltipText: Translation.tr("Current API endpoint: %1\nSet it with %2mode PROVIDER")
.arg(Booru.providers[Booru.currentProvider].url)
.arg(root.commandPrefix)
}
StyledText {
@@ -1,6 +1,5 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
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
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
Behavior on width {
@@ -263,7 +263,6 @@ Rectangle {
}
Flow { // Annotations
id: annotationFlowLayout
visible: root.messageData?.annotationSources?.length > 0
spacing: 5
Layout.fillWidth: true
@@ -274,12 +273,28 @@ Rectangle {
values: root.messageData?.annotationSources || []
}
delegate: AnnotationSourceButton {
id: annotationButton
required property var modelData
displayText: modelData.text
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: {
if (url) {
Qt.openUrlExternally(url)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
GlobalStates.sidebarLeftOpen = false
}
}
@@ -12,12 +12,15 @@ import Quickshell
import org.kde.syntaxhighlighting
ColumnLayout {
id: root
// These are needed on the parent loader
property bool editing: parent?.editing ?? false
property bool renderMarkdown: parent?.renderMarkdown ?? true
property bool enableMouseSelection: parent?.enableMouseSelection ?? false
property var segmentContent: parent?.segmentContent ?? ({})
property var segmentLang: parent?.segmentLang ?? "txt"
property bool isCommandRequest: segmentLang === "command"
property var displayLang: (isCommandRequest ? "bash" : segmentLang)
property var messageData: parent?.messageData ?? {}
property real codeBlockBackgroundRounding: Appearance.rounding.small
@@ -56,7 +59,7 @@ ColumnLayout {
font.pixelSize: Appearance.font.pixelSize.small
font.weight: Font.DemiBold
color: Appearance.colors.colOnLayer2
text: segmentLang ? Repository.definitionForName(segmentLang).name : "plain"
text: root.displayLang ? Repository.definitionForName(root.displayLang).name : "plain"
}
Item { Layout.fillWidth: true }
@@ -123,6 +126,7 @@ ColumnLayout {
Rectangle { // Line numbers
implicitWidth: 40
implicitHeight: lineNumberColumnLayout.implicitHeight
Layout.fillHeight: true
Layout.fillWidth: false
topLeftRadius: Appearance.rounding.unsharpen
@@ -133,10 +137,13 @@ ColumnLayout {
ColumnLayout {
id: lineNumberColumnLayout
anchors.left: parent.left
anchors.right: parent.right
anchors.rightMargin: 5
anchors.verticalCenter: parent.verticalCenter
anchors {
left: parent.left
right: parent.right
rightMargin: 5
top: parent.top
topMargin: 6
}
spacing: 0
Repeater {
@@ -162,82 +169,116 @@ ColumnLayout {
topRightRadius: Appearance.rounding.unsharpen
bottomRightRadius: codeBlockBackgroundRounding
color: Appearance.colors.colLayer2
implicitHeight: codeTextArea.implicitHeight
implicitHeight: codeColumnLayout.implicitHeight
ScrollView {
id: codeScrollView
Layout.fillWidth: true
Layout.fillHeight: true
implicitWidth: parent.width
implicitHeight: codeTextArea.implicitHeight + 1
contentWidth: codeTextArea.width - 1
// contentHeight: codeTextArea.contentHeight
clip: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal: ScrollBar {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
padding: 5
policy: ScrollBar.AsNeeded
opacity: visualSize == 1 ? 0 : 1
visible: opacity > 0
ColumnLayout {
id: codeColumnLayout
anchors.fill: parent
spacing: 0
ScrollView {
id: codeScrollView
Layout.fillWidth: true
// Layout.fillHeight: true
implicitWidth: parent.width
implicitHeight: codeTextArea.implicitHeight + 1
contentWidth: codeTextArea.width - 1
// contentHeight: codeTextArea.contentHeight
clip: true
ScrollBar.vertical.policy: ScrollBar.AlwaysOff
ScrollBar.horizontal: ScrollBar {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
padding: 5
policy: ScrollBar.AsNeeded
opacity: visualSize == 1 ? 0 : 1
visible: opacity > 0
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
Behavior on opacity {
NumberAnimation {
duration: Appearance.animation.elementMoveFast.duration
easing.type: Appearance.animation.elementMoveFast.type
easing.bezierCurve: Appearance.animation.elementMoveFast.bezierCurve
}
}
contentItem: Rectangle {
implicitHeight: 6
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2Active
}
}
contentItem: Rectangle {
implicitHeight: 6
radius: Appearance.rounding.small
color: Appearance.colors.colLayer2Active
TextArea { // Code
id: codeTextArea
Layout.fillWidth: true
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
}
}
}
TextArea { // Code
id: codeTextArea
Loader {
active: root.isCommandRequest && root.messageData.functionPending
visible: active
Layout.fillWidth: true
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;
Layout.margins: 6
Layout.topMargin: 0
sourceComponent: RowLayout {
Item { Layout.fillWidth: true }
ButtonGroup {
GroupButton {
contentItem: StyledText {
text: Translation.tr("Reject")
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnLayer2
}
onClicked: Ai.rejectCommand(root.messageData)
}
GroupButton {
toggled: true
contentItem: StyledText {
text: Translation.tr("Approve")
font.pixelSize: Appearance.font.pixelSize.small
color: Appearance.colors.colOnPrimary
}
onClicked: Ai.approveCommand(root.messageData)
}
}
}
SyntaxHighlighter {
id: highlighter
textEdit: codeTextArea
repository: Repository
definition: Repository.definitionForName(segmentLang || "plaintext")
theme: Appearance.syntaxHighlightingTheme
}
}
}
@@ -128,7 +128,7 @@ ColumnLayout {
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
GlobalStates.sidebarLeftOpen = false
}
MouseArea { // Pointing hand for links
@@ -92,7 +92,7 @@ Item {
id: thinkBlockLanguage
Layout.fillWidth: false
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 }
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
width: root.rowHeight * modelData.aspect_ratio
height: root.rowHeight
visible: opacity > 0
opacity: status === Image.Ready ? 1 : 0
fillMode: Image.PreserveAspectFit
source: modelData.preview_url
sourceSize.width: root.rowHeight * modelData.aspect_ratio
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.effect: OpacityMask {
maskSource: Rectangle {
@@ -78,10 +82,6 @@ Button {
radius: imageRadius
}
}
Behavior on opacity {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
RippleButton {
@@ -97,7 +97,7 @@ Rectangle {
}
}
Flickable { // Tag strip
StyledFlickable { // Tag strip
id: tagsFlickable
visible: root.responseData.tags.length > 0
Layout.alignment: Qt.AlignLeft
@@ -158,7 +158,7 @@ Rectangle {
textFormat: Text.MarkdownText
onLinkActivated: (link) => {
Qt.openUrlExternally(link)
Hyprland.dispatch("global quickshell:sidebarLeftClose")
GlobalStates.sidebarLeftOpen = false
}
PointingHandLinkHover {}
}
@@ -17,7 +17,7 @@ Scope {
id: root
property int sidebarWidth: Appearance.sizes.sidebarWidth
property int sidebarPadding: 12
property string settingsQmlPath: Quickshell.configPath("settings.qml")
property string settingsQmlPath: Quickshell.shellPath("settings.qml")
PanelWindow {
id: sidebarRoot
@@ -51,15 +51,10 @@ Scope {
Loader {
id: sidebarContentLoader
active: GlobalStates.sidebarRightOpen
active: GlobalStates.sidebarRightOpen || Config?.options.sidebar.keepRightSidebarLoaded
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
left: parent.left
topMargin: Appearance.sizes.hyprlandGapsOut
rightMargin: Appearance.sizes.hyprlandGapsOut
bottomMargin: Appearance.sizes.hyprlandGapsOut
fill: parent
margins: Appearance.sizes.hyprlandGapsOut
leftMargin: Appearance.sizes.elevationMargin
}
width: sidebarWidth - Appearance.sizes.hyprlandGapsOut - Appearance.sizes.elevationMargin
@@ -87,7 +82,7 @@ Scope {
implicitWidth: sidebarWidth - Appearance.sizes.hyprlandGapsOut * 2
color: Appearance.colors.colLayer0
border.width: 1
border.color: Appearance.m3colors.m3outlineVariant
border.color: Appearance.colors.colLayer0Border
radius: Appearance.rounding.screenRounding - Appearance.sizes.hyprlandGapsOut + 1
ColumnLayout {
@@ -102,26 +97,19 @@ Scope {
Layout.topMargin: 5
Layout.bottomMargin: 0
Item {
implicitWidth: distroIcon.width
implicitHeight: distroIcon.height
CustomIcon {
id: distroIcon
width: 25
height: 25
source: SystemInfo.distroIcon
}
ColorOverlay {
anchors.fill: distroIcon
source: distroIcon
color: Appearance.colors.colOnLayer0
}
CustomIcon {
id: distroIcon
width: 25
height: 25
source: SystemInfo.distroIcon
colorize: true
color: Appearance.colors.colOnLayer0
}
StyledText {
font.pixelSize: Appearance.font.pixelSize.normal
color: Appearance.colors.colOnLayer0
text: Translation.tr("Uptime: %1").arg(DateTime.uptime)
text: Translation.tr("Up %1").arg(DateTime.uptime)
textFormat: Text.MarkdownText
}
@@ -145,7 +133,7 @@ Scope {
toggled: false
buttonIcon: "settings"
onClicked: {
Hyprland.dispatch("global quickshell:sidebarRightClose")
GlobalStates.sidebarRightOpen = false
Quickshell.execDetached(["qs", "-p", root.settingsQmlPath])
}
StyledToolTip {
@@ -156,7 +144,7 @@ Scope {
toggled: false
buttonIcon: "power_settings_new"
onClicked: {
Hyprland.dispatch("global quickshell:sessionOpen")
GlobalStates.sessionOpen = true
}
StyledToolTip {
content: Translation.tr("Session")
@@ -16,7 +16,7 @@ QuickToggleButton {
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Config.options.apps.bluetooth}`])
Hyprland.dispatch("global quickshell:sidebarRightClose")
GlobalStates.sidebarRightOpen = false
}
Process {
id: toggleBluetooth
@@ -22,7 +22,7 @@ QuickToggleButton {
altAction: () => {
Quickshell.execDetached(["easyeffects"])
Hyprland.dispatch("global quickshell:sidebarRightClose")
GlobalStates.sidebarRightOpen = false
}
Process {
@@ -17,7 +17,7 @@ QuickToggleButton {
}
altAction: () => {
Quickshell.execDetached(["bash", "-c", `${Network.ethernet ? Config.options.apps.networkEthernet : Config.options.apps.network}`])
Hyprland.dispatch("global quickshell:sidebarRightClose")
GlobalStates.sidebarRightOpen = false
}
Process {
id: toggleNetwork
@@ -1,41 +1,28 @@
import QtQuick
import qs.modules.common
import qs.modules.common.widgets
import qs
import qs.services
import Quickshell.Io
QuickToggleButton {
id: nightLightButton
property bool enabled: false
property bool enabled: Hyprsunset.active
toggled: enabled
buttonIcon: "nightlight"
buttonIcon: Config.options.light.night.automatic ? "night_sight_auto" : "bedtime"
onClicked: {
nightLightButton.enabled = !nightLightButton.enabled
if (enabled) {
nightLightOn.startDetached()
}
else {
nightLightOff.startDetached()
}
Hyprsunset.toggle()
}
Process {
id: nightLightOn
command: ["gammastep"]
altAction: () => {
Config.options.light.night.automatic = !Config.options.light.night.automatic
}
Process {
id: nightLightOff
command: ["pkill", "gammastep"]
}
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
}
}
Component.onCompleted: {
Hyprsunset.fetchState()
}
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 listBottomPadding: 80
Flickable {
StyledFlickable {
id: flickable
anchors.fill: parent
contentHeight: columnLayout.height
@@ -40,7 +40,7 @@ Item {
Item {
Layout.fillWidth: true
Layout.fillHeight: true
ListView {
StyledListView {
id: listView
model: root.appPwNodes
clip: true
@@ -187,7 +187,7 @@ Item {
Layout.rightMargin: dialogMargins
}
Flickable {
StyledFlickable {
id: dialogFlickable
Layout.fillWidth: true
clip: true
+1
View File
@@ -1,6 +1,7 @@
//@ pragma UseQApplication
//@ pragma Env QS_NO_RELOAD_POPUP=1
//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic
//@ pragma Env QT_QUICK_FLICKABLE_WHEEL_DECELERATION=10000
// Adjust this to make it smaller or larger
//@ 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/"
# else
# "$MATUGEN_DIR/scripts/least_busy_region.py" \
# --screen-width "$screen_width" --screen-height "$screen_height" \
# --width 300 --height 200 \
# "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
# --screen-width "$screen_width" --screen-height "$screen_height" \
# --width 300 --height 200 \
# "$wallpaper_path" > "$STATE_DIR"/user/generated/wallpaper/least_busy_region.json
# fi
}
@@ -85,18 +85,18 @@ check_and_prompt_upscale() {
fi
if [[ "$img_width" -lt "$min_width_desired" || "$img_height" -lt "$min_height_desired" ]]; then
action=$(notify-send "Upscale?" \
"Image resolution (${img_width}x${img_height}) is lower than screen resolution (${min_width_desired}x${min_height_desired})" \
-A "open_upscayl=Open Upscayl"\
"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 "Wallpaper switcher")
if [[ "$action" == "open_upscayl" ]]; then
if command -v upscayl &>/dev/null; then
nohup upscayl > /dev/null 2>&1 &
else
action2=$(notify-send \
-a "Wallpaper switcher" \
-c "im.error" \
-A "install_upscayl=Install Upscayl (Arch)" \
"Install Upscayl?" \
-a "Wallpaper switcher" \
-c "im.error" \
-A "install_upscayl=Install Upscayl (Arch)" \
"Install Upscayl?" \
"yay -S upscayl-bin")
if [[ "$action2" == "install_upscayl" ]]; then
kitty -1 yay -S upscayl-bin
@@ -110,15 +110,15 @@ check_and_prompt_upscale() {
fi
}
THUMBNAIL_DIR="/tmp/mpvpaper_thumbnails"
CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom"
RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts"
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() {
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() {
@@ -135,7 +135,7 @@ create_restore_script() {
pkill -f -9 mpvpaper
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
done
EOF
@@ -158,6 +158,13 @@ set_wallpaper_path() {
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() {
imgpath="$1"
mode_flag="$2"
@@ -197,10 +204,10 @@ switch() {
echo "Missing deps: ${missing_deps[*]}"
echo "Arch: sudo pacman -S ${missing_deps[*]}"
action=$(notify-send \
-a "Wallpaper switcher" \
-c "im.error" \
-A "install_arch=Install (Arch)" \
"Can't switch to video wallpaper" \
-a "Wallpaper switcher" \
-c "im.error" \
-A "install_arch=Install (Arch)" \
"Can't switch to video wallpaper" \
"Missing dependencies: ${missing_deps[*]}")
if [[ "$action" == "install_arch" ]]; then
kitty -1 sudo pacman -S "${missing_deps[*]}"
@@ -218,7 +225,7 @@ switch() {
local video_path="$imgpath"
monitors=$(hyprctl monitors -j | jq -r '.[] | .name')
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
done
@@ -226,6 +233,9 @@ switch() {
thumbnail="$THUMBNAIL_DIR/$(basename "$imgpath").jpg"
ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null
# Set thumbnail path
set_thumbnail_path "$thumbnail"
if [ -f "$thumbnail" ]; then
matugen_args=(image "$thumbnail")
generate_colors_material_args=(--path "$thumbnail")
+370 -361
View File
@@ -6,25 +6,55 @@ import qs.modules.common
import qs
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "./ai/"
/**
* 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 {
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 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 messageIDs: []
property var messageByID: ({})
readonly property var apiKeys: KeyringStorage.keyringData?.apiKeys ?? {}
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 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) {
// Generate a unique ID using timestamp and random value
@@ -32,7 +62,7 @@ Singleton {
}
function safeModelName(modelName) {
return modelName.replace(/:/g, "_").replace(/\./g, "_")
return modelName.replace(/:/g, "_").replace(/ /g, "-").replace(/\//g, "-")
}
property list<var> defaultPrompts: []
@@ -40,6 +70,171 @@ Singleton {
property list<var> promptFiles: [...defaultPrompts, ...userPrompts]
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:
// - name: 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_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".
// - 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.
property var models: {
"gemini-2.0-flash-search": {
"name": "Gemini 2.0 Flash (Search)",
"gemini-2.0-flash": aiModelComponent.createObject(this, {
"name": "Gemini 2.0 Flash",
"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",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
"model": "gemini-2.0-flash",
@@ -66,133 +260,60 @@ Singleton {
"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"),
"api_format": "gemini",
"tools": [
{
"google_search": {}
},
]
},
"gemini-2.0-flash-tools": {
"name": "Gemini 2.0 Flash (Tools)",
}),
"gemini-2.5-flash": aiModelComponent.createObject(this, {
"name": "Gemini 2.5 Flash",
"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",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash:streamGenerateContent",
"model": "gemini-2.0-flash",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:streamGenerateContent",
"model": "gemini-2.5-flash",
"requires_key": true,
"key_id": "gemini",
"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"),
"api_format": "gemini",
"tools": [
{
"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"]
}
},
]
}
]
},
"gemini-2.5-flash-search": {
"name": "Gemini 2.5 Flash (Search)",
}),
"gemini-2.5-flash-pro": aiModelComponent.createObject(this, {
"name": "Gemini 2.5 Pro",
"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",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent",
"model": "gemini-2.5-flash-preview-05-20",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:streamGenerateContent",
"model": "gemini-2.5-pro",
"requires_key": true,
"key_id": "gemini",
"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"),
"api_format": "gemini",
"tools": [
{
"google_search": ({})
},
]
},
"gemini-2.5-flash-tools": {
"name": "Gemini 2.5 Flash (Tools)",
}),
"gemini-2.5-flash-lite": aiModelComponent.createObject(this, {
"name": "Gemini 2.5 Flash-Lite",
"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",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-preview-05-20:streamGenerateContent",
"model": "gemini-2.5-flash-preview-05-20",
"endpoint": "https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash-lite:streamGenerateContent",
"model": "gemini-2.5-flash-lite",
"requires_key": true,
"key_id": "gemini",
"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"),
"api_format": "gemini",
"tools": [
{
"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"]
}
},
]
}
]
},
"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",
}),
"mistral-medium-3": aiModelComponent.createObject(this, {
"name": "Mistral Medium 3",
"icon": "mistral-symbolic",
"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"),
"homepage": "https://mistral.ai/news/mistral-medium-3",
"endpoint": "https://api.mistral.ai/v1/chat/completions",
"model": "mistral-medium-2505",
"requires_key": true,
"key_id": "openrouter",
"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"),
},
"openrouter-deepseek-r1": {
"key_id": "mistral",
"key_get_link": "https://console.mistral.ai/api-keys",
"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": aiModelComponent.createObject(this, {
"name": "DeepSeek R1",
"icon": "deepseek-symbolic",
"description": Translation.tr("Online via %1 | %2's model").arg("OpenRouter").arg("DeepSeek"),
@@ -203,11 +324,29 @@ Singleton {
"key_id": "openrouter",
"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"),
},
}),
}
property var modelList: Object.keys(root.models)
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: {
setModel(currentModelId, false, false); // Do necessary setup for model
}
@@ -233,6 +372,10 @@ Singleton {
return result;
}
function addModel(modelName, data) {
root.models[modelName] = aiModelComponent.createObject(this, data);
}
Process {
id: getOllamaModels
running: true
@@ -245,14 +388,15 @@ Singleton {
root.modelList = [...root.modelList, ...dataJson];
dataJson.forEach(model => {
const safeModelName = root.safeModelName(model);
root.models[safeModelName] = {
root.addModel(safeModelName, {
"name": guessModelName(model),
"icon": guessModelLogo(model),
"description": Translation.tr("Local Ollama model | %1").arg(model),
"homepage": `https://ollama.com/library/${model}`,
"endpoint": "http://localhost:11434/v1/chat/completions",
"model": model,
}
"requires_key": false,
})
});
root.modelList = Object.keys(root.models);
@@ -350,8 +494,8 @@ Singleton {
function addApiKeyAdvice(model) {
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')
.arg(model.name).arg(model.key_get_link).arg(model.key_get_description ?? Translation.tr("<i>No further instruction provided</i>")),
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("/key"),
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```"
}
}
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() {
return root.temperature;
@@ -438,24 +591,16 @@ Singleton {
function clearMessages() {
root.messageIDs = [];
root.messageByID = ({});
root.tokenCount.input = -1;
root.tokenCount.output = -1;
root.tokenCount.total = -1;
}
Process {
id: requester
property var baseCommand: ["bash", "-c"]
property var message
property bool isReasoning
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;
}
property list<string> baseCommand: ["bash", "-c"]
property AiMessageData message
property ApiStrategy currentStrategy
function markDone() {
requester.message.done = true;
@@ -466,82 +611,20 @@ Singleton {
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() {
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 */
if (model.requires_key) requester.environment[`${root.apiKeyEnvVarName}`] = root.apiKeys ? (root.apiKeys[model.key_id] ?? "") : ""
/* 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 data = (apiFormat === "gemini") ? buildGeminiRequestData(model, messageArray) : buildOpenAIRequestData(model, messageArray);
// console.log("REQUEST DATA: ", JSON.stringify(data, null, 2));
const filteredMessageArray = messageArray.filter(message => message.role !== Ai.interfaceRole);
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 = {
"Content-Type": "application/json",
@@ -569,153 +652,46 @@ Singleton {
// console.log("Request headers: ", JSON.stringify(requestHeaders));
// console.log("Header string: ", headerString);
/* Get authorization header from strategy */
const authHeader = requester.currentStrategy.buildAuthorizationHeader(root.apiKeyEnvVarName);
/* Create command string */
const requestCommandString = `curl --no-buffer "${endpoint}"`
+ ` ${headerString}`
+ ((apiFormat == "gemini") ? "" : ` -H "Authorization: Bearer \$\{${root.apiKeyEnvVarName}\}"`)
+ (authHeader ? ` ${authHeader}` : "")
+ ` -d '${CF.StringUtils.shellSingleQuoteEscape(JSON.stringify(data))}'`
// console.log("Request command: ", requestCommandString);
/* Send the request */
requester.command = baseCommand.concat([requestCommandString]);
/* Reset vars and make the request */
requester.isReasoning = false
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 {
onRead: data => {
// console.log("RAW DATA: ", data);
if (data.length === 0) return;
if (requester.message.thinking) requester.message.thinking = false;
// console.log("[Ai] Raw response line: ", data);
// Handle response line
if (requester.message.thinking) requester.message.thinking = false;
try {
if (requester.apiFormat === "gemini") {
requester.handleGeminiResponseLine(data);
const result = requester.currentStrategy.parseResponseLine(data, requester.message);
// 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") {
requester.handleOpenAIResponseLine(data);
if (result.tokenUsage) {
root.tokenCount.input = result.tokenUsage.input;
root.tokenCount.output = result.tokenUsage.output;
root.tokenCount.total = result.tokenUsage.total;
}
else {
console.log("Unknown API format: ", requester.apiFormat);
requester.message.rawContent += data;
requester.message.content += data;
if (result.finished) {
requester.markDone();
}
} 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.content += data;
}
@@ -723,18 +699,15 @@ Singleton {
}
onExited: (exitCode, exitStatus) => {
if (requester.apiFormat == "gemini") requester.parseGeminiBuffer();
else requester.markDone();
try { // to parse full response into json for error handling
// console.log("Full response: ", requester.message.content + "]");
const parsedResponse = JSON.parse(requester.message.rawContent + "]");
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);
const result = requester.currentStrategy.onRequestFinished(requester.message);
if (result.finished) {
requester.markDone();
} else if (!requester.message.done) {
requester.markDone();
}
// Handle error responses
if (requester.message.content.includes("API key not valid")) {
root.addApiKeyAdvice(models[requester.message.model]);
}
@@ -747,45 +720,72 @@ Singleton {
requester.makeRequest();
}
function addFunctionOutputMessage(name, output) {
const aiMessage = aiMessageComponent.createObject(root, {
function createFunctionOutputMessage(name, output, includeOutputInChat = true) {
return aiMessageComponent.createObject(root, {
"role": "user",
"content": `[[ Output of ${name} ]]`,
"rawContent": `[[ Output of ${name} ]]`,
"content": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
"rawContent": `[[ Output of ${name} ]]${includeOutputInChat ? ("\n\n<think>\n" + output + "\n</think>") : ""}`,
"functionName": name,
"functionResponse": output,
"thinking": false,
"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);
root.messageIDs = [...root.messageIDs, id];
root.messageByID[id] = aiMessage;
}
function buildGeminiFunctionOutput(name, output) {
const functionResponsePart = {
"name": name,
"response": { "content": output }
function rejectCommand(message: AiMessageData) {
if (!message.functionPending) return;
message.functionPending = false; // User decided, no more "thinking"
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 {
"role": "user",
"parts": [{
functionResponse: functionResponsePart,
}]
onExited: (exitCode, exitStatus) => {
commandExecutionProc.message.functionResponse += `[[ Command exited with code ${exitCode} (${exitStatus}) ]]\n`;
requester.makeRequest(); // Continue
}
}
function handleGeminiFunctionCall(name, args) {
function handleFunctionCall(name, args: var, message: AiMessageData) {
if (name === "switch_to_search_mode") {
if (root.currentModelId === "gemini-2.5-flash-tools") {
root.setModel("gemini-2.5-flash-search", false);
root.postResponseHook = () => root.setModel("gemini-2.5-flash-tools", false);
} 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);
}
const modelId = root.currentModelId;
root.currentTool = "search"
root.postResponseHook = () => { root.currentTool = "functions" }
addFunctionOutputMessage(name, Translation.tr("Switched to search mode. Continue with the user's request."))
requester.makeRequest();
} else if (name === "get_shell_config") {
@@ -800,6 +800,15 @@ Singleton {
const key = args.key;
const value = args.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");
}
+1 -2
View File
@@ -1,8 +1,7 @@
pragma Singleton
import qs.modules.common
import "root:/modules/common/functions/fuzzysort.js" as Fuzzy
import "root:/modules/common/functions/levendist.js" as Levendist
import qs.modules.common.functions
import Quickshell
/**
@@ -1,7 +1,7 @@
pragma Singleton
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
import Quickshell
@@ -1,8 +1,6 @@
pragma Singleton
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.functions
import QtQuick
+4 -5
View File
@@ -9,16 +9,15 @@ pragma ComponentBehavior: Bound
* A nice wrapper for date and time strings.
*/
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 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 uptime: "0h, 0m"
SystemClock {
id: clock
precision: SystemClock.Minutes
}
Timer {
interval: 10
running: true
+1 -2
View File
@@ -1,9 +1,8 @@
pragma Singleton
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.functions
import QtQuick
import Quickshell
import Quickshell.Io
@@ -12,7 +12,7 @@ Singleton {
property string firstRunNotifSummary: "Welcome!"
property string firstRunNotifBody: "Hit Super+/ for a list of keybinds"
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() {
firstRunFileView.reload()
+25 -20
View File
@@ -69,10 +69,11 @@ Singleton {
Process {
id: getClients
command: ["bash", "-c", "hyprctl clients -j | jq -c"]
stdout: SplitParser {
onRead: data => {
root.windowList = JSON.parse(data);
command: ["bash", "-c", "hyprctl clients -j"]
stdout: StdioCollector {
id: clientsCollector
onStreamFinished: {
root.windowList = JSON.parse(clientsCollector.text)
let tempWinByAddress = {};
for (var i = 0; i < root.windowList.length; ++i) {
var win = root.windowList[i];
@@ -86,30 +87,33 @@ Singleton {
Process {
id: getMonitors
command: ["bash", "-c", "hyprctl monitors -j | jq -c"]
stdout: SplitParser {
onRead: data => {
root.monitors = JSON.parse(data);
command: ["bash", "-c", "hyprctl monitors -j"]
stdout: StdioCollector {
id: monitorsCollector
onStreamFinished: {
root.monitors = JSON.parse(monitorsCollector.text);
}
}
}
Process {
id: getLayers
command: ["bash", "-c", "hyprctl layers -j | jq -c"]
stdout: SplitParser {
onRead: data => {
root.layers = JSON.parse(data);
command: ["bash", "-c", "hyprctl layers -j"]
stdout: StdioCollector {
id: layersCollector
onStreamFinished: {
root.layers = JSON.parse(layersCollector.text);
}
}
}
Process {
id: getWorkspaces
command: ["bash", "-c", "hyprctl workspaces -j | jq -c"]
stdout: SplitParser {
onRead: data => {
root.workspaces = JSON.parse(data);
command: ["bash", "-c", "hyprctl workspaces -j"]
stdout: StdioCollector {
id: workspacesCollector
onStreamFinished: {
root.workspaces = JSON.parse(workspacesCollector.text);
let tempWorkspaceById = {};
for (var i = 0; i < root.workspaces.length; ++i) {
var ws = root.workspaces[i];
@@ -123,10 +127,11 @@ Singleton {
Process {
id: getActiveWorkspace
command: ["bash", "-c", "hyprctl activeworkspace -j | jq -c"]
stdout: SplitParser {
onRead: data => {
root.activeWorkspace = JSON.parse(data);
command: ["bash", "-c", "hyprctl activeworkspace -j"]
stdout: StdioCollector {
id: activeWorkspaceCollector
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