forked from Shinonome/dots-hyprland
Compare commits
259 Commits
octo-overlord
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 59d1b4a337 | |||
| 151034f4fc | |||
| eb226350fc | |||
| ae081803a5 | |||
| 8cba405b0e | |||
| c61fe63bae | |||
| c8d0cec782 | |||
| a02fd32ee2 | |||
| 6be0a483e8 | |||
| 3c32424793 | |||
| 5535d43146 | |||
| 4f5b41204c | |||
| 6335cdce16 | |||
| ce86247d3e | |||
| 580865cb4c | |||
| 8b4cf2c91d | |||
| ee37fc5f6d | |||
| 7a5635507e | |||
| 609720f540 | |||
| bec280bcbc | |||
| 1dc5e26712 | |||
| c0ad3a4acb | |||
| 33dc50df7a | |||
| 604108a093 | |||
| b5960f4363 | |||
| 1654ba6ee4 | |||
| b1484ba42e | |||
| 76edab5b1c | |||
| c5bb2bf66f | |||
| 624606d853 | |||
| be5772ea09 | |||
| e6dd387e9f | |||
| 84ac1c05e2 | |||
| e1ac8eeed1 | |||
| 415806ca96 | |||
| 650a5fa73c | |||
| 3c496175dc | |||
| b1daadc71e | |||
| aae0668878 | |||
| 7495f5ebbc | |||
| d9a4fba05a | |||
| 64cf3883aa | |||
| 51d1d30a61 | |||
| c7e43f10e3 | |||
| 93bcfd494c | |||
| b76a1e45ef | |||
| 527a8f2b75 | |||
| 1d57900977 | |||
| 6fd73b8691 | |||
| 40dbb31fbc | |||
| d729f27755 | |||
| a270c99361 | |||
| dc379f2ee6 | |||
| 992e54592b | |||
| c502d301b8 | |||
| 58b144792e | |||
| c7d6a04c77 | |||
| a29382966b | |||
| 9d2d722bc6 | |||
| 490fb9ba35 | |||
| 5e7bef47a0 | |||
| 891cad49f0 | |||
| d6c4327639 | |||
| fed7deef79 | |||
| d4b1e53471 | |||
| 40f9981167 | |||
| 1902d47c1e | |||
| ae331f28cf | |||
| 41f93617ad | |||
| ed8b13037d | |||
| 7d0d6fbd9f | |||
| dcb26342b3 | |||
| 09a70bf980 | |||
| ccf5c9a252 | |||
| 06e277c316 | |||
| dc1c238819 | |||
| 8c5f3537f9 | |||
| 2348290bfb | |||
| 7b047e0719 | |||
| bce41fec66 | |||
| a0d060a8c3 | |||
| c83efbc591 | |||
| 230917fbb2 | |||
| 0b3db4e61a | |||
| 1a1a2b309f | |||
| f9129a5fb7 | |||
| 166d9f3f4b | |||
| cdf0188ec8 | |||
| 99638ade76 | |||
| 33953a915b | |||
| 3ad6d635d6 | |||
| f0ec76847f | |||
| a6c6f7958f | |||
| 4dc566a037 | |||
| 55d2a9fe6c | |||
| db7d0a00f0 | |||
| b9a7145c5c | |||
| f94de758d2 | |||
| a34c685b73 | |||
| 16c5b8f999 | |||
| 181014b678 | |||
| 083be17af4 | |||
| f0d7d6f074 | |||
| d561e4f1ec | |||
| 686de93cee | |||
| 1493021354 | |||
| 48d7be6682 | |||
| a9c40d493b | |||
| 3d36ead298 | |||
| 9b1de7c7ce | |||
| ce6e885a23 | |||
| 176ad4ba0b | |||
| 9a1ad0057e | |||
| 2327596517 | |||
| 71b67cc772 | |||
| 962934e82d | |||
| 1528a9f760 | |||
| 28be4d1bb4 | |||
| ad7ee4ad27 | |||
| db66b85e61 | |||
| 66c810ead2 | |||
| 9824bb9c63 | |||
| f806e2c22c | |||
| 3d408b18f7 | |||
| 8aa776ae62 | |||
| a15f3b8c65 | |||
| 4df22c96d0 | |||
| 772df06fa5 | |||
| d3a9d2ea5b | |||
| 4914d9b638 | |||
| 1f8a7be34e | |||
| 97bdfa54c0 | |||
| 64bb730dd1 | |||
| 7013b459a3 | |||
| a31733e2db | |||
| 199b23d14a | |||
| f1c1ed833c | |||
| 0506917b87 | |||
| 4f40ba8e6e | |||
| 733a792610 | |||
| f581fd4821 | |||
| 86ddb61a3f | |||
| 6c3451b912 | |||
| 35e1dc95a5 | |||
| d70f81bfe4 | |||
| d632111cf9 | |||
| f8d162d995 | |||
| 0708070764 | |||
| 87f7bc28a3 | |||
| 3eb7d8ab58 | |||
| 71d0ac4c5e | |||
| 839593b11e | |||
| 13a0927900 | |||
| 00984c599b | |||
| 34ca65a180 | |||
| 596ae72942 | |||
| 88cc91b85a | |||
| d4b8ded6c8 | |||
| 86d2a03a0a | |||
| de1812bf91 | |||
| f36751ff6b | |||
| a9273fc225 | |||
| 2a0b12112f | |||
| 2aea02989f | |||
| 2b554cf286 | |||
| dc2777703d | |||
| 6ae03b545c | |||
| 8e366cfc84 | |||
| 27c2c4fb92 | |||
| 83af589b27 | |||
| 7a937833f3 | |||
| 4110d2529c | |||
| 1c6c165d78 | |||
| a5ffb0e021 | |||
| a08a39b620 | |||
| 968e8195ef | |||
| 52ce2f5384 | |||
| cb2d1bc444 | |||
| 47b81faf3d | |||
| 1483761e72 | |||
| 7f43665e3c | |||
| 01fcd653ad | |||
| 298e947740 | |||
| 91c2014b7e | |||
| 3018ad16b1 | |||
| 1172be241c | |||
| c743b4ab88 | |||
| f6ec718ced | |||
| aa20027de4 | |||
| e504cf11e1 | |||
| a11e0a39d9 | |||
| 26531401b0 | |||
| 0f4293e4cb | |||
| 7172b134ea | |||
| 4a9e342a1c | |||
| f98d869c21 | |||
| 1312310a6e | |||
| ad9c81f405 | |||
| 496caa6fb1 | |||
| 2fd7d45b9c | |||
| 0b087665a8 | |||
| 39862fba2a | |||
| 3ac44d211f | |||
| d3392000af | |||
| 564d2e109f | |||
| fe07298adb | |||
| cc176a999d | |||
| 47c5a41aa6 | |||
| 2ad6f2c9fc | |||
| 8905bc1c27 | |||
| 064d5174c2 | |||
| c69c8f6ef5 | |||
| 7fb81049f3 | |||
| 5099ce15db | |||
| ed500395d3 | |||
| a1e88fc3c2 | |||
| c8b007631d | |||
| 6bc1f8a39f | |||
| fe84f6cab1 | |||
| 27eea1c7a6 | |||
| 32f94704c7 | |||
| 05fdbf3d24 | |||
| f28c791cf2 | |||
| a4b474ff39 | |||
| 38c76fe86b | |||
| d09259c79a | |||
| a683fa2414 | |||
| e744816928 | |||
| 15703bce04 | |||
| f4f5540d08 | |||
| b1b37685c1 | |||
| 0ff4cc572c | |||
| 081b9c17d5 | |||
| eb6b21e7e6 | |||
| baa17c304b | |||
| 7b8b388667 | |||
| 118529d8d3 | |||
| b67c4553f6 | |||
| 47980da78e | |||
| 3d57d444df | |||
| 5870632c19 | |||
| ffeb27f04e | |||
| 012df9dcd7 | |||
| 82fd2334cf | |||
| 7bafa57989 | |||
| 5b4ccd9d59 | |||
| be2b86909a | |||
| 82506ae7cd | |||
| 574a2a11e7 | |||
| 3d5ed9401c | |||
| 3b5a674409 | |||
| f9856bdabd | |||
| b6f75acf53 | |||
| c0f7504b36 | |||
| 365a649776 | |||
| 0ecf72b4c3 | |||
| 90013c7451 | |||
| 715aa8d845 | |||
| ad7fdd1d3f |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}`
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,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
@@ -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;
|
||||
};
|
||||
}
|
||||
);
|
||||
}
|
||||
@@ -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")
|
||||
|
||||
@@ -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,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
|
||||
|
||||
@@ -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,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()
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user