1 Commits

Author SHA1 Message Date
end-4 f7b46516b5 stuff 2025-07-27 08:52:38 +07:00
165 changed files with 4976 additions and 6389 deletions
-3
View File
@@ -1,5 +1,2 @@
# You can make apps auto-start here # You can make apps auto-start here
# Relevant Hyprland wiki section: https://wiki.hyprland.org/Configuring/Keywords/#executing # Relevant Hyprland wiki section: https://wiki.hyprland.org/Configuring/Keywords/#executing
exec-once = solaar -w hide
exec-once = bluetoothctl power on
# exec-once = lact daemon
+1 -5
View File
@@ -1,6 +1,2 @@
# Put general config stuff here # Put general config stuff here
# Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/ # Here's a list of every variable: https://wiki.hyprland.org/Configuring/Variables/
# monitor=DP-2, highres@180,0x1080,1,bitdepth,10,cm,hdr,sdrbrightness,1.4,sdrsaturation,0.98
monitor=DP-1,3440x1440@180,0x1080,1,bitdepth,10,cm,hdr,sdrbrightness,1.4,sdrsaturation,0.98,vrr,0
monitor=DP-2, highres@highrr,760x0,1
monitor=HDMI-A-1, 1920x1080@120, 3440x1440, 1
-31
View File
@@ -9,34 +9,3 @@ bind = Ctrl+Super+Alt, Slash, exec, xdg-open ~/.config/hypr/custom/keybinds.conf
# Use ##! to add a section in that column # Use ##! to add a section in that column
# Add a comment after a bind to add a description, like above # Add a comment after a bind to add a description, like above
bind = Super, H, movefocus, l # [hidden]
bind = Super, L, movefocus, r # [hidden]
bind = Super, K, movefocus, u # [hidden]
bind = Super, J, movefocus, d # [hidden]
bind = Super+Shift, H, movewindow, l # [hidden]
bind = Super+Shift, L, movewindow, r # [hidden]
bind = Super+Shift, K, movewindow, u # [hidden]
bind = Super+Shift, J, movewindow, d # [hidden]
bind = Super+Shift, 1, movetoworkspace, 1 # [hidden]
bind = Super+Shift, 2, movetoworkspace, 2 # [hidden]
bind = Super+Shift, 3, movetoworkspace, 3 # [hidden]
bind = Super+Shift, 4, movetoworkspace, 4 # [hidden]
bind = Super+Shift, 5, movetoworkspace, 5 # [hidden]
bind = Super+Shift, 6, movetoworkspace, 6 # [hidden]
bind = Super+Shift, 7, movetoworkspace, 7 # [hidden]
bind = Super+Shift, 8, movetoworkspace, 8 # [hidden]
bind = Super+Shift, 9, movetoworkspace, 9 # [hidden]
bind = Super+Shift, 0, movetoworkspace, 0 # [hidden]
bindd = Super+Ctrl, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
bindd = Super+Ctrl, J, Toggle bar, global, quickshell:barToggle # Toggle bar
# gaming
bind = Super, G, togglespecialworkspace, gaming
bind = Super+Shift, G, movetoworkspace, special:gaming
bind = Super, T, togglespecialworkspace, steam
bind = Super+Shift, T, movetoworkspace, special:steam
-6
View File
@@ -1,9 +1,3 @@
# You can put custom rules here # You can put custom rules here
# Window/layer rules: https://wiki.hyprland.org/Configuring/Window-Rules/ # Window/layer rules: https://wiki.hyprland.org/Configuring/Window-Rules/
# Workspace rules: https://wiki.hyprland.org/Configuring/Workspace-Rules/ # Workspace rules: https://wiki.hyprland.org/Configuring/Workspace-Rules/
windowrule = workspace special:steam, class:steam
windowrule = workspace special:gaming, class:^(steam_app_).*
workspace = special:gaming, monitor:DP-1, persistent:true
workspace = special:steam, monitor:DP-1, persistent:true, on-created-empty:steam
-3
View File
@@ -22,6 +22,3 @@ env = XDG_MENU_PREFIX, plasma-
# ######## Virtual envrionment ######### # ######## Virtual envrionment #########
env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv env = ILLOGICAL_IMPULSE_VIRTUAL_ENV, ~/.local/state/quickshell/.venv
# ######## Terminal application #########
env = TERMINAL,kitty -1
+1 -1
View File
@@ -1,5 +1,5 @@
# Bar, wallpaper # Bar, wallpaper
exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh exec-once = ~/.config/hypr/hyprland/scripts/start_geoclue_agent.sh & gammastep
exec-once = qs -c $qsConfig & exec-once = qs -c $qsConfig &
# Input method # Input method
+1 -1
View File
@@ -138,7 +138,7 @@ misc {
swallow_regex = (foot|kitty|allacritty|Alacritty) swallow_regex = (foot|kitty|allacritty|Alacritty)
new_window_takes_over_fullscreen = 2 new_window_takes_over_fullscreen = 2
allow_session_lock_restore = true allow_session_lock_restore = true
# session_lock_xray = true session_lock_xray = true
initial_workspace_tracking = false initial_workspace_tracking = false
focus_on_activate = true focus_on_activate = true
} }
+12 -12
View File
@@ -26,11 +26,11 @@ bind = Super+Alt, A, global, quickshell:sidebarLeftToggleDetach # [hidden]
bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden] bind = Super, B, global, quickshell:sidebarLeftToggle # [hidden]
bind = Super, O, global, quickshell:sidebarLeftToggle # [hidden] bind = Super, O, global, quickshell:sidebarLeftToggle # [hidden]
bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar bindd = Super, N, Toggle right sidebar, global, quickshell:sidebarRightToggle # Toggle right sidebar
bindd = Super, Slash, Toggle cheatsheet, global, quickshell:ch+CtrleatsheetToggle # Toggle cheatsheet 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, K, Toggle on-screen keyboard, global, quickshell:oskToggle # Toggle on-screen keyboard
bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls bindd = Super, M, Toggle media controls, global, quickshell:mediaControlsToggle # Toggle media controls
bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu bindd = Ctrl+Alt, Delete, Toggle session menu, global, quickshell:sessionToggle # Toggle session menu
# bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar bindd = Super, J, Toggle bar, global, quickshell:barToggle # Toggle bar
bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback) bind = Ctrl+Alt, Delete, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill wlogout || wlogout -p layer-shell # [hidden] Session menu (fallback)
bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/$qsConfig/welcome.qml # [hidden] Launch welcome app bind = Shift+Super+Alt, Slash, exec, qs -p ~/.config/quickshell/$qsConfig/welcome.qml # [hidden] Launch welcome app
@@ -51,9 +51,9 @@ bind = Ctrl+Super, R, exec, killall ags agsv1 gjs ydotool qs quickshell; qs -c $
# Screenshot, Record, OCR, Color picker, Clipboard history # Screenshot, Record, OCR, Color picker, Clipboard history
bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback) bindd = Super, V, Copy clipboard history entry, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || cliphist list | fuzzel --match-mode fzf --dmenu | cliphist decode | wl-copy # [hidden] Clipboard history >> clipboard (fallback)
bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback) bindd = Super, Period, Copy an emoji, exec, qs -c $qsConfig ipc call TEST_ALIVE || pkill fuzzel || ~/.config/hypr/hyprland/scripts/fuzzel-emoji.sh copy # [hidden] Emoji >> clipboard (fallback)
bindd = Super+Shift, S, Screen snip, exec, hyprshot --clipboard-only --mode region --silent || qs -p ~/.config/quickshell/$qsConfig/screenshot.qml || pidof slurp # Screen snip 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
# OCR # OCR
bindd = Super+Shift, E, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden] bindd = Super+Shift, T, Character recognition,exec,grim -g "$(slurp $SLURP_ARGS)" "tmp.png" && tesseract "tmp.png" - | wl-copy && rm "tmp.png" # [hidden]
# Color picker # Color picker
bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard bindd = Super+Shift, C, Color picker, exec, hyprpicker -a # Pick color (Hex) >> clipboard
# Fullscreen screenshot # Fullscreen screenshot
@@ -177,9 +177,9 @@ bind = Super+Alt, f12, exec, bash -c 'RANDOM_IMAGE=$(find ~/Pictures -type f | g
bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden] bind = Super+Alt, Equal, exec, notify-send "Urgent notification" "Ah hell no" -u critical -a 'Hyprland keybind' # [hidden]
##! Session ##! Session
# bindd = Super, L, Lock, exec, loginctl lock-session # Lock bindd = Super, L, Lock, exec, loginctl lock-session # Lock
# bind = Super+Shift, L, exec, loginctl lock-session # [hidden] bind = Super+Shift, L, exec, loginctl lock-session # [hidden]
# bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep bindld = Super+Shift, L, Suspend system, exec, sleep 0.1 && systemctl suspend || loginctl suspend # Sleep
bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off bindd = Ctrl+Shift+Alt+Super, Delete, Shutdown, exec, systemctl poweroff || loginctl poweroff # [hidden] Power off
##! Screen ##! Screen
@@ -201,10 +201,10 @@ bindl= ,XF86AudioPlay, exec, playerctl play-pause # [hidden]
bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden] bindl= ,XF86AudioPause, exec, playerctl play-pause # [hidden]
##! Apps ##! Apps
bind = Super, Return, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # Terminal 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 "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (alt) 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 "$TERMINAL" "kitty -1" "foot" "alacritty" "wezterm" "konsole" "kgx" "uxterm" "xterm" # [hidden] (terminal) (for Ubuntu people) 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" "$TERMINAL" "kitty -1 fish -c yazi" # File manager 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, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser bind = Super, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "google-chrome-stable" "zen-browser" "firefox" "brave" "chromium" "microsoft-edge-stable" "opera" "librewolf" # Browser
bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor bind = Super, C, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "code" "codium" "cursor" "zed" "zedit" "zeditor" "kate" "gnome-text-editor" "emacs" "command -v nvim && kitty -1 nvim" # Code editor
bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software bind = Super+Shift, W, exec, ~/.config/hypr/hyprland/scripts/launch_first_available.sh "wps" "onlyoffice-desktopeditors" # Office software
@@ -1,7 +1,7 @@
#!/usr/bin/env bash #!/usr/bin/env bash
for cmd in "$@"; do for cmd in "$@"; do
[[ -z "$cmd" ]] && continue
eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue eval "command -v ${cmd%% *}" >/dev/null 2>&1 || continue
eval "$cmd" & eval "$cmd" &
exit exit
done done
exit 1
+5 -11
View File
@@ -8,6 +8,11 @@ $font_material_symbols = Material Symbols Rounded
background { background {
color = rgba(181818FF) color = rgba(181818FF)
# path = {{ SWWW_WALL }}
# path = screenshot
# blur_size = 15
# blur_passes = 4
} }
input-field { input-field {
monitor = monitor =
@@ -25,17 +30,6 @@ input-field {
valign = center valign = center
} }
label {
monitor =
text = $LAYOUT
color = $text_color
font_size = 14
font_family = $font_family
position = -30, 30
halign = right
valign = bottom
}
label { # Caps Lock Warning label { # Caps Lock Warning
monitor = monitor =
text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh
-11
View File
@@ -1,11 +0,0 @@
[Desktop Entry]
DefaultProfile=Profile 1.profile
[General]
ConfigVersion=1
[KonsoleWindow]
UseSingleInstance=true
[UiSettings]
ColorScheme=
@@ -30,17 +30,6 @@ input-field {
valign = center valign = center
} }
label {
monitor =
text = $LAYOUT
color = $text_color
font_size = 14
font_family = $font_family
position = -30, 30
halign = right
valign = bottom
}
label { # Caps Lock Warning label { # Caps Lock Warning
monitor = monitor =
text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh text = cmd[update:250] ${XDG_CONFIG_HOME:-$HOME/.config}/hypr/hyprlock/check-capslock.sh
+20 -10
View File
@@ -12,17 +12,11 @@ Singleton {
property bool barOpen: true property bool barOpen: true
property bool sidebarLeftOpen: false property bool sidebarLeftOpen: false
property bool sidebarRightOpen: false property bool sidebarRightOpen: false
property bool mediaControlsOpen: false
property bool osdBrightnessOpen: false
property bool osdVolumeOpen: false
property bool oskOpen: false
property bool overviewOpen: false property bool overviewOpen: false
property bool workspaceShowNumbers: false
property bool superReleaseMightTrigger: true
property bool screenLocked: false property bool screenLocked: false
property bool screenLockContainsCharacters: false property bool screenLockContainsCharacters: false
property bool sessionOpen: false
property bool superDown: false
property bool superReleaseMightTrigger: true
property bool workspaceShowNumbers: false
property real screenZoom: 1 property real screenZoom: 1
onScreenZoomChanged: { onScreenZoomChanged: {
@@ -32,15 +26,31 @@ Singleton {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
} }
// When user is not reluctant while pressing super, they probably don't need to see workspace numbers
onSuperReleaseMightTriggerChanged: {
workspaceShowNumbersTimer.stop()
}
Timer {
id: workspaceShowNumbersTimer
interval: Config.options.bar.workspaces.showNumberDelay
// interval: 0
repeat: false
onTriggered: {
workspaceShowNumbers = true
}
}
GlobalShortcut { GlobalShortcut {
name: "workspaceNumber" name: "workspaceNumber"
description: "Hold to show workspace numbers, release to show icons" description: "Hold to show workspace numbers, release to show icons"
onPressed: { onPressed: {
root.superDown = true workspaceShowNumbersTimer.start()
} }
onReleased: { onReleased: {
root.superDown = false workspaceShowNumbersTimer.stop()
workspaceShowNumbers = false
} }
} }
@@ -1,95 +0,0 @@
<?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>

Before

Width:  |  Height:  |  Size: 2.8 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 33 KiB

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

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