diff options
48 files changed, 4483 insertions, 13 deletions
diff --git a/config/hypr/UserConfigs/Startup_Apps.conf b/config/hypr/UserConfigs/Startup_Apps.conf index 702f9a90..91ff9cd0 100644 --- a/config/hypr/UserConfigs/Startup_Apps.conf +++ b/config/hypr/UserConfigs/Startup_Apps.conf @@ -30,6 +30,7 @@ exec-once = swaync #exec-once = blueman-applet #exec-once = rog-control-center exec-once = waybar +exec-once = qs #clipboard manager exec-once = wl-paste --type text --watch cliphist store diff --git a/config/hypr/UserConfigs/UserKeybinds.conf b/config/hypr/UserConfigs/UserKeybinds.conf index 871f2ffb..c92352fc 100644 --- a/config/hypr/UserConfigs/UserKeybinds.conf +++ b/config/hypr/UserConfigs/UserKeybinds.conf @@ -18,7 +18,7 @@ source= $UserConfigs/01-UserDefaults.conf #bindr = $mainMod, $mainMod_L, exec, pkill rofi || rofi -show drun -modi drun,filebrowser,run,window # Super Key to Launch rofi menu bind = $mainMod, D, exec, pkill rofi || true && rofi -show drun -modi drun,filebrowser,run,window # Main Menu (APP Launcher) bind = $mainMod, B, exec, xdg-open "https://" # default browser -bind = $mainMod, A, exec, pkill rofi || true && ags -t 'overview' # desktop overview (if installed) +bind = $mainMod, A, global, quickshell:overviewToggle # desktop overview (if installed) bind = $mainMod, Return, exec, $term #terminal bind = $mainMod, E, exec, $files #file manager @@ -39,7 +39,7 @@ bind = $mainMod SHIFT, F, fullscreen # whole full screen bind = $mainMod CTRL, F, fullscreen, 1 # fake full screen bind = $mainMod, SPACE, togglefloating, #Float Mode bind = $mainMod ALT, SPACE, exec, hyprctl dispatch workspaceopt allfloat #All Float Mode -bind = $mainMod SHIFT, Return, exec, [float; move 15% 5%; size 70% 60%] $term # Dropdown terminal +bind = $mainMod SHIFT, Return, exec, $scriptsDir/Dropterminal.sh $term # Dropdown terminal # Desktop zooming or magnifier bind = $mainMod ALT, mouse_down, exec, hyprctl keyword cursor:zoom_factor "$(hyprctl getoption cursor:zoom_factor | awk 'NR==1 {factor = $2; if (factor < 1) {factor = 1}; print factor * 2.0}')" diff --git a/config/hypr/UserConfigs/WindowRules.conf b/config/hypr/UserConfigs/WindowRules.conf index 661b3a17..4485a8f5 100644 --- a/config/hypr/UserConfigs/WindowRules.conf +++ b/config/hypr/UserConfigs/WindowRules.conf @@ -44,7 +44,7 @@ windowrule = tag +screenshare, class:^(com.obsproject.Studio)$ windowrule = tag +im, class:^([Dd]iscord|[Ww]ebCord|[Vv]esktop)$ windowrule = tag +im, class:^([Ff]erdium)$ windowrule = tag +im, class:^([Ww]hatsapp-for-linux)$ -windowrule = tag +im, class:^(ZapZap|com.rtosta.zapzap)$ +windowrule = tag +im, class:^(ZapZap|com.rtosta.zapzap)$ windowrule = tag +im, class:^(org.telegram.desktop|io.github.tdesktop_x64.TDesktop)$ windowrule = tag +im, class:^(teams-for-linux)$ @@ -211,6 +211,10 @@ layerrule = blur, rofi layerrule = ignorezero, rofi layerrule = blur, notifications layerrule = ignorezero, notifications +layerrule = blur, quickshell:overview +layerrule = ignorezero, quickshell:overview +layerrule = ignorealpha 0.5, quickshell:overview + #layerrule = ignorealpha 0.5, tag:notif* #layerrule = ignorezero, class:^([Rr]ofi)$ @@ -219,4 +223,4 @@ layerrule = ignorezero, notifications #layerrule = ignorezero, <rofi> #layerrule = ignorezero, overview -#layerrule = blur, overview
\ No newline at end of file +#layerrule = blur, overview diff --git a/config/hypr/hyprlock-1080p.conf b/config/hypr/hyprlock-1080p.conf index a0e105e0..14f2f35e 100644 --- a/config/hypr/hyprlock-1080p.conf +++ b/config/hypr/hyprlock-1080p.conf @@ -156,6 +156,18 @@ label { valign = bottom } +# battery information +label { + monitor = + text = cmd[update:1000] echo "<b> "$($Scripts/Battery.sh)" </b>" + color = $color13 + font_size = 16 + font_family = Victor Mono Bold Oblique + position = 0, 30 + halign = right + valign = bottom +} + # weather edit the scripts for locations # weather scripts are located in ~/.config/hypr/UserScripts Weather.sh and/or Weather.py # see https://github.com/JaKooLit/Hyprland-Dots/wiki/TIPS#%EF%B8%8F-weather-app-related-for-waybar-and-hyprlock diff --git a/config/hypr/hyprlock.conf b/config/hypr/hyprlock.conf index 48b09b85..b67bba51 100644 --- a/config/hypr/hyprlock.conf +++ b/config/hypr/hyprlock.conf @@ -157,6 +157,18 @@ label { valign = bottom } +# battery information +label { + monitor = + text = cmd[update:1000] echo "<b> "$($Scripts/Battery.sh)" </b>" + color = $color13 + font_size = 18 + font_family = Victor Mono Bold Oblique + position = 0, 30 + halign = right + valign = bottom +} + # weather edit the scripts for locations # weather scripts are located in ~/.config/hypr/UserScripts Weather.sh and/or Weather.py # see https://github.com/JaKooLit/Hyprland-Dots/wiki/TIPS#%EF%B8%8F-weather-app-related-for-waybar-and-hyprlock diff --git a/config/hypr/scripts/Battery.sh b/config/hypr/scripts/Battery.sh new file mode 100644 index 00000000..d7830058 --- /dev/null +++ b/config/hypr/scripts/Battery.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +for i in {0..3}; do + if [ -f /sys/class/power_supply/BAT$i/capacity ]; then + battery_level=$(cat /sys/class/power_supply/BAT$i/status) + battery_capacity=$(cat /sys/class/power_supply/BAT$i/capacity) + echo "Battery: $battery_capacity% ($battery_level)" + fi +done diff --git a/config/hypr/scripts/Brightness.sh b/config/hypr/scripts/Brightness.sh index 8e5d525a..bf28fc8c 100755 --- a/config/hypr/scripts/Brightness.sh +++ b/config/hypr/scripts/Brightness.sh @@ -29,7 +29,7 @@ get_icon() { # Notify notify_user() { - notify-send -e -h string:x-canonical-private-synchronous:brightness_notif -h int:value:$current -u low -i $icon "Screen" "Brightness:$current%" + notify-send -e -h string:x-canonical-private-synchronous:brightness_notif -h int:value:$current -h boolean:SWAYNC_BYPASS_DND:true -u low -i $icon "Screen" "Brightness:$current%" } # Change brightness diff --git a/config/hypr/scripts/BrightnessKbd.sh b/config/hypr/scripts/BrightnessKbd.sh index 4c56bc03..24737b73 100755 --- a/config/hypr/scripts/BrightnessKbd.sh +++ b/config/hypr/scripts/BrightnessKbd.sh @@ -26,7 +26,7 @@ get_icon() { } # Notify notify_user() { - notify-send -e -h string:x-canonical-private-synchronous:brightness_notif -h int:value:$current -u low -i "$icon" "Keyboard" "Brightness:$current%" + notify-send -e -h string:x-canonical-private-synchronous:brightness_notif -h int:value:$current -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$icon" "Keyboard" "Brightness:$current%" } # Change brightness diff --git a/config/hypr/scripts/Dropterminal.sh b/config/hypr/scripts/Dropterminal.sh new file mode 100755 index 00000000..fa5b899b --- /dev/null +++ b/config/hypr/scripts/Dropterminal.sh @@ -0,0 +1,293 @@ +#!/bin/bash +# /* ---- 💫 https://github.com/JaKooLit 💫 ---- */ ## +# Dropdown Terminal +# Usage: ./Dropdown.sh [-d] <terminal_command> +# Example: ./Dropdown.sh foot +# ./Dropdown.sh -d foot (with debug output) +# ./Dropdown.sh "kitty -e zsh" +# ./Dropdown.sh "alacritty --working-directory /home/user" + +DEBUG=false +SPECIAL_WS="special:scratchpad" +ADDR_FILE="/tmp/dropdown_terminal_addr" + +# Dropdown size and position configuration (percentages) +WIDTH_PERCENT=50 # Width as percentage of screen width +HEIGHT_PERCENT=50 # Height as percentage of screen height +X_PERCENT=25 # X position as percentage from left (25% centers a 50% width window) +Y_PERCENT=5 # Y position as percentage from top + +# Animation settings +ANIMATION_DURATION=100 # milliseconds +SLIDE_STEPS=5 +SLIDE_DELAY=5 # milliseconds between steps + +# Parse arguments +if [ "$1" = "-d" ]; then + DEBUG=true + shift +fi + +TERMINAL_CMD="$1" + +# Debug echo function +debug_echo() { + if [ "$DEBUG" = true ]; then + echo "$@" + fi +} + +# Validate input +if [ -z "$TERMINAL_CMD" ]; then + echo "Missing terminal command. Usage: $0 [-d] <terminal_command>" + echo "Examples:" + echo " $0 foot" + echo " $0 -d foot (with debug output)" + echo " $0 'kitty -e zsh'" + echo " $0 'alacritty --working-directory /home/user'" + echo "" + echo "Edit the script to modify size and position:" + echo " WIDTH_PERCENT - Width as percentage of screen (default: 50)" + echo " HEIGHT_PERCENT - Height as percentage of screen (default: 50)" + echo " X_PERCENT - X position from left as percentage (default: 25)" + echo " Y_PERCENT - Y position from top as percentage (default: 5)" + exit 1 +fi + +# Function to get window geometry +get_window_geometry() { + local addr="$1" + hyprctl clients -j | jq -r --arg ADDR "$addr" '.[] | select(.address == $ADDR) | "\(.at[0]) \(.at[1]) \(.size[0]) \(.size[1])"' +} + +# Function to animate window slide down (show) +animate_slide_down() { + local addr="$1" + local target_x="$2" + local target_y="$3" + local width="$4" + local height="$5" + + debug_echo "Animating slide down for window $addr to position $target_x,$target_y" + + # Start position (above screen) + local start_y=$((target_y - height - 50)) + + # Calculate step size + local step_y=$(((target_y - start_y) / SLIDE_STEPS)) + + # Move window to start position instantly (off-screen) + hyprctl dispatch movewindowpixel "exact $target_x $start_y,address:$addr" >/dev/null 2>&1 + sleep 0.05 + + # Animate slide down + for i in $(seq 1 $SLIDE_STEPS); do + local current_y=$((start_y + (step_y * i))) + hyprctl dispatch movewindowpixel "exact $target_x $current_y,address:$addr" >/dev/null 2>&1 + sleep 0.03 + done + + # Ensure final position is exact + hyprctl dispatch movewindowpixel "exact $target_x $target_y,address:$addr" >/dev/null 2>&1 +} + +# Function to animate window slide up (hide) +animate_slide_up() { + local addr="$1" + local start_x="$2" + local start_y="$3" + local width="$4" + local height="$5" + + debug_echo "Animating slide up for window $addr from position $start_x,$start_y" + + # End position (above screen) + local end_y=$((start_y - height - 50)) + + # Calculate step size + local step_y=$(((start_y - end_y) / SLIDE_STEPS)) + + # Animate slide up + for i in $(seq 1 $SLIDE_STEPS); do + local current_y=$((start_y - (step_y * i))) + hyprctl dispatch movewindowpixel "exact $start_x $current_y,address:$addr" >/dev/null 2>&1 + sleep 0.03 + done + + debug_echo "Slide up animation completed" +} + +# Function to get monitor info for centering +get_monitor_info() { + hyprctl monitors -j | jq -r '.[0] | "\(.x) \(.y) \(.width) \(.height)"' +} + +# Function to calculate dropdown position +calculate_dropdown_position() { + local monitor_info=$(get_monitor_info) + local mon_x=$(echo $monitor_info | cut -d' ' -f1) + local mon_y=$(echo $monitor_info | cut -d' ' -f2) + local mon_width=$(echo $monitor_info | cut -d' ' -f3) + local mon_height=$(echo $monitor_info | cut -d' ' -f4) + + # Calculate position and size based on percentages + local width=$((mon_width * WIDTH_PERCENT / 100)) + local height=$((mon_height * HEIGHT_PERCENT / 100)) + local x=$((mon_x + (mon_width * X_PERCENT / 100))) + local y=$((mon_y + (mon_height * Y_PERCENT / 100))) + + echo "$x $y $width $height" +} + +# Get the current workspace +CURRENT_WS=$(hyprctl activeworkspace -j | jq -r '.id') + +# Function to get stored terminal address +get_terminal_address() { + if [ -f "$ADDR_FILE" ] && [ -s "$ADDR_FILE" ]; then + cat "$ADDR_FILE" + fi +} + +# Function to check if terminal exists +terminal_exists() { + local addr=$(get_terminal_address) + if [ -n "$addr" ]; then + hyprctl clients -j | jq -e --arg ADDR "$addr" 'any(.[]; .address == $ADDR)' >/dev/null 2>&1 + else + return 1 + fi +} + +# Function to check if terminal is in special workspace +terminal_in_special() { + local addr=$(get_terminal_address) + if [ -n "$addr" ]; then + hyprctl clients -j | jq -e --arg ADDR "$addr" 'any(.[]; .address == $ADDR and .workspace.name == "special:scratchpad")' >/dev/null 2>&1 + else + return 1 + fi +} + +# Function to spawn terminal and capture its address +spawn_terminal() { + debug_echo "Creating new dropdown terminal with command: $TERMINAL_CMD" + + # Calculate dropdown position for later use + pos_info=$(calculate_dropdown_position) + target_x=$(echo $pos_info | cut -d' ' -f1) + target_y=$(echo $pos_info | cut -d' ' -f2) + width=$(echo $pos_info | cut -d' ' -f3) + height=$(echo $pos_info | cut -d' ' -f4) + + debug_echo "Target position: ${target_x}x${target_y}, size: ${width}x${height}" + + # Get window count before spawning + windows_before=$(hyprctl clients -j) + count_before=$(echo "$windows_before" | jq 'length') + + # Launch terminal directly in special workspace to avoid visible spawn + hyprctl dispatch exec "[float; size $width $height; workspace special:scratchpad silent] $TERMINAL_CMD" + + # Wait for window to appear + sleep 0.1 + + # Get windows after spawning + windows_after=$(hyprctl clients -j) + count_after=$(echo "$windows_after" | jq 'length') + + new_addr="" + + if [ "$count_after" -gt "$count_before" ]; then + # Find the new window by comparing before/after lists + new_addr=$(comm -13 \ + <(echo "$windows_before" | jq -r '.[].address' | sort) \ + <(echo "$windows_after" | jq -r '.[].address' | sort) \ + | head -1) + fi + + # Fallback: try to find by the most recently mapped window + if [ -z "$new_addr" ] || [ "$new_addr" = "null" ]; then + new_addr=$(hyprctl clients -j | jq -r 'sort_by(.focusHistoryID) | .[-1] | .address') + fi + + if [ -n "$new_addr" ] && [ "$new_addr" != "null" ]; then + # Store the address + echo "$new_addr" > "$ADDR_FILE" + debug_echo "Terminal created with address: $new_addr in special workspace" + + # Small delay to ensure it's properly in special workspace + sleep 0.2 + + # Now bring it back with the same animation as subsequent shows + # Use movetoworkspacesilent to avoid affecting workspace history + hyprctl dispatch movetoworkspacesilent "$CURRENT_WS,address:$new_addr" + hyprctl dispatch pin "address:$new_addr" + animate_slide_down "$new_addr" "$target_x" "$target_y" "$width" "$height" + + return 0 + fi + + debug_echo "Failed to get terminal address" + return 1 +} + +# Main logic +if terminal_exists; then + TERMINAL_ADDR=$(get_terminal_address) + debug_echo "Found existing terminal: $TERMINAL_ADDR" + + if terminal_in_special; then + debug_echo "Bringing terminal from scratchpad with slide down animation" + + # Calculate target position + pos_info=$(calculate_dropdown_position) + target_x=$(echo $pos_info | cut -d' ' -f1) + target_y=$(echo $pos_info | cut -d' ' -f2) + width=$(echo $pos_info | cut -d' ' -f3) + height=$(echo $pos_info | cut -d' ' -f4) + + # Use movetoworkspacesilent to avoid affecting workspace history + hyprctl dispatch movetoworkspacesilent "$CURRENT_WS,address:$TERMINAL_ADDR" + hyprctl dispatch pin "address:$TERMINAL_ADDR" + + # Set size and animate slide down + hyprctl dispatch resizewindowpixel "exact $width $height,address:$TERMINAL_ADDR" + animate_slide_down "$TERMINAL_ADDR" "$target_x" "$target_y" "$width" "$height" + + hyprctl dispatch focuswindow "address:$TERMINAL_ADDR" + else + debug_echo "Hiding terminal to scratchpad with slide up animation" + + # Get current geometry for animation + geometry=$(get_window_geometry "$TERMINAL_ADDR") + if [ -n "$geometry" ]; then + curr_x=$(echo $geometry | cut -d' ' -f1) + curr_y=$(echo $geometry | cut -d' ' -f2) + curr_width=$(echo $geometry | cut -d' ' -f3) + curr_height=$(echo $geometry | cut -d' ' -f4) + + debug_echo "Current geometry: ${curr_x},${curr_y} ${curr_width}x${curr_height}" + + # Animate slide up first + animate_slide_up "$TERMINAL_ADDR" "$curr_x" "$curr_y" "$curr_width" "$curr_height" + + # Small delay then move to special workspace and unpin + sleep 0.1 + hyprctl dispatch pin "address:$TERMINAL_ADDR" # Unpin (toggle) + hyprctl dispatch movetoworkspacesilent "$SPECIAL_WS,address:$TERMINAL_ADDR" + else + debug_echo "Could not get window geometry, moving to scratchpad without animation" + hyprctl dispatch pin "address:$TERMINAL_ADDR" + hyprctl dispatch movetoworkspacesilent "$SPECIAL_WS,address:$TERMINAL_ADDR" + fi + fi +else + debug_echo "No existing terminal found, creating new one" + if spawn_terminal; then + TERMINAL_ADDR=$(get_terminal_address) + if [ -n "$TERMINAL_ADDR" ]; then + hyprctl dispatch focuswindow "address:$TERMINAL_ADDR" + fi + fi +fi
\ No newline at end of file diff --git a/config/hypr/scripts/Volume.sh b/config/hypr/scripts/Volume.sh index b205f8f9..8efdb55c 100755 --- a/config/hypr/scripts/Volume.sh +++ b/config/hypr/scripts/Volume.sh @@ -32,9 +32,9 @@ get_icon() { # Notify notify_user() { if [[ "$(get_volume)" == "Muted" ]]; then - notify-send -e -h string:x-canonical-private-synchronous:volume_notif -u low -i "$(get_icon)" " Volume:" " Muted" + notify-send -e -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume:" " Muted" else - notify-send -e -h int:value:"$(get_volume | sed 's/%//')" -h string:x-canonical-private-synchronous:volume_notif -u low -i "$(get_icon)" " Volume Level:" " $(get_volume)" && + notify-send -e -h int:value:"$(get_volume | sed 's/%//')" -h string:x-canonical-private-synchronous:volume_notif -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$(get_icon)" " Volume Level:" " $(get_volume)" && "$sDIR/Sounds.sh" --volume fi } @@ -60,18 +60,18 @@ dec_volume() { # Toggle Mute toggle_mute() { if [ "$(pamixer --get-mute)" == "false" ]; then - pamixer -m && notify-send -e -u low -i "$iDIR/volume-mute.png" " Mute" + pamixer -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/volume-mute.png" " Mute" elif [ "$(pamixer --get-mute)" == "true" ]; then - pamixer -u && notify-send -e -u low -i "$(get_icon)" " Volume:" " Switched ON" + pamixer -u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$(get_icon)" " Volume:" " Switched ON" fi } # Toggle Mic toggle_mic() { if [ "$(pamixer --default-source --get-mute)" == "false" ]; then - pamixer --default-source -m && notify-send -e -u low -i "$iDIR/microphone-mute.png" " Microphone:" " Switched OFF" + pamixer --default-source -m && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone-mute.png" " Microphone:" " Switched OFF" elif [ "$(pamixer --default-source --get-mute)" == "true" ]; then - pamixer -u --default-source u && notify-send -e -u low -i "$iDIR/microphone.png" " Microphone:" " Switched ON" + pamixer -u --default-source u && notify-send -e -u low -h boolean:SWAYNC_BYPASS_DND:true -i "$iDIR/microphone.png" " Microphone:" " Switched ON" fi } # Get Mic Icon @@ -98,7 +98,7 @@ get_mic_volume() { notify_mic_user() { volume=$(get_mic_volume) icon=$(get_mic_icon) - notify-send -e -h int:value:"$volume" -h "string:x-canonical-private-synchronous:volume_notif" -u low -i "$icon" " Mic Level:" " $volume" + notify-send -e -h int:value:"$volume" -h "string:x-canonical-private-synchronous:volume_notif" -h boolean:SWAYNC_BYPASS_DND:true -u low -i "$icon" " Mic Level:" " $volume" } # Increase MIC Volume diff --git a/config/hypr/v2.3.15 b/config/hypr/v2.3.16 index 31b3414d..31b3414d 100644 --- a/config/hypr/v2.3.15 +++ b/config/hypr/v2.3.16 diff --git a/config/quickshell/GlobalStates.qml b/config/quickshell/GlobalStates.qml new file mode 100644 index 00000000..84b53c03 --- /dev/null +++ b/config/quickshell/GlobalStates.qml @@ -0,0 +1,10 @@ +import "root:/modules/common/" +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + id: root + property bool overviewOpen: false +}
\ No newline at end of file diff --git a/config/quickshell/config.json b/config/quickshell/config.json new file mode 100644 index 00000000..286985f5 --- /dev/null +++ b/config/quickshell/config.json @@ -0,0 +1,53 @@ +{ + "ui": { + "theme": "dark", + "scale": 1.25 + }, + "features": { + "animations": true + }, + "appearance": { + "fakeScreenRounding": 1 + }, + "overview": { + "scale": 0.15, + "numOfRows": 2, + "numOfCols": 5, + "showXwaylandIndicator": true, + "windowPadding": 6, + "position": 1, + "workspaceNumberSize": 220 + }, + "resources": { + "updateInterval": 3000 + }, + "hacks": { + "arbitraryRaceConditionDelay": 20 + }, + "search": { + "searchEnabled": true, + "nonAppResultDelay": 30, + "prefix": { + "action": "/", + "clipboard": ";", + "emojis": ":" + } + }, + "bar": { + "bottom": false + }, + "font": { + "family": { + "uiFont": "Open Sans", + "iconFont": "FiraConde Nerd Font", + "codeFont": "JetBrains Mono NF" + }, + "pixelSize": { + "textSmall": 13, + "textBase": 15, + "textMedium": 16, + "textLarge": 19, + "iconLarge": 22 + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/common/Appearance.qml b/config/quickshell/modules/common/Appearance.qml new file mode 100644 index 00000000..675f1d1e --- /dev/null +++ b/config/quickshell/modules/common/Appearance.qml @@ -0,0 +1,201 @@ +import QtQuick +import Quickshell +import "root:/modules/common/functions/color_utils.js" as ColorUtils +pragma Singleton +pragma ComponentBehavior: Bound + + +Singleton { + id: root + property QtObject m3colors + property QtObject animation + property QtObject animationCurves + property QtObject colors + property QtObject rounding + property QtObject font + property QtObject sizes + + property real transparency: 0.5 + property real contentTransparency: 0.1 + property real workpaceTransparency: 0.8 + property string background_image: Directories.config + "/rofi/.current_wallpaper" + + m3colors: QtObject { + property bool darkmode: true + property bool transparent: true + + property color m3windowBackground: "#161217" + property color m3primaryText: "#EAE0E7" + property color m3layerBackground1: "#1F1A1F" + property color m3layerBackground2: "#231E23" + property color m3layerBackground3: "#2D282E" + property color m3surfaceText: "#EAE0E7" + property color m3secondaryText: "#CFC3CD" + property color m3borderPrimary: "#cba6f7" + property color m3shadowColor: "#000000" + property color m3accentPrimary: "#E5B6F2" + property color m3accentSecondary: "#D5C0D7" + property color m3selectionBackground: "#534457" + property color m3accentPrimaryText: "#452152" + property color m3selectionText: "#F2DCF3" + property color m3borderSecondary: "#4C444D" + + property color colTooltip: "#1e1e2e" + property color colOnTooltip: "#F8F9FA" + } + + colors: QtObject { + property color colSubtext: m3colors.m3borderPrimary + property color colLayer0: ColorUtils.transparentize(m3colors.m3windowBackground, root.transparency) + property color colLayer1: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground1, m3colors.m3windowBackground, 0.7), root.contentTransparency); + property color colOnLayer1: m3colors.m3secondaryText; + property color colLayer2: ColorUtils.transparentize(ColorUtils.mix(m3colors.m3layerBackground2, m3colors.m3layerBackground3, 0.55), root.contentTransparency) + property color colOnLayer2: m3colors.m3surfaceText; + property color colLayer1Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.92), root.contentTransparency) + property color colLayer1Active: ColorUtils.transparentize(ColorUtils.mix(colLayer1, colOnLayer1, 0.85), root.contentTransparency); + property color colLayer2Hover: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.90), root.contentTransparency) + property color colLayer2Active: ColorUtils.transparentize(ColorUtils.mix(colLayer2, colOnLayer2, 0.80), root.contentTransparency); + property color colPrimary: m3colors.m3accentPrimary + property color colPrimaryHover: ColorUtils.mix(colors.colPrimary, colLayer1Hover, 0.87) + property color colPrimaryActive: ColorUtils.mix(colors.colPrimary, colLayer1Active, 0.7) + property color colShadow: ColorUtils.transparentize(m3colors.m3shadowColor, 0.7) + } + + rounding: QtObject { + property int unsharpen: 2 + property int verysmall: 8 + property int small: 12 + property int normal: 17 + property int large: 23 + property int verylarge: 30 + property int veryverylarge: 60 + property int full: 9999 + property int screenRounding: veryverylarge + property int windowRounding: veryverylarge + } + + font: QtObject { + property QtObject family: QtObject { + property string uiFont: "Open Sans" + property string iconFont: "FiraConde Nerd Font" + property string codeFont: "JetBrains Mono NF" + } + property QtObject pixelSize: QtObject { + property int textSmall: 13 + property int textBase: 15 + property int textMedium: 16 + property int textLarge: 19 + property int iconLarge: 22 + } + } + + animationCurves: QtObject { + readonly property list<real> expressiveFastSpatial: [0.42, 1.67, 0.21, 0.90, 1, 1] // Default, 350ms + readonly property list<real> expressiveDefaultSpatial: [0.38, 1.21, 0.22, 1.00, 1, 1] // Default, 500ms + readonly property list<real> expressiveSlowSpatial: [0.39, 1.29, 0.35, 0.98, 1, 1] // Default, 650ms + readonly property list<real> expressiveEffects: [0.34, 0.80, 0.34, 1.00, 1, 1] // Default, 200ms + readonly property list<real> emphasized: [0.05, 0, 2 / 15, 0.06, 1 / 6, 0.4, 5 / 24, 0.82, 0.25, 1, 1, 1] + readonly property list<real> emphasizedAccel: [0.3, 0, 0.8, 0.15, 1, 1] + readonly property list<real> emphasizedDecel: [0.05, 0.7, 0.1, 1, 1, 1] + readonly property list<real> standard: [0.2, 0, 0, 1, 1, 1] + readonly property list<real> standardAccel: [0.3, 0, 1, 1, 1, 1] + readonly property list<real> standardDecel: [0, 0, 0, 1, 1, 1] + } + + animation: QtObject { + property QtObject elementMove: QtObject { + property int duration: 500 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveDefaultSpatial + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + property Component colorAnimation: Component { + ColorAnimation { + duration: root.animation.elementMove.duration + easing.type: root.animation.elementMove.type + easing.bezierCurve: root.animation.elementMove.bezierCurve + } + } + } + property QtObject elementMoveEnter: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.emphasizedDecel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveEnter.duration + easing.type: root.animation.elementMoveEnter.type + easing.bezierCurve: root.animation.elementMoveEnter.bezierCurve + } + } + } + property QtObject elementMoveExit: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.emphasizedAccel + property int velocity: 650 + property Component numberAnimation: Component { + NumberAnimation { + duration: root.animation.elementMoveExit.duration + easing.type: root.animation.elementMoveExit.type + easing.bezierCurve: root.animation.elementMoveExit.bezierCurve + } + } + } + property QtObject elementMoveFast: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveEffects + property int velocity: 850 + property Component colorAnimation: Component { ColorAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + }} + property Component numberAnimation: Component { NumberAnimation { + duration: root.animation.elementMoveFast.duration + easing.type: root.animation.elementMoveFast.type + easing.bezierCurve: root.animation.elementMoveFast.bezierCurve + }} + } + + property QtObject clickBounce: QtObject { + property int duration: 200 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.expressiveFastSpatial + property int velocity: 850 + property Component numberAnimation: Component { NumberAnimation { + duration: root.animation.clickBounce.duration + easing.type: root.animation.clickBounce.type + easing.bezierCurve: root.animation.clickBounce.bezierCurve + }} + } + property QtObject scroll: QtObject { + property int duration: 400 + property int type: Easing.BezierSpline + property list<real> bezierCurve: animationCurves.standardDecel + } + property QtObject menuDecel: QtObject { + property int duration: 350 + property int type: Easing.OutExpo + } + } + + sizes: QtObject { + property real barHeight: 40 + property real notificationPopupWidth: 410 + property real searchWidthCollapsed: 260 + property real searchWidth: 450 + property real hyprlandGapsOut: 5 + property real elevationMargin: 10 + property real fabShadowRadius: 5 + property real fabHoveredShadowRadius: 7 + } +} diff --git a/config/quickshell/modules/common/ConfigOptions.qml b/config/quickshell/modules/common/ConfigOptions.qml new file mode 100644 index 00000000..25f0de05 --- /dev/null +++ b/config/quickshell/modules/common/ConfigOptions.qml @@ -0,0 +1,43 @@ +import QtQuick +import Quickshell +pragma Singleton +pragma ComponentBehavior: Bound + +Singleton { + + property QtObject appearance: QtObject { + property int fakeScreenRounding: 1 // 0: None | 1: Always | 2: When not fullscreen + } + + property QtObject overview: QtObject { + property real scale: 0.15 // Relative to screen size + property real numOfRows: 2 + property real numOfCols: 5 + property bool showXwaylandIndicator: true + property real windowPadding: 6 + property real position: 1 // 0: top | 1: middle | 2: bottom + property real workspaceNumberSize: 120 // Set 0, dynamic calculation based on monitor size + } + + property QtObject resources: QtObject { + property int updateInterval: 3000 + } + + property QtObject hacks: QtObject { + property int arbitraryRaceConditionDelay: 20 // milliseconds + } + + property QtObject search: QtObject { + property bool searchEnabled: false + property int nonAppResultDelay: 30 // This prevents lagging when typing + property QtObject prefix: QtObject { + property string action: "/" + property string clipboard: ";" + property string emojis: ":" + } + } + + property QtObject bar: QtObject { + property bool bottom: false // Instead of top + } +} diff --git a/config/quickshell/modules/common/Directories.qml b/config/quickshell/modules/common/Directories.qml new file mode 100644 index 00000000..f4a6bf83 --- /dev/null +++ b/config/quickshell/modules/common/Directories.qml @@ -0,0 +1,20 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common/functions/file_utils.js" as FileUtils +import Qt.labs.platform +import QtQuick +import Quickshell +import Quickshell.Hyprland + +Singleton { + // XDG Dirs, with "file://" + readonly property string config: StandardPaths.standardLocations(StandardPaths.ConfigLocation)[0] + readonly property string state: StandardPaths.standardLocations(StandardPaths.StateLocation)[0] + readonly property string gen_cache: StandardPaths.standardLocations(StandardPaths.GenericCacheLocation)[0] + + // Other dirs used by the shell, without "file://" + property string shellConfig: FileUtils.trimFileProtocol(`${Directories.config}/quickshell`) + property string shellConfigPath: `${Directories.shellConfig}/config.json` + property string generatedMaterialThemePath: `${Directories.shellConfig}/qml_color.json` +} diff --git a/config/quickshell/modules/common/functions/color_utils.js b/config/quickshell/modules/common/functions/color_utils.js new file mode 100644 index 00000000..c0ccfda9 --- /dev/null +++ b/config/quickshell/modules/common/functions/color_utils.js @@ -0,0 +1,85 @@ +// This module provides high level utility functions for color manipulation. + +/** + * Returns a color with the hue of color2 and the saturation, value, and alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take hue from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithHueOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + // Qt.color hsvHue/hsvSaturation/hsvValue/alpha return 0-1 + var hue = c2.hsvHue; + var sat = c1.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Returns a color with the saturation of color2 and the hue/value/alpha of color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The color to take saturation from. + * @returns {Qt.rgba} The resulting color. + */ +function colorWithSaturationOf(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c1.hsvHue; + var sat = c2.hsvSaturation; + var val = c1.hsvValue; + var alpha = c1.a; + + return Qt.hsva(hue, sat, val, alpha); +} + +/** + * Adapts color1 to the accent (hue and saturation) of color2 using HSL, keeping lightness and alpha from color1. + * + * @param {string} color1 - The base color (any Qt.color-compatible string). + * @param {string} color2 - The accent color. + * @returns {Qt.rgba} The resulting color. + */ +function adaptToAccent(color1, color2) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + + var hue = c2.hslHue; + var sat = c2.hslSaturation; + var light = c1.hslLightness; + var alpha = c1.a; + + return Qt.hsla(hue, sat, light, alpha); +} + +/** + * Mixes two colors by a given percentage. + * + * @param {string} color1 - The first color (any Qt.color-compatible string). + * @param {string} color2 - The second color. + * @param {number} percentage - The mix ratio (0-1). 1 = all color1, 0 = all color2. + * @returns {Qt.rgba} The resulting mixed color. + */ +function mix(color1, color2, percentage) { + var c1 = Qt.color(color1); + var c2 = Qt.color(color2); + return Qt.rgba(percentage * c1.r + (1 - percentage) * c2.r, percentage * c1.g + (1 - percentage) * c2.g, percentage * c1.b + (1 - percentage) * c2.b, percentage * c1.a + (1 - percentage) * c2.a); +} + +/** + * Transparentizes a color by a given percentage. + * + * @param {string} color - The color (any Qt.color-compatible string). + * @param {number} percentage - The amount to transparentize (0-1). + * @returns {Qt.rgba} The resulting color. + */ +function transparentize(color, percentage = 1) { + var c = Qt.color(color); + return Qt.rgba(c.r, c.g, c.b, c.a * (1 - percentage)); +} diff --git a/config/quickshell/modules/common/functions/file_utils.js b/config/quickshell/modules/common/functions/file_utils.js new file mode 100644 index 00000000..758950de --- /dev/null +++ b/config/quickshell/modules/common/functions/file_utils.js @@ -0,0 +1,9 @@ +/** + * Trims the File protocol off the input string + * @param {string} str + * @returns {string} + */ +function trimFileProtocol(str) { + return str.startsWith("file://") ? str.slice(7) : str; +} + diff --git a/config/quickshell/modules/common/functions/fuzzysort.js b/config/quickshell/modules/common/functions/fuzzysort.js new file mode 100644 index 00000000..1c1d9b9d --- /dev/null +++ b/config/quickshell/modules/common/functions/fuzzysort.js @@ -0,0 +1,682 @@ +.pragma library + +// https://github.com/farzher/fuzzysort +// License: MIT | Copyright (c) 2018 Stephen Kamenar +// A copy of the license is available in the `licenses` folder of this repository + +var single = (search, target) => { + if(!search || !target) return NULL + + var preparedSearch = getPreparedSearch(search) + if(!isPrepared(target)) target = getPrepared(target) + + var searchBitflags = preparedSearch.bitflags + if((searchBitflags & target._bitflags) !== searchBitflags) return NULL + + return algorithm(preparedSearch, target) +} + +var go = (search, targets, options) => { + if(!search) return options?.all ? all(targets, options) : noResults + + var preparedSearch = getPreparedSearch(search) + var searchBitflags = preparedSearch.bitflags + var containsSpace = preparedSearch.containsSpace + + var threshold = denormalizeScore( options?.threshold || 0 ) + var limit = options?.limit || INFINITY + + var resultsLen = 0; var limitedCount = 0 + var targetsLen = targets.length + + function push_result(result) { + if(resultsLen < limit) { q.add(result); ++resultsLen } + else { + ++limitedCount + if(result._score > q.peek()._score) q.replaceTop(result) + } + } + + // This code is copy/pasted 3 times for performance reasons [options.key, options.keys, no keys] + + // options.key + if(options?.key) { + var key = options.key + for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + var target = getValue(obj, key) + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + result.obj = obj + push_result(result) + } + + // options.keys + } else if(options?.keys) { + var keys = options.keys + var keysLen = keys.length + + outer: for(var i = 0; i < targetsLen; ++i) { var obj = targets[i] + + { // early out based on bitflags + var keysBitflags = 0 + for (var keyI = 0; keyI < keysLen; ++keyI) { + var key = keys[keyI] + var target = getValue(obj, key) + if(!target) { tmpTargets[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + tmpTargets[keyI] = target + + keysBitflags |= target._bitflags + } + + if((searchBitflags & keysBitflags) !== searchBitflags) continue + } + + if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) keysSpacesBestScores[i] = NEGATIVE_INFINITY + + for (var keyI = 0; keyI < keysLen; ++keyI) { + target = tmpTargets[keyI] + if(target === noTarget) { tmpResults[keyI] = noTarget; continue } + + tmpResults[keyI] = algorithm(preparedSearch, target, /*allowSpaces=*/false, /*allowPartialMatch=*/containsSpace) + if(tmpResults[keyI] === NULL) { tmpResults[keyI] = noTarget; continue } + + // todo: this seems weird and wrong. like what if our first match wasn't good. this should just replace it instead of averaging with it + // if our second match isn't good we ignore it instead of averaging with it + if(containsSpace) for(let i=0; i<preparedSearch.spaceSearches.length; i++) { + if(allowPartialMatchScores[i] > -1000) { + if(keysSpacesBestScores[i] > NEGATIVE_INFINITY) { + var tmp = (keysSpacesBestScores[i] + allowPartialMatchScores[i]) / 4/*bonus score for having multiple matches*/ + if(tmp > keysSpacesBestScores[i]) keysSpacesBestScores[i] = tmp + } + } + if(allowPartialMatchScores[i] > keysSpacesBestScores[i]) keysSpacesBestScores[i] = allowPartialMatchScores[i] + } + } + + if(containsSpace) { + for(let i=0; i<preparedSearch.spaceSearches.length; i++) { if(keysSpacesBestScores[i] === NEGATIVE_INFINITY) continue outer } + } else { + var hasAtLeast1Match = false + for(let i=0; i < keysLen; i++) { if(tmpResults[i]._score !== NEGATIVE_INFINITY) { hasAtLeast1Match = true; break } } + if(!hasAtLeast1Match) continue + } + + var objResults = new KeysResult(keysLen) + for(let i=0; i < keysLen; i++) { objResults[i] = tmpResults[i] } + + if(containsSpace) { + var score = 0 + for(let i=0; i<preparedSearch.spaceSearches.length; i++) score += keysSpacesBestScores[i] + } else { + // todo could rewrite this scoring to be more similar to when there's spaces + // if we match multiple keys give us bonus points + var score = NEGATIVE_INFINITY + for(let i=0; i<keysLen; i++) { + var result = objResults[i] + if(result._score > -1000) { + if(score > NEGATIVE_INFINITY) { + var tmp = (score + result._score) / 4/*bonus score for having multiple matches*/ + if(tmp > score) score = tmp + } + } + if(result._score > score) score = result._score + } + } + + objResults.obj = obj + objResults._score = score + if(options?.scoreFn) { + score = options.scoreFn(objResults) + if(!score) continue + score = denormalizeScore(score) + objResults._score = score + } + + if(score < threshold) continue + push_result(objResults) + } + + // no keys + } else { + for(var i = 0; i < targetsLen; ++i) { var target = targets[i] + if(!target) continue + if(!isPrepared(target)) target = getPrepared(target) + + if((searchBitflags & target._bitflags) !== searchBitflags) continue + var result = algorithm(preparedSearch, target) + if(result === NULL) continue + if(result._score < threshold) continue + + push_result(result) + } + } + + if(resultsLen === 0) return noResults + var results = new Array(resultsLen) + for(var i = resultsLen - 1; i >= 0; --i) results[i] = q.poll() + results.total = resultsLen + limitedCount + return results +} + + +// this is written as 1 function instead of 2 for minification. perf seems fine ... +// except when minified. the perf is very slow +var highlight = (result, open='<b>', close='</b>') => { + var callback = typeof open === 'function' ? open : undefined + + var target = result.target + var targetLen = target.length + var indexes = result.indexes + var highlighted = '' + var matchI = 0 + var indexesI = 0 + var opened = false + var parts = [] + + for(var i = 0; i < targetLen; ++i) { var char = target[i] + if(indexes[indexesI] === i) { + ++indexesI + if(!opened) { opened = true + if(callback) { + parts.push(highlighted); highlighted = '' + } else { + highlighted += open + } + } + + if(indexesI === indexes.length) { + if(callback) { + highlighted += char + parts.push(callback(highlighted, matchI++)); highlighted = '' + parts.push(target.substr(i+1)) + } else { + highlighted += char + close + target.substr(i+1) + } + break + } + } else { + if(opened) { opened = false + if(callback) { + parts.push(callback(highlighted, matchI++)); highlighted = '' + } else { + highlighted += close + } + } + } + highlighted += char + } + + return callback ? parts : highlighted +} + + +var prepare = (target) => { + if(typeof target === 'number') target = ''+target + else if(typeof target !== 'string') target = '' + var info = prepareLowerInfo(target) + return new_result(target, {_targetLower:info._lower, _targetLowerCodes:info.lowerCodes, _bitflags:info.bitflags}) +} + +var cleanup = () => { preparedCache.clear(); preparedSearchCache.clear() } + + +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code +// Below this point is only internal code + + +class Result { + get ['indexes']() { return this._indexes.slice(0, this._indexes.len).sort((a,b)=>a-b) } + set ['indexes'](indexes) { return this._indexes = indexes } + ['highlight'](open, close) { return highlight(this, open, close) } + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +class KeysResult extends Array { + get ['score']() { return normalizeScore(this._score) } + set ['score'](score) { this._score = denormalizeScore(score) } +} + +var new_result = (target, options) => { + const result = new Result() + result['target'] = target + result['obj'] = options.obj ?? NULL + result._score = options._score ?? NEGATIVE_INFINITY + result._indexes = options._indexes ?? [] + result._targetLower = options._targetLower ?? '' + result._targetLowerCodes = options._targetLowerCodes ?? NULL + result._nextBeginningIndexes = options._nextBeginningIndexes ?? NULL + result._bitflags = options._bitflags ?? 0 + return result +} + + +var normalizeScore = score => { + if(score === NEGATIVE_INFINITY) return 0 + if(score > 1) return score + return Math.E ** ( ((-score + 1)**.04307 - 1) * -2) +} +var denormalizeScore = normalizedScore => { + if(normalizedScore === 0) return NEGATIVE_INFINITY + if(normalizedScore > 1) return normalizedScore + return 1 - Math.pow((Math.log(normalizedScore) / -2 + 1), 1 / 0.04307) +} + + +var prepareSearch = (search) => { + if(typeof search === 'number') search = ''+search + else if(typeof search !== 'string') search = '' + search = search.trim() + var info = prepareLowerInfo(search) + + var spaceSearches = [] + if(info.containsSpace) { + var searches = search.split(/\s+/) + searches = [...new Set(searches)] // distinct + for(var i=0; i<searches.length; i++) { + if(searches[i] === '') continue + var _info = prepareLowerInfo(searches[i]) + spaceSearches.push({lowerCodes:_info.lowerCodes, _lower:searches[i].toLowerCase(), containsSpace:false}) + } + } + + return {lowerCodes: info.lowerCodes, _lower: info._lower, containsSpace: info.containsSpace, bitflags: info.bitflags, spaceSearches: spaceSearches} +} + + + +var getPrepared = (target) => { + if(target.length > 999) return prepare(target) // don't cache huge targets + var targetPrepared = preparedCache.get(target) + if(targetPrepared !== undefined) return targetPrepared + targetPrepared = prepare(target) + preparedCache.set(target, targetPrepared) + return targetPrepared +} +var getPreparedSearch = (search) => { + if(search.length > 999) return prepareSearch(search) // don't cache huge searches + var searchPrepared = preparedSearchCache.get(search) + if(searchPrepared !== undefined) return searchPrepared + searchPrepared = prepareSearch(search) + preparedSearchCache.set(search, searchPrepared) + return searchPrepared +} + + +var all = (targets, options) => { + var results = []; results.total = targets.length // this total can be wrong if some targets are skipped + + var limit = options?.limit || INFINITY + + if(options?.key) { + for(var i=0;i<targets.length;i++) { var obj = targets[i] + var target = getValue(obj, options.key) + if(target == NULL) continue + if(!isPrepared(target)) target = getPrepared(target) + var result = new_result(target.target, {_score: target._score, obj: obj}) + results.push(result); if(results.length >= limit) return results + } + } else if(options?.keys) { + for(var i=0;i<targets.length;i++) { var obj = targets[i] + var objResults = new KeysResult(options.keys.length) + for (var keyI = options.keys.length - 1; keyI >= 0; --keyI) { + var target = getValue(obj, options.keys[keyI]) + if(!target) { objResults[keyI] = noTarget; continue } + if(!isPrepared(target)) target = getPrepared(target) + target._score = NEGATIVE_INFINITY + target._indexes.len = 0 + objResults[keyI] = target + } + objResults.obj = obj + objResults._score = NEGATIVE_INFINITY + results.push(objResults); if(results.length >= limit) return results + } + } else { + for(var i=0;i<targets.length;i++) { var target = targets[i] + if(target == NULL) continue + if(!isPrepared(target)) target = getPrepared(target) + target._score = NEGATIVE_INFINITY + target._indexes.len = 0 + results.push(target); if(results.length >= limit) return results + } + } + + return results +} + + +var algorithm = (preparedSearch, prepared, allowSpaces=false, allowPartialMatch=false) => { + if(allowSpaces===false && preparedSearch.containsSpace) return algorithmSpaces(preparedSearch, prepared, allowPartialMatch) + + var searchLower = preparedSearch._lower + var searchLowerCodes = preparedSearch.lowerCodes + var searchLowerCode = searchLowerCodes[0] + var targetLowerCodes = prepared._targetLowerCodes + var searchLen = searchLowerCodes.length + var targetLen = targetLowerCodes.length + var searchI = 0 // where we at + var targetI = 0 // where you at + var matchesSimpleLen = 0 + + // very basic fuzzy match; to remove non-matching targets ASAP! + // walk through target. find sequential matches. + // if all chars aren't found then exit + for(;;) { + var isMatch = searchLowerCode === targetLowerCodes[targetI] + if(isMatch) { + matchesSimple[matchesSimpleLen++] = targetI + ++searchI; if(searchI === searchLen) break + searchLowerCode = searchLowerCodes[searchI] + } + ++targetI; if(targetI >= targetLen) return NULL // Failed to find searchI + } + + var searchI = 0 + var successStrict = false + var matchesStrictLen = 0 + + var nextBeginningIndexes = prepared._nextBeginningIndexes + if(nextBeginningIndexes === NULL) nextBeginningIndexes = prepared._nextBeginningIndexes = prepareNextBeginningIndexes(prepared.target) + targetI = matchesSimple[0]===0 ? 0 : nextBeginningIndexes[matchesSimple[0]-1] + + // Our target string successfully matched all characters in sequence! + // Let's try a more advanced and strict test to improve the score + // only count it as a match if it's consecutive or a beginning character! + var backtrackCount = 0 + if(targetI !== targetLen) for(;;) { + if(targetI >= targetLen) { + // We failed to find a good spot for this search char, go back to the previous search char and force it forward + if(searchI <= 0) break // We failed to push chars forward for a better match + + ++backtrackCount; if(backtrackCount > 200) break // exponential backtracking is taking too long, just give up and return a bad match + + --searchI + var lastMatch = matchesStrict[--matchesStrictLen] + targetI = nextBeginningIndexes[lastMatch] + + } else { + var isMatch = searchLowerCodes[searchI] === targetLowerCodes[targetI] + if(isMatch) { + matchesStrict[matchesStrictLen++] = targetI + ++searchI; if(searchI === searchLen) { successStrict = true; break } + ++targetI + } else { + targetI = nextBeginningIndexes[targetI] + } + } + } + + // check if it's a substring match + var substringIndex = searchLen <= 1 ? -1 : prepared._targetLower.indexOf(searchLower, matchesSimple[0]) // perf: this is slow + var isSubstring = !!~substringIndex + var isSubstringBeginning = !isSubstring ? false : substringIndex===0 || prepared._nextBeginningIndexes[substringIndex-1] === substringIndex + + // if it's a substring match but not at a beginning index, let's try to find a substring starting at a beginning index for a better score + if(isSubstring && !isSubstringBeginning) { + for(var i=0; i<nextBeginningIndexes.length; i=nextBeginningIndexes[i]) { + if(i <= substringIndex) continue + + for(var s=0; s<searchLen; s++) if(searchLowerCodes[s] !== prepared._targetLowerCodes[i+s]) break + if(s === searchLen) { substringIndex = i; isSubstringBeginning = true; break } + } + } + + // tally up the score & keep track of matches for highlighting later + // if it's a simple match, we'll switch to a substring match if a substring exists + // if it's a strict match, we'll switch to a substring match only if that's a better score + + var calculateScore = matches => { + var score = 0 + + var extraMatchGroupCount = 0 + for(var i = 1; i < searchLen; ++i) { + if(matches[i] - matches[i-1] !== 1) {score -= matches[i]; ++extraMatchGroupCount} + } + var unmatchedDistance = matches[searchLen-1] - matches[0] - (searchLen-1) + + score -= (12+unmatchedDistance) * extraMatchGroupCount // penality for more groups + + if(matches[0] !== 0) score -= matches[0]*matches[0]*.2 // penality for not starting near the beginning + + if(!successStrict) { + score *= 1000 + } else { + // successStrict on a target with too many beginning indexes loses points for being a bad target + var uniqueBeginningIndexes = 1 + for(var i = nextBeginningIndexes[0]; i < targetLen; i=nextBeginningIndexes[i]) ++uniqueBeginningIndexes + + if(uniqueBeginningIndexes > 24) score *= (uniqueBeginningIndexes-24)*10 // quite arbitrary numbers here ... + } + + score -= (targetLen - searchLen)/2 // penality for longer targets + + if(isSubstring) score /= 1+searchLen*searchLen*1 // bonus for being a full substring + if(isSubstringBeginning) score /= 1+searchLen*searchLen*1 // bonus for substring starting on a beginningIndex + + score -= (targetLen - searchLen)/2 // penality for longer targets + + return score + } + + if(!successStrict) { + if(isSubstring) for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches + var matchesBest = matchesSimple + var score = calculateScore(matchesBest) + } else { + if(isSubstringBeginning) { + for(var i=0; i<searchLen; ++i) matchesSimple[i] = substringIndex+i // at this point it's safe to overwrite matchehsSimple with substr matches + var matchesBest = matchesSimple + var score = calculateScore(matchesSimple) + } else { + var matchesBest = matchesStrict + var score = calculateScore(matchesStrict) + } + } + + prepared._score = score + + for(var i = 0; i < searchLen; ++i) prepared._indexes[i] = matchesBest[i] + prepared._indexes.len = searchLen + + const result = new Result() + result.target = prepared.target + result._score = prepared._score + result._indexes = prepared._indexes + return result +} +var algorithmSpaces = (preparedSearch, target, allowPartialMatch) => { + var seen_indexes = new Set() + var score = 0 + var result = NULL + + var first_seen_index_last_search = 0 + var searches = preparedSearch.spaceSearches + var searchesLen = searches.length + var changeslen = 0 + + // Return _nextBeginningIndexes back to its normal state + var resetNextBeginningIndexes = () => { + for(let i=changeslen-1; i>=0; i--) target._nextBeginningIndexes[nextBeginningIndexesChanges[i*2 + 0]] = nextBeginningIndexesChanges[i*2 + 1] + } + + var hasAtLeast1Match = false + for(var i=0; i<searchesLen; ++i) { + allowPartialMatchScores[i] = NEGATIVE_INFINITY + var search = searches[i] + + result = algorithm(search, target) + if(allowPartialMatch) { + if(result === NULL) continue + hasAtLeast1Match = true + } else { + if(result === NULL) {resetNextBeginningIndexes(); return NULL} + } + + // if not the last search, we need to mutate _nextBeginningIndexes for the next search + var isTheLastSearch = i === searchesLen - 1 + if(!isTheLastSearch) { + var indexes = result._indexes + + var indexesIsConsecutiveSubstring = true + for(let i=0; i<indexes.len-1; i++) { + if(indexes[i+1] - indexes[i] !== 1) { + indexesIsConsecutiveSubstring = false; break; + } + } + + if(indexesIsConsecutiveSubstring) { + var newBeginningIndex = indexes[indexes.len-1] + 1 + var toReplace = target._nextBeginningIndexes[newBeginningIndex-1] + for(let i=newBeginningIndex-1; i>=0; i--) { + if(toReplace !== target._nextBeginningIndexes[i]) break + target._nextBeginningIndexes[i] = newBeginningIndex + nextBeginningIndexesChanges[changeslen*2 + 0] = i + nextBeginningIndexesChanges[changeslen*2 + 1] = toReplace + changeslen++ + } + } + } + + score += result._score / searchesLen + allowPartialMatchScores[i] = result._score / searchesLen + + // dock points based on order otherwise "c man" returns Manifest.cpp instead of CheatManager.h + if(result._indexes[0] < first_seen_index_last_search) { + score -= (first_seen_index_last_search - result._indexes[0]) * 2 + } + first_seen_index_last_search = result._indexes[0] + + for(var j=0; j<result._indexes.len; ++j) seen_indexes.add(result._indexes[j]) + } + + if(allowPartialMatch && !hasAtLeast1Match) return NULL + + resetNextBeginningIndexes() + + // allows a search with spaces that's an exact substring to score well + var allowSpacesResult = algorithm(preparedSearch, target, /*allowSpaces=*/true) + if(allowSpacesResult !== NULL && allowSpacesResult._score > score) { + if(allowPartialMatch) { + for(var i=0; i<searchesLen; ++i) { + allowPartialMatchScores[i] = allowSpacesResult._score / searchesLen + } + } + return allowSpacesResult + } + + if(allowPartialMatch) result = target + result._score = score + + var i = 0 + for (let index of seen_indexes) result._indexes[i++] = index + result._indexes.len = i + + return result +} + +// we use this instead of just .normalize('NFD').replace(/[\u0300-\u036f]/g, '') because that screws with japanese characters +var remove_accents = (str) => str.replace(/\p{Script=Latin}+/gu, match => match.normalize('NFD')).replace(/[\u0300-\u036f]/g, '') + +var prepareLowerInfo = (str) => { + str = remove_accents(str) + var strLen = str.length + var lower = str.toLowerCase() + var lowerCodes = [] // new Array(strLen) sparse array is too slow + var bitflags = 0 + var containsSpace = false // space isn't stored in bitflags because of how searching with a space works + + for(var i = 0; i < strLen; ++i) { + var lowerCode = lowerCodes[i] = lower.charCodeAt(i) + + if(lowerCode === 32) { + containsSpace = true + continue // it's important that we don't set any bitflags for space + } + + var bit = lowerCode>=97&&lowerCode<=122 ? lowerCode-97 // alphabet + : lowerCode>=48&&lowerCode<=57 ? 26 // numbers + // 3 bits available + : lowerCode<=127 ? 30 // other ascii + : 31 // other utf8 + bitflags |= 1<<bit + } + + return {lowerCodes:lowerCodes, bitflags:bitflags, containsSpace:containsSpace, _lower:lower} +} +var prepareBeginningIndexes = (target) => { + var targetLen = target.length + var beginningIndexes = []; var beginningIndexesLen = 0 + var wasUpper = false + var wasAlphanum = false + for(var i = 0; i < targetLen; ++i) { + var targetCode = target.charCodeAt(i) + var isUpper = targetCode>=65&&targetCode<=90 + var isAlphanum = isUpper || targetCode>=97&&targetCode<=122 || targetCode>=48&&targetCode<=57 + var isBeginning = isUpper && !wasUpper || !wasAlphanum || !isAlphanum + wasUpper = isUpper + wasAlphanum = isAlphanum + if(isBeginning) beginningIndexes[beginningIndexesLen++] = i + } + return beginningIndexes +} +var prepareNextBeginningIndexes = (target) => { + target = remove_accents(target) + var targetLen = target.length + var beginningIndexes = prepareBeginningIndexes(target) + var nextBeginningIndexes = [] // new Array(targetLen) sparse array is too slow + var lastIsBeginning = beginningIndexes[0] + var lastIsBeginningI = 0 + for(var i = 0; i < targetLen; ++i) { + if(lastIsBeginning > i) { + nextBeginningIndexes[i] = lastIsBeginning + } else { + lastIsBeginning = beginningIndexes[++lastIsBeginningI] + nextBeginningIndexes[i] = lastIsBeginning===undefined ? targetLen : lastIsBeginning + } + } + return nextBeginningIndexes +} + +var preparedCache = new Map() +var preparedSearchCache = new Map() + +// the theory behind these being globals is to reduce garbage collection by not making new arrays +var matchesSimple = []; var matchesStrict = [] +var nextBeginningIndexesChanges = [] // allows straw berry to match strawberry well, by modifying the end of a substring to be considered a beginning index for the rest of the search +var keysSpacesBestScores = []; var allowPartialMatchScores = [] +var tmpTargets = []; var tmpResults = [] + +// prop = 'key' 2.5ms optimized for this case, seems to be about as fast as direct obj[prop] +// prop = 'key1.key2' 10ms +// prop = ['key1', 'key2'] 27ms +// prop = obj => obj.tags.join() ??ms +var getValue = (obj, prop) => { + var tmp = obj[prop]; if(tmp !== undefined) return tmp + if(typeof prop === 'function') return prop(obj) // this should run first. but that makes string props slower + var segs = prop + if(!Array.isArray(prop)) segs = prop.split('.') + var len = segs.length + var i = -1 + while (obj && (++i < len)) obj = obj[segs[i]] + return obj +} + +var isPrepared = (x) => { return typeof x === 'object' && typeof x._bitflags === 'number' } +var INFINITY = Infinity; var NEGATIVE_INFINITY = -INFINITY +var noResults = []; noResults.total = 0 +var NULL = null + +var noTarget = prepare('') + +// Hacked version of https://github.com/lemire/FastPriorityQueue.js +var fastpriorityqueue=r=>{var e=[],o=0,a={},v=r=>{for(var a=0,v=e[a],c=1;c<o;){var s=c+1;a=c,s<o&&e[s]._score<e[c]._score&&(a=s),e[a-1>>1]=e[a],c=1+(a<<1)}for(var f=a-1>>1;a>0&&v._score<e[f]._score;f=(a=f)-1>>1)e[a]=e[f];e[a]=v};return a.add=(r=>{var a=o;e[o++]=r;for(var v=a-1>>1;a>0&&r._score<e[v]._score;v=(a=v)-1>>1)e[a]=e[v];e[a]=r}),a.poll=(r=>{if(0!==o){var a=e[0];return e[0]=e[--o],v(),a}}),a.peek=(r=>{if(0!==o)return e[0]}),a.replaceTop=(r=>{e[0]=r,v()}),a} +var q = fastpriorityqueue() // reuse this diff --git a/config/quickshell/modules/common/functions/levendist.js b/config/quickshell/modules/common/functions/levendist.js new file mode 100644 index 00000000..90180d21 --- /dev/null +++ b/config/quickshell/modules/common/functions/levendist.js @@ -0,0 +1,141 @@ +// Original code from https://github.com/koeqaife/hyprland-material-you +// Original code license: GPLv3 +// Translated to Js from Cython with an LLM and reviewed + +function min3(a, b, c) { + return a < b && a < c ? a : b < c ? b : c; +} + +function max3(a, b, c) { + return a > b && a > c ? a : b > c ? b : c; +} + +function min2(a, b) { + return a < b ? a : b; +} + +function max2(a, b) { + return a > b ? a : b; +} + +function levenshteinDistance(s1, s2) { + let len1 = s1.length; + let len2 = s2.length; + + if (len1 === 0) return len2; + if (len2 === 0) return len1; + + if (len2 > len1) { + [s1, s2] = [s2, s1]; + [len1, len2] = [len2, len1]; + } + + let prev = new Array(len2 + 1); + let curr = new Array(len2 + 1); + + for (let j = 0; j <= len2; j++) { + prev[j] = j; + } + + for (let i = 1; i <= len1; i++) { + curr[0] = i; + for (let j = 1; j <= len2; j++) { + let cost = s1[i - 1] === s2[j - 1] ? 0 : 1; + curr[j] = min3(prev[j] + 1, curr[j - 1] + 1, prev[j - 1] + cost); + } + [prev, curr] = [curr, prev]; + } + + return prev[len2]; +} + +function partialRatio(shortS, longS) { + let lenS = shortS.length; + let lenL = longS.length; + let best = 0.0; + + if (lenS === 0) return 1.0; + + for (let i = 0; i <= lenL - lenS; i++) { + let sub = longS.slice(i, i + lenS); + let dist = levenshteinDistance(shortS, sub); + let score = 1.0 - (dist / lenS); + if (score > best) best = score; + } + + return best; +} + +function computeScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.85 * full + 0.15 * part; + + if (s1 && s2 && s1[0] !== s2[0]) { + score -= 0.05; + } + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 3) { + score -= 0.05 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.02 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.06; + } + + return Math.max(0.0, Math.min(1.0, score)); +} + +function computeTextMatchScore(s1, s2) { + if (s1 === s2) return 1.0; + + let dist = levenshteinDistance(s1, s2); + let maxLen = max2(s1.length, s2.length); + if (maxLen === 0) return 1.0; + + let full = 1.0 - (dist / maxLen); + let part = s1.length < s2.length ? partialRatio(s1, s2) : partialRatio(s2, s1); + + let score = 0.4 * full + 0.6 * part; + + let lenDiff = Math.abs(s1.length - s2.length); + if (lenDiff >= 10) { + score -= 0.02 * lenDiff / maxLen; + } + + let commonPrefixLen = 0; + let minLen = min2(s1.length, s2.length); + for (let i = 0; i < minLen; i++) { + if (s1[i] === s2[i]) { + commonPrefixLen++; + } else { + break; + } + } + score += 0.01 * commonPrefixLen; + + if (s1.includes(s2) || s2.includes(s1)) { + score += 0.2; + } + + return Math.max(0.0, Math.min(1.0, score)); +} diff --git a/config/quickshell/modules/common/functions/object_utils.js b/config/quickshell/modules/common/functions/object_utils.js new file mode 100644 index 00000000..96c632fd --- /dev/null +++ b/config/quickshell/modules/common/functions/object_utils.js @@ -0,0 +1,45 @@ +function toPlainObject(qtObj) { + if (qtObj === null || typeof qtObj !== "object") return qtObj; + + // Handle arrays + if (Array.isArray(qtObj)) { + return qtObj.map(toPlainObject); + } + + const result = ({}); + for (let key in qtObj) { + if ( + typeof qtObj[key] !== "function" && + !key.startsWith("objectName") && + !key.startsWith("children") && + !key.startsWith("object") && + !key.startsWith("parent") && + !key.startsWith("metaObject") && + !key.startsWith("destroyed") && + !key.startsWith("reloadableId") + ) { + result[key] = toPlainObject(qtObj[key]); + } + } + return result; +} + +function applyToQtObject(qtObj, jsonObj) { + if (!qtObj || typeof jsonObj !== "object" || jsonObj === null) return; + + for (let key in jsonObj) { + if (!qtObj.hasOwnProperty(key)) continue; + + // Check if the property is a QtObject (not a value) + const value = qtObj[key]; + const jsonValue = jsonObj[key]; + + // If it's an object and not an array, recurse + if (value && typeof value === "object" && !Array.isArray(value)) { + applyToQtObject(value, jsonValue); + } else { + // Otherwise, assign the value + qtObj[key] = jsonValue; + } + } +} diff --git a/config/quickshell/modules/common/functions/string_utils.js b/config/quickshell/modules/common/functions/string_utils.js new file mode 100644 index 00000000..c31edf49 --- /dev/null +++ b/config/quickshell/modules/common/functions/string_utils.js @@ -0,0 +1,53 @@ +/** + * Formats a string according to the args that are passed in + * @param { string } str + * @param {...any} args + * @returns + */ +function format(str, ...args) { + return str.replace(/{(\d+)}/g, (match, index) => + typeof args[index] !== 'undefined' ? args[index] : match + ); +} + +/** + * Returns the domain of the passed in url or null + * @param { string } url + * @returns { string| null } + */ +function getDomain(url) { + const match = url.match(/^(?:https?:\/\/)?(?:www\.)?([^\/]+)/); + return match ? match[1] : null; +} + +/** + * Returns the base url of the passed in url or null + * @param { string } url + * @returns { string | null } + */ +function getBaseUrl(url) { + const match = url.match(/^(https?:\/\/[^\/]+)(\/.*)?$/); + return match ? match[1] : null; +} + +/** + * Escapes single quotes in shell commands + * @param { string } str + * @returns { string } + */ +function shellSingleQuoteEscape(str) { + // escape single quotes + return String(str) + // .replace(/\\/g, '\\\\') + .replace(/'/g, "'\\''"); +} + +function escapeHtml(str) { + if (typeof str !== 'string') return str; + return str + .replace(/&/g, '&') + .replace(/</g, '<') + .replace(/>/g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); +} diff --git a/config/quickshell/modules/common/widgets/CliphistImage.qml b/config/quickshell/modules/common/widgets/CliphistImage.qml new file mode 100644 index 00000000..9de34450 --- /dev/null +++ b/config/quickshell/modules/common/widgets/CliphistImage.qml @@ -0,0 +1,101 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/services" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/file_utils.js" as FileUtils +import Qt5Compat.GraphicalEffects +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +Rectangle { + id: root + property string entry + property real maxWidth + property real maxHeight + + property string imageDecodePath: Directories.cliphistDecode + property string imageDecodeFileName: `${entryNumber}` + property string imageDecodeFilePath: `${imageDecodePath}/${imageDecodeFileName}` + property string source + + property int entryNumber: { + if (!root.entry) return 0 + const match = root.entry.match(/^(\d+)\t/) + return match ? parseInt(match[1]) : 0 + } + property int imageWidth: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[1]) : 0 + } + property int imageHeight: { + if (!root.entry) return 0 + const match = root.entry.match(/(\d+)x(\d+)/) + return match ? parseInt(match[2]) : 0 + } + property real scale: { + return Math.min( + root.maxWidth / imageWidth, + root.maxHeight / imageHeight, + 1 + ) + } + + color: Appearance.colors.colLayer1 + radius: Appearance.rounding.small + implicitHeight: imageHeight * scale + implicitWidth: imageWidth * scale + + Component.onCompleted: { + decodeImageProcess.running = true + } + + Process { + id: decodeImageProcess + command: ["bash", "-c", + `[ -f ${imageDecodeFilePath} ] || echo '${StringUtils.shellSingleQuoteEscape(root.entry)}' | cliphist decode > '${imageDecodeFilePath}'` + ] + onExited: (exitCode, exitStatus) => { + if (exitCode === 0) { + root.source = imageDecodeFilePath + } else { + console.error("[CliphistImage] Failed to decode image for entry:", root.entry) + root.source = "" + } + } + } + + Component.onDestruction: { + Hyprland.dispatch(`exec bash -c "[ -f '${imageDecodeFilePath}' ] && rm -f '${imageDecodeFilePath}'"`) + } + + Image { + id: image + anchors.fill: parent + + source: Qt.resolvedUrl(root.source) + fillMode: Image.PreserveAspectFit + antialiasing: true + asynchronous: true + + width: root.imageWidth * root.scale + height: root.imageHeight * root.scale + sourceSize.width: width + sourceSize.height: height + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: image.width + height: image.height + radius: root.radius + } + } + } +} + diff --git a/config/quickshell/modules/common/widgets/DialogButton.qml b/config/quickshell/modules/common/widgets/DialogButton.qml new file mode 100644 index 00000000..b799336a --- /dev/null +++ b/config/quickshell/modules/common/widgets/DialogButton.qml @@ -0,0 +1,38 @@ +import "root:/modules/common" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io + +/** + * Material 3 dialog button. See https://m3.material.io/components/dialogs/overview + */ +RippleButton { + id: button + + property string buttonText + implicitHeight: 30 + implicitWidth: buttonTextWidget.implicitWidth + 15 * 2 + buttonRadius: Appearance?.rounding.full ?? 9999 + + property color colEnabled: Appearance?.colors.colPrimary + property color colDisabled: Appearance?.m3colors.m3borderPrimary + + contentItem: StyledText { + id: buttonTextWidget + anchors.fill: parent + anchors.leftMargin: 15 + anchors.rightMargin: 15 + text: buttonText + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance?.font.pixelSize.textBase ?? 12 + color: button.enabled ? button.colEnabled : button.colDisabled + + Behavior on color { + animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this) + } + } + +} diff --git a/config/quickshell/modules/common/widgets/MaterialSymbol.qml b/config/quickshell/modules/common/widgets/MaterialSymbol.qml new file mode 100644 index 00000000..214f838e --- /dev/null +++ b/config/quickshell/modules/common/widgets/MaterialSymbol.qml @@ -0,0 +1,27 @@ +import "root:/modules/common/" +import QtQuick + +Text { + id: root + property real iconSize: Appearance?.font.pixelSize.textBase ?? 16 + property real fill: 0 + renderType: Text.NativeRendering + font.hintingPreference: Font.PreferFullHinting + verticalAlignment: Text.AlignVCenter + font.family: Appearance?.font.family.iconFont ?? "Material Symbols Rounded" + font.pixelSize: iconSize + color: Appearance.m3colors.m3primaryText + + Behavior on fill { + NumberAnimation { + duration: Appearance?.animation.elementMoveFast.duration ?? 200 + easing.type: Appearance?.animation.elementMoveFast.type ?? Easing.BezierSpline + easing.bezierCurve: Appearance?.animation.elementMoveFast.bezierCurve ?? [0.34, 0.80, 0.34, 1.00, 1, 1] + } + } + + font.variableAxes: { + "FILL": fill, + "opsz": iconSize, + } +} diff --git a/config/quickshell/modules/common/widgets/PointingHandInteraction.qml b/config/quickshell/modules/common/widgets/PointingHandInteraction.qml new file mode 100644 index 00000000..cf8b065f --- /dev/null +++ b/config/quickshell/modules/common/widgets/PointingHandInteraction.qml @@ -0,0 +1,7 @@ +import QtQuick + +MouseArea { + anchors.fill: parent + onPressed: (mouse) => mouse.accepted = false + cursorShape: Qt.PointingHandCursor +}
\ No newline at end of file diff --git a/config/quickshell/modules/common/widgets/RippleButton.qml b/config/quickshell/modules/common/widgets/RippleButton.qml new file mode 100644 index 00000000..cd7762b9 --- /dev/null +++ b/config/quickshell/modules/common/widgets/RippleButton.qml @@ -0,0 +1,185 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell.Io +import Quickshell.Widgets + +/** + * A button with ripple effect similar to in Material Design. + */ +Button { + id: root + property bool toggled + property string buttonText + property real buttonRadius: Appearance?.rounding?.small ?? 4 + property real buttonRadiusPressed: buttonRadius + property real buttonEffectiveRadius: root.down ? root.buttonRadiusPressed : root.buttonRadius + property int rippleDuration: 1200 + property bool rippleEnabled: true + property var downAction // When left clicking (down) + property var releaseAction // When left clicking (release) + property var altAction // When right clicking + property var middleClickAction // When middle clicking + + property color colBackground: ColorUtils.transparentize(Appearance?.colors.colLayer1Hover, 1) || "transparent" + property color colBackgroundHover: Appearance?.colors.colLayer1Hover ?? "#E5DFED" + property color colBackgroundToggled: Appearance?.colors.colPrimary ?? "#65558F" + property color colBackgroundToggledHover: Appearance?.colors.colPrimaryHover ?? "#77699C" + property color colRipple: Appearance?.colors.colLayer1Active ?? "#D6CEE2" + property color colRippleToggled: Appearance?.colors.colPrimaryActive ?? "#D6CEE2" + + property color buttonColor: root.enabled ? (root.toggled ? + (root.hovered ? colBackgroundToggledHover : + colBackgroundToggled) : + (root.hovered ? colBackgroundHover : + colBackground)) : colBackground + property color rippleColor: root.toggled ? colRippleToggled : colRipple + + function startRipple(x, y) { + const stateY = buttonBackground.y; + rippleAnim.x = x; + rippleAnim.y = y - stateY; + + const dist = (ox,oy) => ox*ox + oy*oy + const stateEndY = stateY + buttonBackground.height + rippleAnim.radius = Math.sqrt(Math.max(dist(0, stateY), dist(0, stateEndY), dist(width, stateY), dist(width, stateEndY))) + + rippleFadeAnim.complete(); + rippleAnim.restart(); + } + + component RippleAnim: NumberAnimation { + duration: rippleDuration + easing.type: Appearance?.animation.elementMoveEnter.type + easing.bezierCurve: Appearance?.animationCurves.standardDecel + } + + MouseArea { + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton + onPressed: (event) => { + if(event.button === Qt.RightButton) { + if (root.altAction) root.altAction(); + return; + } + if(event.button === Qt.MiddleButton) { + if (root.middleClickAction) root.middleClickAction(); + return; + } + root.down = true + if (root.downAction) root.downAction(); + if (!root.rippleEnabled) return; + const {x,y} = event + startRipple(x, y) + } + onReleased: (event) => { + root.down = false + if (event.button != Qt.LeftButton) return; + if (root.releaseAction) root.releaseAction(); + root.click() // Because the MouseArea already consumed the event + if (!root.rippleEnabled) return; + rippleFadeAnim.restart(); + } + onCanceled: (event) => { + root.down = false + if (!root.rippleEnabled) return; + rippleFadeAnim.restart(); + } + } + + RippleAnim { + id: rippleFadeAnim + target: ripple + property: "opacity" + to: 0 + } + + SequentialAnimation { + id: rippleAnim + + property real x + property real y + property real radius + + PropertyAction { + target: ripple + property: "x" + value: rippleAnim.x + } + PropertyAction { + target: ripple + property: "y" + value: rippleAnim.y + } + PropertyAction { + target: ripple + property: "opacity" + value: 1 + } + ParallelAnimation { + RippleAnim { + target: ripple + properties: "implicitWidth,implicitHeight" + from: 0 + to: rippleAnim.radius * 2 + } + } + } + + background: Rectangle { + id: buttonBackground + radius: root.buttonEffectiveRadius + implicitHeight: 50 + + color: root.buttonColor + Behavior on color { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: buttonBackground.width + height: buttonBackground.height + radius: root.buttonEffectiveRadius + } + } + + Item { + id: ripple + width: ripple.implicitWidth + height: ripple.implicitHeight + opacity: 0 + + property real implicitWidth: 0 + property real implicitHeight: 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.colorAnimation.createObject(this) + } + + RadialGradient { + anchors.fill: parent + gradient: Gradient { + GradientStop { position: 0.0; color: root.rippleColor } + GradientStop { position: 0.3; color: root.rippleColor } + GradientStop { position: 0.5; color: Qt.rgba(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, 0) } + } + } + + transform: Translate { + x: -ripple.width / 2 + y: -ripple.height / 2 + } + } + } + + contentItem: StyledText { + text: root.buttonText + } +} diff --git a/config/quickshell/modules/common/widgets/RoundCorner.qml b/config/quickshell/modules/common/widgets/RoundCorner.qml new file mode 100644 index 00000000..c9a2827a --- /dev/null +++ b/config/quickshell/modules/common/widgets/RoundCorner.qml @@ -0,0 +1,64 @@ +import QtQuick 2.9 + +Item { + id: root + + property int size: 25 + property color color: "#000000" + + onColorChanged: { + canvas.requestPaint(); + } + + property QtObject cornerEnum: QtObject { + property int topLeft: 0 + property int topRight: 1 + property int bottomLeft: 2 + property int bottomRight: 3 + } + + property int corner: cornerEnum.topLeft // Default to TopLeft + + width: size + height: size + + Canvas { + id: canvas + + anchors.fill: parent + antialiasing: true + + onPaint: { + var ctx = getContext("2d"); + var r = root.size; + + ctx.beginPath(); + switch (root.corner) { + case cornerEnum.topLeft: + ctx.arc(r, r, r, Math.PI, 3 * Math.PI / 2); + ctx.lineTo(0, 0); + break; + case cornerEnum.topRight: + ctx.arc(0, r, r, 3 * Math.PI / 2, 2 * Math.PI); + ctx.lineTo(r, 0); + break; + case cornerEnum.bottomLeft: + ctx.arc(r, 0, r, Math.PI / 2, Math.PI); + ctx.lineTo(0, r); + break; + case cornerEnum.bottomRight: + ctx.arc(0, 0, r, 0, Math.PI / 2); + ctx.lineTo(r, r); + break; + } + ctx.closePath(); + ctx.fillStyle = root.color; + ctx.fill(); + } + } + + Behavior on size { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + +} diff --git a/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml b/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml new file mode 100644 index 00000000..6e1f2e16 --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledRectangularShadow.qml @@ -0,0 +1,13 @@ +import QtQuick +import QtQuick.Effects +import "root:/modules/common" + +RectangularShadow { + required property var target + anchors.fill: target + radius: target.radius + blur: 1.2 * Appearance.sizes.elevationMargin + spread: 1 + color: Appearance.colors.colShadow + cached: true +} diff --git a/config/quickshell/modules/common/widgets/StyledText.qml b/config/quickshell/modules/common/widgets/StyledText.qml new file mode 100644 index 00000000..988c136d --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledText.qml @@ -0,0 +1,13 @@ +import "root:/modules/common" +import QtQuick + +Text { + renderType: Text.NativeRendering + verticalAlignment: Text.AlignVCenter + font { + hintingPreference: Font.PreferFullHinting + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + } + color: Appearance?.m3colors.m3primaryText ?? "black" +} diff --git a/config/quickshell/modules/common/widgets/StyledTextArea.qml b/config/quickshell/modules/common/widgets/StyledTextArea.qml new file mode 100644 index 00000000..af3cf34d --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledTextArea.qml @@ -0,0 +1,15 @@ +import "root:/modules/common" +import QtQuick +import QtQuick.Controls + +TextArea { + renderType: Text.NativeRendering + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground + placeholderTextColor: Appearance.m3colors.m3borderPrimary + font { + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + hintingPreference: Font.PreferFullHinting + } +} diff --git a/config/quickshell/modules/common/widgets/StyledToolTip.qml b/config/quickshell/modules/common/widgets/StyledToolTip.qml new file mode 100644 index 00000000..aaaad813 --- /dev/null +++ b/config/quickshell/modules/common/widgets/StyledToolTip.qml @@ -0,0 +1,60 @@ +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts + +ToolTip { + id: root + property string content + property bool extraVisibleCondition: true + property bool alternativeVisibleCondition: false + property bool internalVisibleCondition: { + const ans = (extraVisibleCondition && (parent.hovered === undefined || parent?.hovered)) || alternativeVisibleCondition + return ans + } + verticalPadding: 5 + horizontalPadding: 10 + opacity: internalVisibleCondition ? 1 : 0 + visible: opacity > 0 + + Behavior on opacity { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + background: null + + contentItem: Item { + id: contentItemBackground + implicitWidth: tooltipTextObject.width + 2 * root.horizontalPadding + implicitHeight: tooltipTextObject.height + 2 * root.verticalPadding + + Rectangle { + id: backgroundRectangle + anchors.bottom: contentItemBackground.bottom + anchors.horizontalCenter: contentItemBackground.horizontalCenter + color: Appearance?.m3colors.colTooltip ?? "#3C4043" + radius: Appearance?.rounding.verysmall ?? 7 + width: internalVisibleCondition ? (tooltipTextObject.width + 2 * padding) : 0 + height: internalVisibleCondition ? (tooltipTextObject.height + 2 * padding) : 0 + clip: true + + Behavior on width { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on height { + animation: Appearance?.animation.elementMoveFast.numberAnimation.createObject(this) + } + + StyledText { + id: tooltipTextObject + anchors.centerIn: parent + text: content + font.pixelSize: Appearance?.font.pixelSize.textSmall ?? 14 + font.hintingPreference: Font.PreferNoHinting // Prevent shaky text + color: Appearance?.m3colors.colOnTooltip ?? "#FFFFFF" + wrapMode: Text.Wrap + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/Overview.qml b/config/quickshell/modules/overview/Overview.qml new file mode 100644 index 00000000..ef5a49c3 --- /dev/null +++ b/config/quickshell/modules/overview/Overview.qml @@ -0,0 +1,279 @@ +import "root:/" +import "root:/services" +import "root:/modules/common" +import "root:/modules/common/widgets" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +Scope { + id: overviewScope + property bool dontAutoCancelSearch: false + property bool searchEnabled: ConfigOptions.search.searchEnabled + + Variants { + id: overviewVariants + model: Quickshell.screens + PanelWindow { + id: root + required property var modelData + property string searchingText: "" + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.screen) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + screen: modelData + visible: GlobalStates.overviewOpen + + WlrLayershell.namespace: "quickshell:overview" + WlrLayershell.layer: WlrLayer.Overlay + color: "transparent" + + mask: Region { + item: GlobalStates.overviewOpen ? columnLayout : null + } + HyprlandWindow.visibleMask: Region { + item: GlobalStates.overviewOpen ? columnLayout : null + } + + anchors { + top: true + left: true + right: true + bottom: true + } + + HyprlandFocusGrab { + id: grab + windows: [ root ] + property bool canBeActive: root.monitorIsFocused + active: false + onCleared: () => { + if (!active) GlobalStates.overviewOpen = false + } + } + + Connections { + target: GlobalStates + function onOverviewOpenChanged() { + if (!GlobalStates.overviewOpen) { + if (overviewScope.searchEnabled && searchWidget) { + searchWidget.disableExpandAnimation() + } + overviewScope.dontAutoCancelSearch = false; + } else { + if (!overviewScope.dontAutoCancelSearch && overviewScope.searchEnabled && searchWidget) { + searchWidget.cancelSearch() + } + delayedGrabTimer.start() + } + } + } + + Timer { + id: delayedGrabTimer + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + onTriggered: { + if (!grab.canBeActive) return + grab.active = GlobalStates.overviewOpen + } + } + + implicitWidth: columnLayout.implicitWidth + implicitHeight: columnLayout.implicitHeight + + function setSearchingText(text) { + if (overviewScope.searchEnabled && searchWidget) { + searchWidget.setSearchingText(text); + } + } + + ColumnLayout { + id: columnLayout + visible: GlobalStates.overviewOpen + anchors { + horizontalCenter: parent.horizontalCenter + top: ConfigOptions.overview.position === 0 ? parent.top : undefined + verticalCenter: ConfigOptions.overview.position === 1 ? parent.verticalCenter : undefined + bottom: ConfigOptions.overview.position === 2 ? parent.bottom : undefined + } + + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Escape) { + GlobalStates.overviewOpen = false; + } + } + + Item { + height: 1 + width: 1 + } + + // Conditionally render SearchWidget - only exists when searchEnabled is true + SearchWidget { + id: searchWidget + Layout.alignment: Qt.AlignHCenter + visible: overviewScope.searchEnabled + height: overviewScope.searchEnabled ? implicitHeight : 0 + Layout.preferredHeight: overviewScope.searchEnabled ? implicitHeight : 0 + onSearchingTextChanged: (text) => { + root.searchingText = searchingText + } + } + + Item { + Layout.preferredHeight: overviewScope.searchEnabled ? 0 : 20 + Layout.fillWidth: true + visible: !overviewScope.searchEnabled + } + + Loader { + id: overviewLoader + active: GlobalStates.overviewOpen + sourceComponent: OverviewWidget { + panelWindow: root + // Show OverviewWidget when search is disabled OR when search text is empty + visible: !overviewScope.searchEnabled || (root.searchingText == "") + } + } + } + } + } + + IpcHandler { + target: "overview" + + function toggle() { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + function close() { + GlobalStates.overviewOpen = false + } + function open() { + GlobalStates.overviewOpen = true + } + function toggleReleaseInterrupt() { + GlobalStates.superReleaseMightTrigger = false + } + // Add function to control search + function toggleSearch() { + overviewScope.searchEnabled = !overviewScope.searchEnabled + } + function enableSearch() { + overviewScope.searchEnabled = true + } + function disableSearch() { + overviewScope.searchEnabled = false + } + } + + GlobalShortcut { + name: "overviewToggle" + description: qsTr("Toggles overview on press") + + onPressed: { + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + } + + GlobalShortcut { + name: "overviewClose" + description: qsTr("Closes overview") + + onPressed: { + GlobalStates.overviewOpen = false + } + } + + GlobalShortcut { + name: "overviewToggleRelease" + description: qsTr("Toggles overview on release") + + onPressed: { + GlobalStates.superReleaseMightTrigger = true + } + + onReleased: { + if (!GlobalStates.superReleaseMightTrigger) { + GlobalStates.superReleaseMightTrigger = true + return + } + GlobalStates.overviewOpen = !GlobalStates.overviewOpen + } + } + + GlobalShortcut { + name: "overviewToggleReleaseInterrupt" + description: qsTr("Interrupts possibility of overview being toggled on release. ") + + qsTr("This is necessary because GlobalShortcut.onReleased in quickshell triggers whether or not you press something else while holding the key. ") + + qsTr("To make sure this works consistently, use binditn = MODKEYS, catchall in an automatically triggered submap that includes everything.") + + onPressed: { + GlobalStates.superReleaseMightTrigger = false + } + } + + // Only enable clipboard/emoji shortcuts when search is enabled + GlobalShortcut { + name: "overviewClipboardToggle" + description: qsTr("Toggle clipboard query on overview widget") + + onPressed: { + if (!overviewScope.searchEnabled) return; // Skip if search disabled + + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText( + ConfigOptions.search.prefix.clipboard + ); + GlobalStates.overviewOpen = true; + return + } + } + } + } + + GlobalShortcut { + name: "overviewEmojiToggle" + description: qsTr("Toggle emoji query on overview widget") + + onPressed: { + if (!overviewScope.searchEnabled) return; // Skip if search disabled + + if (GlobalStates.overviewOpen && overviewScope.dontAutoCancelSearch) { + GlobalStates.overviewOpen = false; + return; + } + for (let i = 0; i < overviewVariants.instances.length; i++) { + let panelWindow = overviewVariants.instances[i]; + if (panelWindow.modelData.name == Hyprland.focusedMonitor.name) { + overviewScope.dontAutoCancelSearch = true; + panelWindow.setSearchingText( + ConfigOptions.search.prefix.emojis + ); + GlobalStates.overviewOpen = true; + return + } + } + } + } + + // Optional: Add shortcut to toggle search functionality + GlobalShortcut { + name: "overviewToggleSearch" + description: qsTr("Toggle search functionality in overview") + + onPressed: { + overviewScope.searchEnabled = !overviewScope.searchEnabled + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/OverviewWidget.qml b/config/quickshell/modules/overview/OverviewWidget.qml new file mode 100644 index 00000000..2ea8d58a --- /dev/null +++ b/config/quickshell/modules/overview/OverviewWidget.qml @@ -0,0 +1,343 @@ +import "root:/" +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import QtQuick +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Wayland +import Quickshell.Hyprland + +Item { + id: root + required property var panelWindow + readonly property HyprlandMonitor monitor: Hyprland.monitorFor(panelWindow.screen) + readonly property var toplevels: ToplevelManager.toplevels + readonly property int workspacesShown: ConfigOptions.overview.numOfRows * ConfigOptions.overview.numOfCols + readonly property int workspaceGroup: Math.floor((monitor.activeWorkspace?.id - 1) / workspacesShown) + property bool monitorIsFocused: (Hyprland.focusedMonitor?.id == monitor.id) + property var windows: HyprlandData.windowList + property var windowByAddress: HyprlandData.windowByAddress + property var windowAddresses: HyprlandData.addresses + property var monitorData: HyprlandData.monitors.find(m => m.id === root.monitor.id) + property real scale: ConfigOptions.overview.scale + property color activeBorderColor: Appearance.m3colors.m3accentSecondary + + property real workspaceImplicitWidth: Math.max(100, (monitorData?.transform % 2 === 1) ? + ((monitor.height - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale) : + ((monitor.width - monitorData?.reserved[0] - monitorData?.reserved[2]) * root.scale / monitor.scale)) + property real workspaceImplicitHeight: Math.max(60, (monitorData?.transform % 2 === 1) ? + ((monitor.width - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale) : + ((monitor.height - monitorData?.reserved[1] - monitorData?.reserved[3]) * root.scale / monitor.scale)) + + property real workspaceNumberMargin: 80 + property real workspaceNumberSize: (ConfigOptions.overview.workspaceNumberSize > 0) + ? ConfigOptions.overview.workspaceNumberSize + : Math.min(workspaceImplicitHeight, workspaceImplicitWidth) * monitor.scale + property int workspaceZ: 0 + property int windowZ: 1 + property int windowDraggingZ: 99999 + property real workspaceSpacing: 5 + + property int draggingFromWorkspace: -1 + property int draggingTargetWorkspace: -1 + + implicitWidth: overviewBackground.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: overviewBackground.implicitHeight + Appearance.sizes.elevationMargin * 2 + + property Component windowComponent: OverviewWindow {} + property list<OverviewWindow> windowWidgets: [] + + // Shared wallpaper image - loaded once and reused + Image { + id: sharedWallpaper + source: Appearance.background_image || "" + visible: false // Hidden as it's only used as a source + cache: true + asynchronous: true + smooth: true + opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque) + } + + StyledRectangularShadow { + target: overviewBackground + } + Rectangle { // Background + id: overviewBackground + property real padding: 10 + anchors.fill: parent + anchors.margins: Appearance.sizes.elevationMargin + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.2) + border.width : 2 + + implicitWidth: workspaceColumnLayout.implicitWidth + padding * 2 + implicitHeight: workspaceColumnLayout.implicitHeight + padding * 2 + radius: Appearance.rounding.screenRounding * root.scale + padding + color: Appearance.colors.colLayer0 + + + ColumnLayout { // Workspaces + id: workspaceColumnLayout + + z: root.workspaceZ + anchors.centerIn: parent + spacing: workspaceSpacing + Repeater { + model: ConfigOptions.overview.numOfRows + delegate: RowLayout { + id: row + property int rowIndex: index + spacing: workspaceSpacing + + Repeater { // Workspace repeater + model: ConfigOptions.overview.numOfCols + Rectangle { // Workspace + id: workspace + property int colIndex: index + property int workspaceValue: root.workspaceGroup * workspacesShown + rowIndex * ConfigOptions.overview.numOfCols + colIndex + 1 + property color defaultWorkspaceColor: Appearance.colors.colLayer1 + property color hoveredWorkspaceColor: ColorUtils.mix(defaultWorkspaceColor, Appearance.colors.colLayer1Hover, 0.1) + property color hoveredBorderColor: Appearance.colors.colLayer2Hover + property bool hoveredWhileDragging: false + readonly property int padding: ConfigOptions.overview.windowPadding + + Layout.preferredWidth: root.workspaceImplicitWidth + Layout.preferredHeight: root.workspaceImplicitHeight + Layout.minimumWidth: 100 + Layout.minimumHeight: 60 + + width: root.workspaceImplicitWidth + height: root.workspaceImplicitHeight + color: "transparent" + radius: Appearance.rounding.screenRounding * root.scale + clip: true + opacity: Appearance.workpaceTransparency // Adds slight transparency (0.0 = fully transparent, 1.0 = fully opaque) + + + // Efficient wallpaper using ShaderEffectSource + Rectangle { + id: wallpaperContainer + anchors.fill: parent + anchors.margins: 2 // Leave space for border + radius: workspace.radius - 2 + color: workspace.defaultWorkspaceColor // Fallback color + clip: true + + ShaderEffectSource { + id: wallpaperSource + anchors.fill: parent + sourceItem: sharedWallpaper + visible: sharedWallpaper.status === Image.Ready + smooth: true + + // Scale to fill while preserving aspect ratio + transform: Scale { + property real aspectRatio: sharedWallpaper.implicitWidth / Math.max(1, sharedWallpaper.implicitHeight) + property real containerRatio: wallpaperContainer.width / Math.max(1, wallpaperContainer.height) + + xScale: aspectRatio > containerRatio ? + wallpaperContainer.height * aspectRatio / wallpaperContainer.width : 1 + yScale: aspectRatio > containerRatio ? + 1 : wallpaperContainer.width / (wallpaperContainer.height * aspectRatio) + + origin.x: wallpaperContainer.width / 2 + origin.y: wallpaperContainer.height / 2 + } + } + + // Fallback when image fails to load or isn't ready + Rectangle { + anchors.fill: parent + color: workspace.defaultWorkspaceColor + visible: sharedWallpaper.status !== Image.Ready + } + + // Optional: Add overlay for better text readability and hover effects + Rectangle { + anchors.fill: parent + color: hoveredWhileDragging ? hoveredWorkspaceColor : "black" + opacity: hoveredWhileDragging ? 0.3 : 0.1 + } + } + + // Border overlay - on top of wallpaper + Rectangle { + anchors.fill: parent + color: "transparent" + radius: parent.radius + border.width: 1 + border.color: hoveredWhileDragging ? hoveredBorderColor : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.6) + z: 10 // Ensure it's on top + } + + StyledText { + // Position in top-left corner with padding + anchors.top: parent.top + anchors.left: parent.left + anchors.topMargin: 12 // Padding from top edge + anchors.leftMargin: 12 // Padding from left edge + + text: workspaceValue + font.pixelSize: root.workspaceNumberSize * root.scale + font.weight: Font.DemiBold + color: ColorUtils.transparentize(Appearance.colors.colOnLayer1, 0.8) + horizontalAlignment: Text.AlignLeft + verticalAlignment: Text.AlignTop + z: 15 // Above border + } + + MouseArea { + id: workspaceArea + anchors.fill: parent + acceptedButtons: Qt.LeftButton + z: 20 // Above all visual elements + onClicked: { + if (root.draggingTargetWorkspace === -1) { + GlobalStates.overviewOpen = false + Hyprland.dispatch(`workspace ${workspaceValue}`) + } + } + } + + DropArea { + anchors.fill: parent + z: 20 // Same level as MouseArea + onEntered: { + root.draggingTargetWorkspace = workspaceValue + if (root.draggingFromWorkspace == root.draggingTargetWorkspace) return; + hoveredWhileDragging = true + } + onExited: { + hoveredWhileDragging = false + if (root.draggingTargetWorkspace == workspaceValue) root.draggingTargetWorkspace = -1 + } + } + + } + } + } + } + } + + Item { // Windows & focused workspace indicator + id: windowSpace + anchors.centerIn: parent + implicitWidth: workspaceColumnLayout.implicitWidth + implicitHeight: workspaceColumnLayout.implicitHeight + + Repeater { // Window repeater + model: ScriptModel { + values: windowAddresses.filter((address) => { + var win = windowByAddress[address] + return (root.workspaceGroup * root.workspacesShown < win?.workspace?.id && win?.workspace?.id <= (root.workspaceGroup + 1) * root.workspacesShown) + }) + } + delegate: OverviewWindow { + id: window + windowData: windowByAddress[modelData] + monitorData: root.monitorData + scale: root.scale + availableWorkspaceWidth: root.workspaceImplicitWidth + availableWorkspaceHeight: root.workspaceImplicitHeight + + property bool atInitPosition: (initX == x && initY == y) + restrictToWorkspace: Drag.active || atInitPosition + + property int workspaceColIndex: (windowData?.workspace.id - 1) % ConfigOptions.overview.numOfCols + property int workspaceRowIndex: Math.floor((windowData?.workspace.id - 1) % root.workspacesShown / ConfigOptions.overview.numOfCols) + xOffset: (root.workspaceImplicitWidth + workspaceSpacing) * workspaceColIndex + yOffset: (root.workspaceImplicitHeight + workspaceSpacing) * workspaceRowIndex + + Timer { + id: updateWindowPosition + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + window.x = Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + window.y = Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + } + } + + z: atInitPosition ? root.windowZ : root.windowDraggingZ + Drag.hotSpot.x: targetWindowWidth / 2 + Drag.hotSpot.y: targetWindowHeight / 2 + MouseArea { + id: dragArea + anchors.fill: parent + hoverEnabled: true + onEntered: hovered = true + onExited: hovered = false + acceptedButtons: Qt.LeftButton | Qt.MiddleButton + drag.target: parent + onPressed: { + root.draggingFromWorkspace = windowData?.workspace.id + window.pressed = true + window.Drag.active = true + window.Drag.source = window + } + onReleased: { + const targetWorkspace = root.draggingTargetWorkspace + window.pressed = false + window.Drag.active = false + root.draggingFromWorkspace = -1 + if (targetWorkspace !== -1 && targetWorkspace !== windowData?.workspace.id) { + Hyprland.dispatch(`movetoworkspacesilent ${targetWorkspace}, address:${window.windowData?.address}`) + updateWindowPosition.restart() + } + else { + window.x = window.initX + window.y = window.initY + } + } + onClicked: (event) => { + if (!windowData) return; + + if (event.button === Qt.LeftButton) { + GlobalStates.overviewOpen = false + Hyprland.dispatch(`focuswindow address:${windowData.address}`) + event.accepted = true + } else if (event.button === Qt.MiddleButton) { + Hyprland.dispatch(`closewindow address:${windowData.address}`) + event.accepted = true + } + } + + StyledToolTip { + extraVisibleCondition: false + alternativeVisibleCondition: dragArea.containsMouse && !window.Drag.active + content: `${windowData.title}\n[${windowData.class}] ${windowData.xwayland ? "[XWayland] " : ""}\n` + } + } + } + } + + Rectangle { // Focused workspace indicator + id: focusedWorkspaceIndicator + property int activeWorkspaceInGroup: monitor.activeWorkspace?.id - (root.workspaceGroup * root.workspacesShown) + property int activeWorkspaceRowIndex: Math.floor((activeWorkspaceInGroup - 1) / ConfigOptions.overview.numOfCols) + property int activeWorkspaceColIndex: (activeWorkspaceInGroup - 1) % ConfigOptions.overview.numOfCols + x: (root.workspaceImplicitWidth + workspaceSpacing) * activeWorkspaceColIndex + y: (root.workspaceImplicitHeight + workspaceSpacing) * activeWorkspaceRowIndex + z: root.windowZ + width: Math.max(100, root.workspaceImplicitWidth) + height: Math.max(60, root.workspaceImplicitHeight) + color: "transparent" + radius: Appearance.rounding.screenRounding * root.scale + border.width: 2 + border.color: root.activeBorderColor + visible: width > 0 && height > 0 && activeWorkspaceInGroup > 0 + Behavior on x { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + Behavior on y { + animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this) + } + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/OverviewWindow.qml b/config/quickshell/modules/overview/OverviewWindow.qml new file mode 100644 index 00000000..449a98c4 --- /dev/null +++ b/config/quickshell/modules/overview/OverviewWindow.qml @@ -0,0 +1,94 @@ +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import Qt5Compat.GraphicalEffects +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.Io +import Quickshell.Hyprland + +Rectangle { // Window + id: root + property var windowData + property var monitorData + property var scale + property var availableWorkspaceWidth + property var availableWorkspaceHeight + property bool restrictToWorkspace: true + property real initX: Math.max((windowData?.at[0] - monitorData?.reserved[0]) * root.scale, 0) + xOffset + property real initY: Math.max((windowData?.at[1] - monitorData?.reserved[1]) * root.scale, 0) + yOffset + property real xOffset: 0 + property real yOffset: 0 + + property var targetWindowWidth: windowData?.size[0] * scale + property var targetWindowHeight: windowData?.size[1] * scale + property bool hovered: false + property bool pressed: false + + property var iconToWindowRatio: 0.35 + property var xwaylandIndicatorToIconRatio: 0.35 + property var iconToWindowRatioCompact: 0.6 + property var iconPath: Quickshell.iconPath(AppSearch.guessIcon(windowData?.class), "image-missing") + property bool compactMode: Appearance.font.pixelSize.textSmall * 4 > targetWindowHeight || Appearance.font.pixelSize.textSmall * 4 > targetWindowWidth + + property bool indicateXWayland: (ConfigOptions.overview.showXwaylandIndicator && windowData?.xwayland) ?? false + + x: initX + y: initY + width: Math.min(windowData?.size[0] * root.scale, (restrictToWorkspace ? windowData?.size[0] : availableWorkspaceWidth - x + xOffset)) + height: Math.min(windowData?.size[1] * root.scale, (restrictToWorkspace ? windowData?.size[1] : availableWorkspaceHeight - y + yOffset)) + + radius: Appearance.rounding.windowRounding * root.scale + color: pressed ? Appearance.colors.colLayer2Active : hovered ? Appearance.colors.colLayer2Hover : Appearance.colors.colLayer2 + // border.color : ColorUtils.transparentize(Appearance.m3colors.m3outline, 0.9) + border.color : ColorUtils.transparentize(Appearance.m3colors.m3borderPrimary, 0.4) + border.pixelAligned : false + border.width : 2 + + Behavior on x { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on y { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on width { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + Behavior on height { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + + ColumnLayout { + anchors.verticalCenter: parent.verticalCenter + anchors.left: parent.left + anchors.right: parent.right + spacing: Appearance.font.pixelSize.textSmall * 0.5 + + IconImage { + id: windowIcon + Layout.alignment: Qt.AlignHCenter + source: root.iconPath + implicitSize: Math.min(targetWindowWidth, targetWindowHeight) * (root.compactMode ? root.iconToWindowRatioCompact : root.iconToWindowRatio) + + Behavior on implicitSize { + animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this) + } + } + + StyledText { + Layout.leftMargin: 10 + Layout.rightMargin: 10 + visible: !compactMode + Layout.fillWidth: true + Layout.fillHeight: true + horizontalAlignment: Text.AlignHCenter + font.pixelSize: Appearance.font.pixelSize.textSmall + font.italic: indicateXWayland ? true : false + elide: Text.ElideRight + text: windowData?.title ?? "" + } + } +}
\ No newline at end of file diff --git a/config/quickshell/modules/overview/SearchItem.qml b/config/quickshell/modules/overview/SearchItem.qml new file mode 100644 index 00000000..1357d03c --- /dev/null +++ b/config/quickshell/modules/overview/SearchItem.qml @@ -0,0 +1,220 @@ +// pragma NativeMethodBehavior: AcceptThisObject +import "root:/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/color_utils.js" as ColorUtils +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Widgets +import Quickshell.Hyprland + +RippleButton { + id: root + property var entry + property string query + property bool entryShown: entry?.shown ?? true + property string itemType: entry?.type + property string itemName: entry?.name + property string itemIcon: entry?.icon ?? "" + property var itemExecute: entry?.execute + property string fontType: entry?.fontType ?? "uiFont" + property string itemClickActionName: entry?.clickActionName + property string bigText: entry?.bigText ?? "" + property string materialSymbol: entry?.materialSymbol ?? "" + property string cliphistRawString: entry?.cliphistRawString ?? "" + + property string highlightPrefix: `<u><font color="${Appearance.colors.colPrimary}">` + property string highlightSuffix: `</font></u>` + function highlightContent(content, query) { + if (!query || query.length === 0 || content == query || fontType === "codeFont") + return StringUtils.escapeHtml(content); + + let contentLower = content.toLowerCase(); + let queryLower = query.toLowerCase(); + + let result = ""; + let lastIndex = 0; + let qIndex = 0; + + for (let i = 0; i < content.length && qIndex < query.length; i++) { + if (contentLower[i] === queryLower[qIndex]) { + // Add non-highlighted part (escaped) + if (i > lastIndex) + result += StringUtils.escapeHtml(content.slice(lastIndex, i)); + // Add highlighted character (escaped) + result += root.highlightPrefix + StringUtils.escapeHtml(content[i]) + root.highlightSuffix; + lastIndex = i + 1; + qIndex++; + } + } + // Add the rest of the string (escaped) + if (lastIndex < content.length) + result += StringUtils.escapeHtml(content.slice(lastIndex)); + + return result; + } + property string displayContent: highlightContent(root.itemName, root.query) + + property list<string> urls: { + if (!root.itemName) return []; + // Regular expression to match URLs + const urlRegex = /https?:\/\/[^\s<>"{}|\\^`[\]]+/gi; + const matches = root.itemName?.match(urlRegex) + ?.filter(url => !url.includes("…")) // Elided = invalid + return matches ? matches : []; + } + + visible: root.entryShown + property int horizontalMargin: 10 + property int buttonHorizontalPadding: 10 + property int buttonVerticalPadding: 5 + property bool keyboardDown: false + + implicitHeight: rowLayout.implicitHeight + root.buttonVerticalPadding * 2 + implicitWidth: rowLayout.implicitWidth + root.buttonHorizontalPadding * 2 + buttonRadius: Appearance.rounding.normal + colBackground: (root.down || root.keyboardDown) ? Appearance.colors.colLayer1Active : + ((root.hovered || root.focus) ? Appearance.colors.colLayer1Hover : + ColorUtils.transparentize(Appearance.m3colors.m3layerBackground3, 1)) + colBackgroundHover: Appearance.colors.colLayer1Hover + colRipple: Appearance.colors.colLayer1Active + + background { + anchors.fill: root + anchors.leftMargin: root.horizontalMargin + anchors.rightMargin: root.horizontalMargin + } + + PointingHandInteraction {} + onClicked: { + root.itemExecute() + Hyprland.dispatch("global quickshell:overviewClose") + } + Keys.onPressed: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.keyboardDown = true + root.clicked() + event.accepted = true; + } + } + Keys.onReleased: (event) => { + if (event.key === Qt.Key_Return || event.key === Qt.Key_Enter) { + root.keyboardDown = false + event.accepted = true; + } + } + + RowLayout { + id: rowLayout + spacing: iconLoader.sourceComponent === null ? 0 : 10 + anchors.fill: parent + anchors.leftMargin: root.horizontalMargin + root.buttonHorizontalPadding + anchors.rightMargin: root.horizontalMargin + root.buttonHorizontalPadding + + // Icon + Loader { + id: iconLoader + active: true + sourceComponent: root.materialSymbol !== "" ? materialSymbolComponent : + root.bigText ? bigTextComponent : + root.itemIcon !== "" ? iconImageComponent : + null + } + + Component { + id: iconImageComponent + IconImage { + source: Quickshell.iconPath(root.itemIcon, "image-missing") + width: 35 + height: 35 + } + } + + Component { + id: materialSymbolComponent + MaterialSymbol { + text: root.materialSymbol + iconSize: 30 + color: Appearance.m3colors.m3surfaceText + } + } + + Component { + id: bigTextComponent + StyledText { + text: root.bigText + font.pixelSize: Appearance.font.pixelSize.textLarge + color: Appearance.m3colors.m3surfaceText + } + } + + // Main text + ColumnLayout { + id: contentColumn + Layout.fillWidth: true + Layout.alignment: Qt.AlignVCenter + spacing: 0 + StyledText { + font.pixelSize: Appearance.font.pixelSize.textSmall + color: Appearance.colors.colSubtext + visible: root.itemType && root.itemType != qsTr("App") + text: root.itemType + } + RowLayout { + Loader { // Checkmark for copied clipboard entry + visible: itemName == Quickshell.clipboardText && root.cliphistRawString + active: itemName == Quickshell.clipboardText && root.cliphistRawString + sourceComponent: Rectangle { + implicitWidth: activeText.implicitHeight + implicitHeight: activeText.implicitHeight + radius: Appearance.rounding.full + color: Appearance.colors.colPrimary + MaterialSymbol { + id: activeText + anchors.centerIn: parent + text: "check" + font.pixelSize: Appearance.font.pixelSize.textMedium + color: Appearance.m3colors.m3accentPrimaryText + } + } + } + StyledText { // Item name/content + Layout.fillWidth: true + id: nameText + textFormat: Text.StyledText // RichText also works, but StyledText ensures elide work + font.pixelSize: Appearance.font.pixelSize.textBase + font.family: Appearance.font.family[root.fontType] + color: Appearance.m3colors.m3surfaceText + horizontalAlignment: Text.AlignLeft + elide: Text.ElideRight + text: `${root.displayContent}` + } + } + Loader { // Clipboard image preview + active: root.cliphistRawString && /^\d+\t\[\[.*binary data.*\d+x\d+.*\]\]$/.test(root.cliphistRawString) + sourceComponent: CliphistImage { + Layout.fillWidth: true + entry: root.cliphistRawString + maxWidth: contentColumn.width + maxHeight: 140 + } + } + } + + // Action text + StyledText { + Layout.fillWidth: false + visible: (root.hovered || root.focus) + id: clickAction + font.pixelSize: Appearance.font.pixelSize.textMedium + color: Appearance.colors.colSubtext + horizontalAlignment: Text.AlignRight + text: root.itemClickActionName + } + } +} diff --git a/config/quickshell/modules/overview/SearchWidget.qml b/config/quickshell/modules/overview/SearchWidget.qml new file mode 100644 index 00000000..f84aa558 --- /dev/null +++ b/config/quickshell/modules/overview/SearchWidget.qml @@ -0,0 +1,425 @@ +import "root:/" +import "root:/services/" +import "root:/modules/common" +import "root:/modules/common/widgets" +import "root:/modules/common/functions/string_utils.js" as StringUtils +import Qt5Compat.GraphicalEffects +import Qt.labs.platform +import QtQuick +import QtQuick.Controls +import QtQuick.Effects +import QtQuick.Layouts +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland + +Item { // Wrapper + id: root + readonly property string xdgConfigHome: Directories.config + property string searchingText: "" + property bool showResults: searchingText != "" + property real searchBarHeight: searchBar.height + Appearance.sizes.elevationMargin * 2 + implicitWidth: searchWidgetContent.implicitWidth + Appearance.sizes.elevationMargin * 2 + implicitHeight: searchWidgetContent.implicitHeight + Appearance.sizes.elevationMargin * 2 + + property string mathResult: "" + + function disableExpandAnimation() { + searchWidthBehavior.enabled = false; + } + + function cancelSearch() { + searchInput.selectAll() + root.searchingText = "" + searchWidthBehavior.enabled = true; + } + + function setSearchingText(text) { + searchInput.text = text; + root.searchingText = text; + } + + property var searchActions: [ + { + action: "img", + execute: () => { + executor.executeCommand(Directories.wallpaperSwitchScriptPath) + } + }, + { + action: "dark", + execute: () => { + executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode dark --noswitch`) + } + }, + { + action: "light", + execute: () => { + executor.executeCommand(`${Directories.wallpaperSwitchScriptPath} --mode light --noswitch`) + } + }, + { + action: "accentcolor", + execute: (args) => { + executor.executeCommand( + `${Directories.wallpaperSwitchScriptPath} --noswitch --color ${args != '' ? ("'"+args+"'") : ""}` + ) + } + }, + { + action: "todo", + execute: (args) => { + Todo.addTask(args) + } + }, + ] + + function focusFirstItemIfNeeded() { + if (searchInput.focus) appResults.currentIndex = 0; // Focus the first item + } + + Timer { + id: nonAppResultsTimer + interval: ConfigOptions.search.nonAppResultDelay + onTriggered: { + mathProcess.calculateExpression(root.searchingText); + } + } + + Process { + id: mathProcess + property list<string> baseCommand: ["qalc", "-t"] + function calculateExpression(expression) { + // mathProcess.running = false + mathProcess.command = baseCommand.concat(expression) + mathProcess.running = true + } + stdout: SplitParser { + onRead: data => { + root.mathResult = data + root.focusFirstItemIfNeeded() + } + } + } + + Process { + id: executor + property list<string> baseCommand: ["bash", "-c"] + function executeCommand(command) { + executor.command = baseCommand.concat( + `${command} || ${ConfigOptions.apps.terminal} fish -C 'echo "${qsTr("Searching for package with that command")}..." && pacman -F ${command}'` + ) + executor.startDetached() + } + } + + Keys.onPressed: (event) => { + // Prevent Esc and Backspace from registering + if (event.key === Qt.Key_Escape) return; + + // Handle Backspace: focus and delete character if not focused + if (event.key === Qt.Key_Backspace) { + if (!searchInput.activeFocus) { + searchInput.forceActiveFocus(); + if (event.modifiers & Qt.ControlModifier) { + // Delete word before cursor + let text = searchInput.text; + let pos = searchInput.cursorPosition; + if (pos > 0) { + // Find the start of the previous word + let left = text.slice(0, pos); + let match = left.match(/(\s*\S+)\s*$/); + let deleteLen = match ? match[0].length : 1; + searchInput.text = text.slice(0, pos - deleteLen) + text.slice(pos); + searchInput.cursorPosition = pos - deleteLen; + } + } else { + // Delete character before cursor if any + if (searchInput.cursorPosition > 0) { + searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition - 1) + + searchInput.text.slice(searchInput.cursorPosition); + searchInput.cursorPosition -= 1; + } + } + // Always move cursor to end after programmatic edit + searchInput.cursorPosition = searchInput.text.length; + event.accepted = true; + } + // If already focused, let TextField handle it + return; + } + + // Only handle visible printable characters (ignore control chars, arrows, etc.) + if ( + event.text && + event.text.length === 1 && + event.key !== Qt.Key_Enter && + event.key !== Qt.Key_Return && + event.text.charCodeAt(0) >= 0x20 // ignore control chars like Backspace, Tab, etc. + ) { + if (!searchInput.activeFocus) { + searchInput.forceActiveFocus(); + // Insert the character at the cursor position + searchInput.text = searchInput.text.slice(0, searchInput.cursorPosition) + + event.text + + searchInput.text.slice(searchInput.cursorPosition); + searchInput.cursorPosition += 1; + event.accepted = true; + } + } + } + + StyledRectangularShadow { + target: searchWidgetContent + } + Rectangle { // Background + id: searchWidgetContent + anchors.centerIn: parent + implicitWidth: columnLayout.implicitWidth + implicitHeight: columnLayout.implicitHeight + radius: Appearance.rounding.large + color: Appearance.colors.colLayer0 + + ColumnLayout { + id: columnLayout + anchors.centerIn: parent + spacing: 0 + + // clip: true + layer.enabled: true + layer.effect: OpacityMask { + maskSource: Rectangle { + width: searchWidgetContent.width + height: searchWidgetContent.width + radius: searchWidgetContent.radius + } + } + + RowLayout { + id: searchBar + spacing: 5 + MaterialSymbol { + id: searchIcon + Layout.leftMargin: 15 + iconSize: Appearance.font.pixelSize.iconLarge + color: Appearance.m3colors.m3surfaceText + text: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? 'content_paste_search' : '' + } + TextField { // Search box + id: searchInput + + focus: GlobalStates.overviewOpen + Layout.rightMargin: 15 + padding: 15 + renderType: Text.NativeRendering + font { + family: Appearance?.font.family.uiFont ?? "sans-serif" + pixelSize: Appearance?.font.pixelSize.textBase ?? 15 + hintingPreference: Font.PreferFullHinting + } + color: activeFocus ? Appearance.m3colors.m3surfaceText : Appearance.m3colors.m3secondaryText + selectedTextColor: Appearance.m3colors.m3selectionText + selectionColor: Appearance.m3colors.m3selectionBackground + placeholderText: qsTr("Search, calculate or run") + placeholderTextColor: Appearance.m3colors.m3borderPrimary + implicitWidth: root.searchingText == "" ? Appearance.sizes.searchWidthCollapsed : Appearance.sizes.searchWidth + + Behavior on implicitWidth { + id: searchWidthBehavior + enabled: false + NumberAnimation { + duration: 300 + easing.type: Appearance.animation.elementMove.type + easing.bezierCurve: Appearance.animation.elementMove.bezierCurve + } + } + + onTextChanged: root.searchingText = text + + onAccepted: { + if (appResults.count > 0) { + // Get the first visible delegate and trigger its click + let firstItem = appResults.itemAtIndex(0); + if (firstItem && firstItem.clicked) { + firstItem.clicked(); + } + } + } + + background: null + + cursorDelegate: Rectangle { + width: 1 + color: searchInput.activeFocus ? Appearance.colors.colPrimary : "transparent" + radius: 1 + } + } + } + + Rectangle { // Separator + visible: root.showResults + Layout.fillWidth: true + height: 1 + color: Appearance.m3colors.m3borderSecondary + } + + ListView { // App results + id: appResults + visible: root.showResults + Layout.fillWidth: true + implicitHeight: Math.min(600, appResults.contentHeight + topMargin + bottomMargin) + clip: true + topMargin: 10 + bottomMargin: 10 + spacing: 2 + KeyNavigation.up: searchBar + highlightMoveDuration : 100 + + onFocusChanged: { + if(focus) appResults.currentIndex = 1; + } + + Connections { + target: root + function onSearchingTextChanged() { + if (appResults.count > 0) + appResults.currentIndex = 0; + } + } + + model: ScriptModel { + id: model + values: { // Search results are handled here + ////////////////// Skip? ////////////////// + if(root.searchingText == "") return []; + + ///////////// Special cases /////////////// + if (root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard)) { // Clipboard + const searchString = root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length); + return Cliphist.fuzzyQuery(searchString).map(entry => { + return { + cliphistRawString: entry, + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: "", + type: `#${entry.match(/^\s*(\S+)/)?.[1] || ""}`, + execute: () => { + Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(entry)}' | cliphist decode | wl-copy`); + } + }; + }).filter(Boolean); + } + if (root.searchingText.startsWith(ConfigOptions.search.prefix.emojis)) { // Clipboard + const searchString = root.searchingText.slice(ConfigOptions.search.prefix.emojis.length); + return Emojis.fuzzyQuery(searchString).map(entry => { + return { + cliphistRawString: entry, + bigText: entry.match(/^\s*(\S+)/)?.[1] || "", + name: entry.replace(/^\s*\S+\s+/, ""), + clickActionName: "", + type: "Emoji", + execute: () => { + Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(entry.match(/^\s*(\S+)/)?.[1])}'`); + } + }; + }).filter(Boolean); + } + + + ////////////////// Init /////////////////// + nonAppResultsTimer.restart(); + const mathResultObject = { + name: root.mathResult, + clickActionName: qsTr("Copy"), + type: qsTr("Math result"), + fontType: "monospace", + materialSymbol: 'calculate', + execute: () => { + Hyprland.dispatch(`exec wl-copy '${StringUtils.shellSingleQuoteEscape(root.mathResult)}'`) + } + } + const commandResultObject = { + name: searchingText.replace("file://", ""), + clickActionName: qsTr("Run"), + type: qsTr("Run command"), + fontType: "monospace", + materialSymbol: 'terminal', + execute: () => { + executor.executeCommand(searchingText.startsWith('sudo') ? `${ConfigOptions.apps.terminal} fish -C '${root.searchingText.replace("file://", "")}'` : root.searchingText.replace("file://", "")); + } + } + const launcherActionObjects = root.searchActions + .map(action => { + const actionString = `${ConfigOptions.search.prefix.action}${action.action}`; + if (actionString.startsWith(root.searchingText) || root.searchingText.startsWith(actionString)) { + return { + name: root.searchingText.startsWith(actionString) ? root.searchingText : actionString, + clickActionName: qsTr("Run"), + type: qsTr("Action"), + materialSymbol: 'settings_suggest', + execute: () => { + action.execute(root.searchingText.split(" ").slice(1).join(" ")) + }, + }; + } + return null; + }) + .filter(Boolean); + + let result = []; + + //////////////// Apps ////////////////// + result = result.concat( + AppSearch.fuzzyQuery(root.searchingText) + .map((entry) => { + entry.clickActionName = qsTr("Launch"); + entry.type = qsTr("App"); + return entry; + }) + ); + + ////////// Launcher actions //////////// + result = result.concat(launcherActionObjects); + + /////////// Math result & command ////////// + const startsWithNumber = /^\d/.test(root.searchingText); + if (startsWithNumber) { + result.push(mathResultObject); + result.push(commandResultObject); + } else { + result.push(commandResultObject); + result.push(mathResultObject); + } + + ///////////////// Web search //////////////// + result.push({ + name: root.searchingText, + clickActionName: qsTr("Search"), + type: qsTr("Search the web"), + materialSymbol: 'travel_explore', + execute: () => { + let url = ConfigOptions.search.engineBaseUrl + root.searchingText + for (let site of ConfigOptions.search.excludedSites) { + url += ` -site:${site}`; + } + Qt.openUrlExternally(url); + } + }); + + return result; + } + } + + delegate: SearchItem { // The selectable item for each search result + required property var modelData + anchors.left: parent?.left + anchors.right: parent?.right + entry: modelData + query: root.searchingText.startsWith(ConfigOptions.search.prefix.clipboard) ? + root.searchingText.slice(ConfigOptions.search.prefix.clipboard.length) : + root.searchingText; + } + } + + } + } +}
\ No newline at end of file diff --git a/config/quickshell/qml_color.json b/config/quickshell/qml_color.json new file mode 100644 index 00000000..888ba3e6 --- /dev/null +++ b/config/quickshell/qml_color.json @@ -0,0 +1,17 @@ +{ + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "#1F1A1F", + "layerBackground2": "#231E23", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "#cba6f7", + "shadowColor": "#000000", + "accentPrimary": "#6750A4", + "accentSecondary": "#D5C0D7", + "selectionBackground": "#534457", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#4C444D" +}
\ No newline at end of file diff --git a/config/quickshell/services/AppSearch.qml b/config/quickshell/services/AppSearch.qml new file mode 100644 index 00000000..876df183 --- /dev/null +++ b/config/quickshell/services/AppSearch.qml @@ -0,0 +1,116 @@ +pragma Singleton + +import "root:/modules/common" +import "root:/modules/common/functions/fuzzysort.js" as Fuzzy +import "root:/modules/common/functions/levendist.js" as Levendist +import Quickshell +import Quickshell.Io + +/** + * - Eases fuzzy searching for applications by name + * - Guesses icon name for window class name + */ +Singleton { + id: root + property bool sloppySearch: ConfigOptions?.search.sloppy ?? false + property real scoreThreshold: 0.2 + property var substitutions: ({ + "code-url-handler": "visual-studio-code", + "Code": "visual-studio-code", + "gnome-tweaks": "org.gnome.tweaks", + "pavucontrol-qt": "pavucontrol", + "wps": "wps-office2019-kprometheus", + "wpsoffice": "wps-office2019-kprometheus", + "footclient": "foot", + "zen": "zen-browser", + }) + property var regexSubstitutions: [ + { + "regex": /^steam_app_(\\d+)$/, + "replace": "steam_icon_$1" + }, + { + "regex": /Minecraft.*/, + "replace": "minecraft" + }, + { + "regex": /.*polkit.*/, + "replace": "system-lock-screen" + }, + { + "regex": /gcr.prompter/, + "replace": "system-lock-screen" + } + ] + + readonly property list<DesktopEntry> list: Array.from(DesktopEntries.applications.values) + .sort((a, b) => a.name.localeCompare(b.name)) + + readonly property var preppedNames: list.map(a => ({ + name: Fuzzy.prepare(`${a.name} `), + entry: a + })) + + function fuzzyQuery(search: string): var { // Idk why list<DesktopEntry> doesn't work + if (root.sloppySearch) { + const results = list.map(obj => ({ + entry: obj, + score: Levendist.computeScore(obj.name.toLowerCase(), search.toLowerCase()) + })).filter(item => item.score > root.scoreThreshold) + .sort((a, b) => b.score - a.score) + return results + .map(item => item.entry) + } + + return Fuzzy.go(search, preppedNames, { + all: true, + key: "name" + }).map(r => { + return r.obj.entry + }); + } + + function iconExists(iconName) { + return (Quickshell.iconPath(iconName, true).length > 0) + && !iconName.includes("image-missing"); + } + + function guessIcon(str) { + if (!str || str.length == 0) return "image-missing"; + + // Normal substitutions + if (substitutions[str]) + return substitutions[str]; + + // Regex substitutions + for (let i = 0; i < regexSubstitutions.length; i++) { + const substitution = regexSubstitutions[i]; + const replacedName = str.replace( + substitution.regex, + substitution.replace, + ); + if (replacedName != str) return replacedName; + } + + // If it gets detected normally, no need to guess + if (iconExists(str)) return str; + + let guessStr = str; + // Guess: Take only app name of reverse domain name notation + guessStr = str.split('.').slice(-1)[0].toLowerCase(); + if (iconExists(guessStr)) return guessStr; + // Guess: normalize to kebab case + guessStr = str.toLowerCase().replace(/\s+/g, "-"); + if (iconExists(guessStr)) return guessStr; + // Guess: First fuzze desktop entry match + const searchResults = root.fuzzyQuery(str); + if (searchResults.length > 0) { + const firstEntry = searchResults[0]; + guessStr = firstEntry.icon + if (iconExists(guessStr)) return guessStr; + } + + // Give up + return str; + } +} diff --git a/config/quickshell/services/ConfigLoader.qml b/config/quickshell/services/ConfigLoader.qml new file mode 100644 index 00000000..d3fb4e26 --- /dev/null +++ b/config/quickshell/services/ConfigLoader.qml @@ -0,0 +1,146 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import "root:/modules/common/functions/file_utils.js" as FileUtils +import "root:/modules/common/functions/string_utils.js" as StringUtils +import "root:/modules/common/functions/object_utils.js" as ObjectUtils +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Hyprland +import Qt.labs.platform + +/** + * Loads and manages the shell configuration file. + * The config file is by default at XDG_CONFIG_HOME/quickshell/config.json. + * Automatically reloaded when the file changes, but does not provide a way to save changes. + */ +Singleton { + id: root + property string filePath: Directories.shellConfigPath + property bool firstLoad: true + + function loadConfig() { + configFileView.reload() + } + + function applyConfig(fileContent) { + try { + const json = JSON.parse(fileContent); + + // Extract font configuration if it exists + let fontConfig = null; + let configForOptions = {}; + + // Copy all properties except font to configForOptions + for (let key in json) { + if (key !== "font") { + configForOptions[key] = json[key]; + } else { + fontConfig = json[key]; + } + } + + // Apply the non-font configuration to ConfigOptions + ObjectUtils.applyToQtObject(ConfigOptions, configForOptions); + + // Apply font configuration to Appearance if it exists + if (fontConfig && typeof Appearance !== 'undefined') { + if (fontConfig.family && Appearance.font && Appearance.font.family) { + ObjectUtils.applyToQtObject(Appearance.font.family, fontConfig.family); + } + if (fontConfig.pixelSize && Appearance.font && Appearance.font.pixelSize) { + ObjectUtils.applyToQtObject(Appearance.font.pixelSize, fontConfig.pixelSize); + } + } + + if (root.firstLoad) { + root.firstLoad = false; + } else { + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration reloaded")}" "${root.filePath}"`) + } + } catch (e) { + console.error("[ConfigLoader] Error reading file:", e); + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) + return; + } + } + + function setLiveConfigValue(nestedKey, value) { + let keys = nestedKey.split("."); + let targetObject = ConfigOptions; + + // Check if this is a font-related configuration + if (keys[0] === "font") { + targetObject = Appearance; + } + + let obj = targetObject; + let parents = [obj]; + + // Traverse and collect parent objects + for (let i = 0; i < keys.length - 1; ++i) { + if (!obj[keys[i]] || typeof obj[keys[i]] !== "object") { + obj[keys[i]] = {}; + } + obj = obj[keys[i]]; + parents.push(obj); + } + + // Convert value to correct type using JSON.parse when safe + let convertedValue = value; + if (typeof value === "string") { + let trimmed = value.trim(); + if (trimmed === "true" || trimmed === "false" || !isNaN(Number(trimmed))) { + try { + convertedValue = JSON.parse(trimmed); + } catch (e) { + convertedValue = value; + } + } + } + + console.log(`[ConfigLoader] Setting live config value: ${nestedKey} = ${convertedValue}`); + obj[keys[keys.length - 1]] = convertedValue; + } + + function saveConfig() { + const plainConfig = ObjectUtils.toPlainObject(ConfigOptions); + Hyprland.dispatch(`exec echo '${StringUtils.shellSingleQuoteEscape(JSON.stringify(plainConfig, null, 2))}' > '${root.filePath}'`) + } + + Timer { + id: delayedFileRead + interval: ConfigOptions.hacks.arbitraryRaceConditionDelay + repeat: false + running: false + onTriggered: { + root.applyConfig(configFileView.text()) + } + } + + FileView { + id: configFileView + path: Qt.resolvedUrl(root.filePath) + watchChanges: true + onFileChanged: { + console.log("[ConfigLoader] File changed, reloading...") + this.reload() + delayedFileRead.start() + } + onLoadedChanged: { + const fileContent = configFileView.text() + root.applyConfig(fileContent) + } + onLoadFailed: (error) => { + if(error == FileViewError.FileNotFound) { + console.log("[ConfigLoader] File not found, creating new file.") + root.saveConfig() + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration created")}" "${root.filePath}"`) + } else { + Hyprland.dispatch(`exec notify-send "${qsTr("Shell configuration failed to load")}" "${root.filePath}"`) + } + } + } +}
\ No newline at end of file diff --git a/config/quickshell/services/HyprlandData.qml b/config/quickshell/services/HyprlandData.qml new file mode 100644 index 00000000..2b88ad9c --- /dev/null +++ b/config/quickshell/services/HyprlandData.qml @@ -0,0 +1,69 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +/** + * Provides access to some Hyprland data not available in Quickshell.Hyprland. + */ +Singleton { + id: root + property var windowList: [] + property var addresses: [] + property var windowByAddress: ({}) + property var monitors: [] + + function updateWindowList() { + getClients.running = true + getMonitors.running = true + } + + Component.onCompleted: { + updateWindowList() + } + + Connections { + target: Hyprland + + function onRawEvent(event) { + // Filter out redundant old v1 events for the same thing + if(event.name in [ + "activewindow", "focusedmon", "monitoradded", + "createworkspace", "destroyworkspace", "moveworkspace", + "activespecial", "movewindow", "windowtitle" + ]) return ; + updateWindowList() + } + } + + Process { + id: getClients + command: ["bash", "-c", "hyprctl clients -j | jq -c"] + stdout: SplitParser { + onRead: (data) => { + root.windowList = JSON.parse(data) + let tempWinByAddress = {} + for (var i = 0; i < root.windowList.length; ++i) { + var win = root.windowList[i] + tempWinByAddress[win.address] = win + } + root.windowByAddress = tempWinByAddress + root.addresses = root.windowList.map((win) => win.address) + } + } + } + Process { + id: getMonitors + command: ["bash", "-c", "hyprctl monitors -j | jq -c"] + stdout: SplitParser { + onRead: (data) => { + root.monitors = JSON.parse(data) + } + } + } +} + diff --git a/config/quickshell/services/HyprlandKeybinds.qml b/config/quickshell/services/HyprlandKeybinds.qml new file mode 100644 index 00000000..189ba76d --- /dev/null +++ b/config/quickshell/services/HyprlandKeybinds.qml @@ -0,0 +1,73 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import "root:/modules/common/functions/file_utils.js" as FileUtils +import QtQuick +import Quickshell +import Quickshell.Io +import Quickshell.Wayland +import Quickshell.Hyprland + +/** + * A service that provides access to Hyprland keybinds. + * Uses the `get_keybinds.py` script to parse comments in config files in a certain format and convert to JSON. + */ +Singleton { + id: root + property string keybindParserPath: FileUtils.trimFileProtocol(`${Directories.config}/quickshell/scripts/hyprland/get_keybinds.py`) + property string defaultKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/hyprland/keybinds.conf`) + property string userKeybindConfigPath: FileUtils.trimFileProtocol(`${Directories.config}/hypr/custom/keybinds.conf`) + property var defaultKeybinds: {"children": []} + property var userKeybinds: {"children": []} + property var keybinds: ({ + children: [ + ...(defaultKeybinds.children ?? []), + ...(userKeybinds.children ?? []), + ] + }) + + Connections { + target: Hyprland + + function onRawEvent(event) { + if (event.name == "configreloaded") { + getDefaultKeybinds.running = true + getUserKeybinds.running = true + } + } + } + + Process { + id: getDefaultKeybinds + running: true + command: [root.keybindParserPath, "--path", root.defaultKeybindConfigPath,] + + stdout: SplitParser { + onRead: data => { + try { + root.defaultKeybinds = JSON.parse(data) + } catch (e) { + console.error("[CheatsheetKeybinds] Error parsing keybinds:", e) + } + } + } + } + + Process { + id: getUserKeybinds + running: true + command: [root.keybindParserPath, "--path", root.userKeybindConfigPath] + + stdout: SplitParser { + onRead: data => { + try { + root.userKeybinds = JSON.parse(data) + } catch (e) { + console.error("[CheatsheetKeybinds] Error parsing keybinds:", e) + } + } + } + } +} + diff --git a/config/quickshell/services/MaterialThemeLoader.qml b/config/quickshell/services/MaterialThemeLoader.qml new file mode 100644 index 00000000..2d67ad5b --- /dev/null +++ b/config/quickshell/services/MaterialThemeLoader.qml @@ -0,0 +1,58 @@ +pragma Singleton +pragma ComponentBehavior: Bound + +import "root:/modules/common" +import QtQuick +import Quickshell +import Quickshell.Io + +/** + * Automatically reloads generated material colors. + * It is necessary to run reapplyTheme() on startup because Singletons are lazily loaded. + */ +Singleton { + id: root + property string filePath: Directories.generatedMaterialThemePath + + function reapplyTheme() { + themeFileView.reload() + } + + function applyColors(fileContent) { + const json = JSON.parse(fileContent) + for (const key in json) { + if (json.hasOwnProperty(key)) { + // Convert snake_case to CamelCase + const camelCaseKey = key.replace(/_([a-z])/g, (g) => g[1].toUpperCase()) + const m3Key = `m3${camelCaseKey}` + Appearance.m3colors[m3Key] = json[key] + } + } + + Appearance.m3colors.darkmode = (Appearance.m3colors.m3windowBackground.hslLightness < 0.5) + } + + Timer { + id: delayedFileRead + interval: ConfigOptions?.hacks?.arbitraryRaceConditionDelay ?? 100 + repeat: false + running: false + onTriggered: { + root.applyColors(themeFileView.text()) + } + } + + FileView { + id: themeFileView + path: Qt.resolvedUrl(root.filePath) + watchChanges: true + onFileChanged: { + this.reload() + delayedFileRead.start() + } + onLoadedChanged: { + const fileContent = themeFileView.text() + root.applyColors(fileContent) + } + } +} diff --git a/config/quickshell/shell.qml b/config/quickshell/shell.qml new file mode 100644 index 00000000..b14c8773 --- /dev/null +++ b/config/quickshell/shell.qml @@ -0,0 +1,26 @@ +//@ pragma UseQApplication +//@ pragma Env QS_NO_RELOAD_POPUP=1 +//@ pragma Env QT_QUICK_CONTROLS_STYLE=Basic + +import "./modules/common/" +import "./modules/overview/" +import QtQuick +import QtQuick.Controls +import QtQuick.Layouts +import Quickshell +import "./services/" + +ShellRoot { + // Enable/disable modules here. False = not loaded at all, so rest assured + // no unnecessary stuff will take up memory if you decide to only use, say, the overview. + property bool enableOverview: true + + // Force initialization of some singletons + Component.onCompleted: { + MaterialThemeLoader.reapplyTheme() + ConfigLoader.loadConfig() + } + + Loader { active: enableOverview; sourceComponent: Overview {} } + +}
\ No newline at end of file diff --git a/config/wallust/templates/qml_color.json b/config/wallust/templates/qml_color.json new file mode 100644 index 00000000..03565181 --- /dev/null +++ b/config/wallust/templates/qml_color.json @@ -0,0 +1,17 @@ +{ + "windowBackground": "#0f0f15", + "primaryText": "#bac2de", + "layerBackground1": "{{color7}}", + "layerBackground2": "{{color6}}", + "layerBackground3": "#2D282E", + "surfaceText": "#EAE0E7", + "secondaryText": "#CFC3CD", + "borderPrimary": "{{color7}}", + "shadowColor": "#000000", + "accentPrimary": "{{color7}}", + "accentSecondary": "#{{color7}}", + "selectionBackground": "{{color7}}", + "accentPrimaryText": "#FFFFFF", + "selectionText": "#F2DCF3", + "borderSecondary": "#{{color5}}" +}
\ No newline at end of file diff --git a/config/wallust/wallust.toml b/config/wallust/wallust.toml index a7f66721..d1f40ab2 100644 --- a/config/wallust/wallust.toml +++ b/config/wallust/wallust.toml @@ -49,6 +49,9 @@ waybar.target = '~/.config/waybar/wallust/colors-waybar.css' kitty.template = 'colors-kitty.conf' kitty.target = '~/.config/kitty/kitty-themes/01-Wallust.conf' +quickshell.template = 'qml_color.json' +quickshell.target = '~/.config/quickshell/qml_color.json' + #swaync.template = 'colors-swaync.css' #swaync.target = '~/.config/swaync/wallust/colors-wallust.css' diff --git a/i18n/README.ru.markdown b/i18n/README.ru.markdown new file mode 100644 index 00000000..d80a2f53 --- /dev/null +++ b/i18n/README.ru.markdown @@ -0,0 +1,194 @@ +<h3 align="center"> +<img align="center" width="80%" src=https://github.com/user-attachments/assets/bc18bd4d-944b-4d5f-a119-7578fa38f9b4 /> +</h3> + +<p align="center"> + <img src="https://raw.githubusercontent.com/JaKooLit/Hyprland-Dots/main/assets/latte.png" width="400" /> +</p> + +<div align="center"> +<br> + <a href="#установка"><kbd> <br> Установка <br> </kbd></a> + <a href="https://www.youtube.com/playlist?list=PLDtGd5Fw5_GjXCznR0BzCJJDIQSZJRbxx"><kbd> <br> YouTube <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki"><kbd> <br> Вики <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/discussions"><kbd> <br> Обсуждения <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki/Keybinds"><kbd> <br> Горячие клавиши <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki/FAQ"><kbd> <br> Часто задаваемые вопросы <br> </kbd></a> + <a href="https://discord.gg/kool-tech-world"><kbd> <br> Discord <br> </kbd></a> +</div><br> + +<div align="center"> + +   <a href="https://discord.gg/kool-tech-world"> <img src="https://img.shields.io/discord/1151869464405606400?style=for-the-badge&logo=discord&color=cba6f7&link=https%3A%2F%2Fdiscord.gg%kool-tech-world"> </a> + +<br/> +</div> + +<h3 align="center"> + <img src="https://github.com/JaKooLit/Telegram-Animated-Emojis/blob/main/Activity/Sparkles.webp" alt="Sparkles" width="38" height="38" /> + Демонстрация Dotfiles Hyprland от KooL + <img src="https://github.com/JaKooLit/Telegram-Animated-Emojis/blob/main/Activity/Sparkles.webp" alt="Sparkles" width="38" height="38" /> +</h3> + +<div align="center"> + +https://github.com/user-attachments/assets/49bc12b2-abaf-45de-a21c-67aacd9bb872 + +</div> + +### 📹 Видеообзоры +- в конце страницы + +### 🎞️ Демо AGS Overview +- если интересно, вот короткое демо AGS overview [Ссылка на YouTube](https://youtu.be/zY5SLNPBJTs) + +</details> + +--- +[](https://git.io/typing-svg) +### 🚩 🏁 Автоматические скрипты установки Hyprland для дистрибутивов, клонирование и запуск 🇵🇭 +> [!ВНИМАНИЕ] +> Если вы используете FISH SHELL, НЕ используйте эту функцию. Вместо этого клонируйте Distro-Hyprland и запустите install.sh. + +- ПРИМЕЧАНИЕ: для работы требуется пакет `curl` + +```bash +sh <(curl -L https://raw.githubusercontent.com/JaKooLit/Hyprland-Dots/main/Distro-Hyprland.sh) +``` + +- теперь вы можете использовать приведённую выше команду для автоматического клонирования скриптов установки Distro-Hyprland, указанных ниже +- она клонирует скрипты установки и запускает `install.sh` 😎 + +### 👁️🗨️ Мои скрипты установки Hyprland 👁️🗨️ +- Автоматические скрипты Hyprland для выбранного дистрибутива, которые загрузят эти dotfiles, если вы выберете установку этих конфигураций + +- [Arch-Linux](https://github.com/JaKooLit/Arch-Hyprland) +- [OpenSUSE(Tumbleweed)](https://github.com/JaKooLit/OpenSuse-Hyprland) +- [Fedora-Linux](https://github.com/JaKooLit/Fedora-Hyprland) +- [Debian-Linux (Trixie & SID)](https://github.com/JaKooLit/Debian-Hyprland) +- [NixOS](https://github.com/JaKooLit/NixOS-Hyprland) +- [Ubuntu 24.04 LTS](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/24.04) +- [Ubuntu 24.10](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/24.10) +- [Ubuntu 25.04 - (АЛЬФА-СТАДИЯ)](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/25.04) + +--- + +### 🪧 Внимание 🪧 +- Этот репозиторий НЕ содержит и НЕ устанавливает пакеты. Это только предварительно настроенные конфигурации Hyprland или dotfiles +- обратитесь к скриптам установки, чтобы узнать, какие пакеты нужно установить... но, как минимум, пакеты Hyprland необходимы 😏😏😏 очевидно!! +- Этот репозиторий будет загружен скриптами установки Distro-Hyprland, указанными выше, если вы выберете загрузку предварительно настроенных dotfiles + +### 👀 Скриншоты 👀 +- Все скриншоты собраны здесь [Скриншоты](https://github.com/JaKooLit/screenshots/tree/main/Hyprland-ScreenShots) + +### 📦 Что нового? +- Чтобы легко отслеживать изменения, я буду обновлять [Журнал изменений](https://github.com/JaKooLit/Hyprland-Dots/wiki/Changelogs). Скриншоты будут включены, если изменения заслуживают упоминания! + +> [!ПРИМЕЧАНИЕ] +> Обратите внимание, что по умолчанию dotfiles от KooL настроены для дисплеев 2k (1440p) без масштабирования. + +### 💥 Инструкции по копированию / установке / обновлению 💥 +- [`БОЛЬШЕ ИНФОРМАЦИИ ЗДЕСЬ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Install_&_Update) +> [!Примечание] +> Автоматический скрипт копирования „copy.sh“ создаёт резервные копии директорий, которые будут скопированы. Тем не менее, рекомендуется сделать резервную копию вручную на случай, если скрипт не сможет этого сделать! + +- клонируйте этот репозиторий с помощью git. Перейдите в директорию, сделайте скрипт исполняемым и запустите его + +> для загрузки из ветки Master +```bash +git clone --depth=1 https://github.com/JaKooLit/Hyprland-Dots.git +cd Hyprland-Dots +``` + +> для загрузки из ветки Development (разработка и тестирование) +```bash +git clone --depth=1 https://github.com/JaKooLit/Hyprland-Dots.git -b development +cd Hyprland-Dots +``` + +- автоматическое копирование/установка предварительно настроенных dotfiles (рекомендуется для обновлений) +```bash +chmod +x copy.sh +./copy.sh +``` + +- для копирования/установки из релизов (стабильные) (примечание: это на одну версию старше, чем в основной ветке) +```bash +chmod +x release.sh +./release.sh +``` + +- Функция UPGRADE.sh +> [!ВАЖНО] +> Для работы требуется rsync +> у вас уже должен быть настроен и запущен Hyprland от KooL перед использованием этой функции +```bash +chmod +x upgrade.sh +./upgrade.sh +``` + +## ❗❗❗ ВНИМАНИЕ ДЛЯ ПОЛЬЗОВАТЕЛЕЙ DEBIAN И UBUNTU! +- Я получаю огромное количество сообщений об обновлении dotfiles Hyprland от KooL. Я сделал большую заметку в [`ВИКИ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Install_&_Update) + +#### ⚠️⚠️⚠️ ВНИМАНИЕ - РЕЗЕРВНЫЕ КОПИИ, СОЗДАННЫЕ СКРИПТОМ +> [!ВНИМАНИЕ] +> copy.sh, release.sh и даже upgrade.sh создают резервную копию! +> Проверьте содержимое в $HOME/.config вручную +> Удалите вручную все ненужные резервные копии + +#### 🛎️ Небольшое замечание об обоях +- по умолчанию копируется только несколько обоев (по 1 для тёмного и светлого режима, плюс ещё 3). Вам будет предложено загрузить дополнительные обои. Вы можете просмотреть/проверить дополнительные обои по [`ЭТОЙ`](https://github.com/JaKooLit/Wallpaper-Bank/tree/main/wallpapers) ссылке + +#### ⚠️⚠️⚠️ ОБЯЗАТЕЛЬНО! после копирования / установки этих dotfiles ++ Нажмите SUPER W и установите обои. Это также необходимо для инициализации wallust для тем waybar, kitty (tty) и rofi. Однако, если вы используете copy.sh или release.sh, начальные обои уже будут установлены, и этого делать не придётся + ++ Владельцы Nvidia. Обязательно отредактируйте `~/.config/hypr/UserConfigs/ENVariables.conf` (настоятельно рекомендуется). +- Пользователи / владельцы Nvidia, после установки проверьте [`ЭТО`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Notes_to_remember#--for-nvidia-gpu-users) + ++ Если вы уже настроили свои горячие клавиши, мониторы и т.д., просто скопируйте их из созданной резервной копии перед выходом из системы или перезагрузкой. (рекомендуется) + +#### 📖 Известные проблемы и возможные решения +- ознакомьтесь с этой страницей [Часто задаваемые вопросы](https://github.com/JaKooLit/Hyprland-Dots/wiki/FAQ) и [НЕРЕШЁННЫЕ ПРОБЛЕМЫ](https://github.com/JaKooLit/Hyprland-Dots/wiki/Known_Issues) + +#### 🙋 ВОПРОСЫ ?!?! ⁉️ +- Часто задаваемые вопросы! Да, вы можете использовать эти dotfiles на других дистрибутивах! Просто убедитесь, что сначала установлены соответствующие пакеты! Если вам от этого легче, я использую ту же конфигурацию на моём Gentoo :) +- БЫСТРЫЙ СОВЕТ! Нажмите на модуль HINT! в Waybar (примечание: доступно только в стандартном и Simple-L [ВЕРХНЕМ] макете Waybar). Можно запустить с помощью горячей клавиши `SUPER H` +- Ещё вопросы? щёлкните здесь, чтобы просмотреть эту [ВИКИ](https://github.com/JaKooLit/Hyprland-Dots/wiki/) +- Если вам нужны старые конфигурации, они собраны в моём репозитории „Archive“. Смотрите [ЗДЕСЬ](https://github.com/JaKooLit/Hyprland-Dots-releases-Archive) + +#### ⌨ Горячие клавиши +- Горячие клавиши [`ЩЁЛКНИТЕ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Keybinds) + +#### 🙏 Особая просьба +- Если у вас есть улучшения для dotfiles или конфигураций, не стесняйтесь отправить PR для улучшений. Я всегда приветствую улучшения, так как тоже учусь, как и вы! + +#### ✍️ Вклад +- Хотите внести вклад? Щёлкните [`ЗДЕСЬ`](https://github.com/JaKooLit/Hyprland-Dots/blob/main/CONTRIBUTING.md) для руководства по внесению вклада + +#### 🤷♂️ ЧТО ДЕЛАТЬ! +- [ ] Настройка dotfiles - 🚧 в постоянном прогрессе +- ~~[ ] Возможно, переход на starship? Хотя у starship ограниченные темы по сравнению с oh-my-zsh.~~ пока планов нет + +#### 🔮 Сервер Discord +- приглашаю присоединиться к моему [Discord](https://discord.com/invite/kool-tech-world) + +#### 💖 Поддержка +- звезда на моих репозиториях GitHub была бы замечательной 🌟 + +- Подпишитесь на мой канал YouTube [YouTube](https://www.youtube.com/@Ja.KooLit) + +- также вы можете поддержать через кофе или btc 😊 + +[](https://ko-fi.com/jakoolit) + +или + +[](https://www.buymeacoffee.com/JaKooLit) + +Или вы можете пожертвовать криптовалюту на мой btc-кошелёк :) +> 1N3MeV2dsX6gQB42HXU6MF2hAix1mqjo8i + + + +## 🫰 Спасибо за звёзды 🩷 +[](https://starchart.cc/JaKooLit/Hyprland-Dots)
\ No newline at end of file diff --git a/i18n/README.uk.markdown b/i18n/README.uk.markdown new file mode 100644 index 00000000..04d6ad27 --- /dev/null +++ b/i18n/README.uk.markdown @@ -0,0 +1,194 @@ +<h3 align="center"> +<img align="center" width="80%" src=https://github.com/user-attachments/assets/bc18bd4d-944b-4d5f-a119-7578fa38f9b4 /> +</h3> + +<p align="center"> + <img src="https://raw.githubusercontent.com/JaKooLit/Hyprland-Dots/main/assets/latte.png" width="400" /> +</p> + +<div align="center"> +<br> + <a href="#встановлення"><kbd> <br> Встановлення <br> </kbd></a> + <a href="https://www.youtube.com/playlist?list=PLDtGd5Fw5_GjXCznR0BzCJJDIQSZJRbxx"><kbd> <br> YouTube <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki"><kbd> <br> Вікі <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/discussions"><kbd> <br> Обговорення <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki/Keybinds"><kbd> <br> Гарячі клавіші <br> </kbd></a> + <a href="https://github.com/JaKooLit/Hyprland-Dots/wiki/FAQ"><kbd> <br> Поширені запитання <br> </kbd></a> + <a href="https://discord.gg/kool-tech-world"><kbd> <br> Discord <br> </kbd></a> +</div><br> + +<div align="center"> + +   <a href="https://discord.gg/kool-tech-world"> <img src="https://img.shields.io/discord/1151869464405606400?style=for-the-badge&logo=discord&color=cba6f7&link=https%3A%2F%2Fdiscord.gg%kool-tech-world"> </a> + +<br/> +</div> + +<h3 align="center"> + <img src="https://github.com/JaKooLit/Telegram-Animated-Emojis/blob/main/Activity/Sparkles.webp" alt="Sparkles" width="38" height="38" /> + Презентація Dotfiles Hyprland від KooL + <img src="https://github.com/JaKooLit/Telegram-Animated-Emojis/blob/main/Activity/Sparkles.webp" alt="Sparkles" width="38" height="38" /> +</h3> + +<div align="center"> + +https://github.com/user-attachments/assets/49bc12b2-abaf-45de-a21c-67aacd9bb872 + +</div> + +### 📹 Відеоогляди +- у кінці сторінки + +### 🎞️ Демо AGS Overview +- якщо вам цікаво, ось коротке демо AGS overview [Посилання на YouTube](https://youtu.be/zY5SLNPBJTs) + +</details> + +--- +[](https://git.io/typing-svg) +### 🚩 🏁 Автоматичні скрипти встановлення Hyprland для дистрибутивів, клонування та запуск 🇵🇭 +> [!УВАГА] +> Якщо ви використовуєте FISH SHELL, НЕ використовуйте цю функцію. Натомість клонуйте Distro-Hyprland і запустіть install.sh. + +- ПРИМІТКА: для роботи потрібен пакет `curl` + +```bash +sh <(curl -L https://raw.githubusercontent.com/JaKooLit/Hyprland-Dots/main/Distro-Hyprland.sh) +``` + +- тепер ви можете використовувати наведену вище команду для автоматичного клонування скриптів встановлення Distro-Hyprland, зазначених нижче +- вона клонує скрипти встановлення та запускає `install.sh` 😎 + +### 👁️🗨️ Мої скрипти встановлення Hyprland 👁️🗨️ +- Автоматичні скрипти Hyprland для обраного дистрибутива, які завантажать ці dotfiles, якщо ви оберете встановлення цих конфігурацій + +- [Arch-Linux](https://github.com/JaKooLit/Arch-Hyprland) +- [OpenSUSE(Tumbleweed)](https://github.com/JaKooLit/OpenSuse-Hyprland) +- [Fedora-Linux](https://github.com/JaKooLit/Fedora-Hyprland) +- [Debian-Linux (Trixie & SID)](https://github.com/JaKooLit/Debian-Hyprland) +- [NixOS](https://github.com/JaKooLit/NixOS-Hyprland) +- [Ubuntu 24.04 LTS](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/24.04) +- [Ubuntu 24.10](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/24.10) +- [Ubuntu 25.04 - (АЛЬФА-ЕТАП)](https://github.com/JaKooLit/Ubuntu-Hyprland/tree/25.04) + +--- + +### 🪧 Увага 🪧 +- Цей репозиторій НЕ містить і НЕ встановлює жодних пакетів. Це лише попередньо налаштовані конфігурації Hyprland або dotfiles +- зверніться до скриптів встановлення, щоб дізнатися, які пакети потрібно встановити... але принаймні пакети Hyprland потрібні 😏😏😏 очевидно!! +- Цей репозиторій буде завантажено скриптами встановлення Distro-Hyprland, зазначеними вище, якщо ви оберете завантаження попередньо налаштованих dotfiles + +### 👀 Скріншоти 👀 +- Усі скріншоти зібрано тут [Скріншоти](https://github.com/JaKooLit/screenshots/tree/main/Hyprland-ScreenShots) + +### 📦 Що нового? +- Щоб легко відстежувати зміни, я оновлюватиму [Журнал змін](https://github.com/JaKooLit/Hyprland-Dots/wiki/Changelogs). Скріншоти будуть додані, якщо зміни варті згадки! + +> [!ПРИМІТКА] +> Зверніть увагу, що за замовчуванням dotfiles від KooL налаштовані для дисплеїв 2k (1440p) без масштабування. + +### 💥 Інструкції з копіювання / встановлення / оновлення 💥 +- [`БІЛЬШЕ ІНФОРМАЦІЇ ТУТ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Install_&_Update) +> [!Примітака] +> Автоматичний скрипт копіювання „copy.sh“ створює резервні копії директорій, які будуть скопійовані. Проте рекомендується зробити резервну копію вручну на випадок, якщо скрипт не зможе цього зробити! + +- клонуйте цей репозиторій за допомогою git. Перейдіть до директорії, зробіть скрипт виконуваним і запустіть його + +> для завантаження з гілки Master +```bash +git clone --depth=1 https://github.com/JaKooLit/Hyprland-Dots.git +cd Hyprland-Dots +``` + +> для завантаження з гілки Development (розробка та тестування) +```bash +git clone --depth=1 https://github.com/JaKooLit/Hyprland-Dots.git -b development +cd Hyprland-Dots +``` + +- автоматичне копіювання/встановлення попередньо налаштованих dotfiles (рекомендується для оновлень) +```bash +chmod +x copy.sh +./copy.sh +``` + +- для копіювання/встановлення з релізів (стабільні) (примітка: це на одну версію старше, ніж у основній гілці) +```bash +chmod +x release.sh +./release.sh +``` + +- Функція UPGRADE.sh +> [!ВАЖЛИВО] +> Для роботи потрібен rsync +> у вас уже має бути налаштований і запущений Hyprland від KooL перед використанням цієї функції +```bash +chmod +x upgrade.sh +./upgrade.sh +``` + +## ❗❗❗ УВАГА ДЛЯ КОРИСТУВАЧІВ DEBIAN ТА UBUNTU! +- Я отримую величезну кількість повідомлень щодо оновлення dotfiles Hyprland від KooL. Я зробив велику примітку у [`ВІКІ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Install_&_Update) + +#### ⚠️⚠️⚠️ УВАГА - РЕЗЕРВНІ КОПІЇ, СТВОРЕНІ СКРИПТОМ +> [!УВАГА] +> copy.sh, release.sh і навіть upgrade.sh створюють резервну копію! +> Перевірте вміст у $HOME/.config вручну +> Видаліть вручну всі непотрібні резервні копії + +#### 🛎️ Невелика примітка про шпалери +- за замовчуванням копіюється лише кілька шпалер (по 1 для темного та світлого режиму, плюс ще 3). Вам буде запропоновано завантажити додаткові шпалери. Ви можете переглянути/перевірити додаткові шпалери за [`ЦИМ`](https://github.com/JaKooLit/Wallpaper-Bank/tree/main/wallpapers) посиланням + +#### ⚠️⚠️⚠️ ОБОВ’ЯЗКОВО! після копіювання / встановлення цих dotfiles ++ Натисніть SUPER W і встановіть шпалери. Це також необхідно для ініціалізації wallust для тем waybar, kitty (tty) і rofi. Однак, якщо ви використовуєте copy.sh або release.sh, початкові шпалери вже будуть встановлені, і цього робити не доведеться + ++ Власники Nvidia. Обов’язково відредагуйте `~/.config/hypr/UserConfigs/ENVariables.conf` (настійно рекомендується). +- Користувачі / власники Nvidia, після встановлення перевірте [`ЦЕ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Notes_to_remember#--for-nvidia-gpu-users) + ++ Якщо ви вже налаштували власні гарячі клавіші, монітори тощо, просто скопіюйте їх із створеної резервної копії перед виходом із системи або перезавантаженням. (рекомендується) + +#### 📖 Відомі проблеми та можливі рішення +- перегляньте цю сторінку [Поширені запитання](https://github.com/JaKooLit/Hyprland-Dots/wiki/FAQ) та [НЕРОЗВ’ЯЗАНІ ПРОБЛЕМИ](https://github.com/JaKooLit/Hyprland-Dots/wiki/Known_Issues) + +#### 🙋 ЗАПИТАННЯ ?!?! ⁉️ +- Поширені запитання! Так, ви можете використовувати ці dotfiles на інших дистрибутивах! Просто переконайтеся, що спочатку встановлено відповідні пакети! Якщо вам від цього легше, я використовую ту саму конфігурацію на моєму Gentoo :) +- ШВИДКА ПОРАДА! Натисніть на модуль HINT! у Waybar (примітка: доступно лише у стандартному та Simple-L [ВЕРХНЬОМУ] макеті Waybar). Можна запустити за допомогою гарячої клавіші `SUPER H` +- Ще запитання? натисніть тут, щоб переглянути цю [ВІКІ](https://github.com/JaKooLit/Hyprland-Dots/wiki/) +- Якщо вам потрібні старі конфігурації, вони зібрані в моєму репозиторії „Archive“. Дивіться [ТУТ](https://github.com/JaKooLit/Hyprland-Dots-releases-Archive) + +#### ⌨ Гарячі клавіші +- Гарячі клавіші [`КЛІКНІТЬ`](https://github.com/JaKooLit/Hyprland-Dots/wiki/Keybinds) + +#### 🙏 Особливе прохання +- Якщо у вас є покращення для dotfiles або конфігурацій, не соромтеся надіслати PR для покращень. Я завжди вітаю покращення, адже я також вчуся, як і ви! + +#### ✍️ Внесок +- Хочете зробити внесок? Клікніть [`ТУТ`](https://github.com/JaKooLit/Hyprland-Dots/blob/main/CONTRIBUTING.md) для посібника зі внесення внеску + +#### 🤷♂️ ЩО РОБИТИ! +- [ ] Налаштування dotfiles - 🚧 у постійному прогресі +- ~~[ ] Можливо, перехід на starship? Хоча у starship обмежені теми порівняно з oh-my-zsh.~~ поки що планів немає + +#### 🔮 Сервер Discord +- запрошую приєднатися до мого [Discord](https://discord.com/invite/kool-tech-world) + +#### 💖 Підтримка +- зірка на моїх репозиторіях GitHub була б чудовою 🌟 + +- Підпишіться на мій канал YouTube [YouTube](https://www.youtube.com/@Ja.KooLit) + +- також ви можете підтримати через каву або btc 😊 + +[](https://ko-fi.com/jakoolit) + +або + +[](https://www.buymeacoffee.com/JaKooLit) + +Або ви можете пожертвувати криптовалюту на мій btc-гаманець :) +> 1N3MeV2dsX6gQB42HXU6MF2hAix1mqjo8i + + + +## 🫰 Дякую за зірки 🩷 +[](https://starchart.cc/JaKooLit/Hyprland-Dots)
\ No newline at end of file |
