diff --git a/config/hypr/hypridle.conf b/config/hypr/hypridle.conf deleted file mode 100644 index 271823a9..00000000 --- a/config/hypr/hypridle.conf +++ /dev/null @@ -1,13 +0,0 @@ - -general { - # lock_cmd = echo "Locked Hyprland Session" - unlock_cmd = notify-send "Welcome back to Hyprland, $USER!" - ignore_dbus_inhibit = false - ignore_systemd_inhibit = false -} - -listener { - timeout = 1800 # 1800 -> 30m | 3600 -> 1h | 7200 -> 2h - on-timeout = hyprlock - # on-resume = notify-send "Welcome back to Hyprland, $USER!" -} diff --git a/config/hypr/hyprland.conf b/config/hypr/hyprland.conf deleted file mode 100644 index f1b14a96..00000000 --- a/config/hypr/hyprland.conf +++ /dev/null @@ -1,15 +0,0 @@ - -########################################### -## Hyprland Core Settings for Colorshell ## -########################################## - -# From https://github.com/retrozinndev/Hyprland-Dots -# Made with lots of love 󰋑 , by retrozinndev -# Licensed under the BSD 3-Clause License - - -# Shell configurations (it's not recommended to modify) -source = ./shell/hyprland.conf - -# User configurations (please use the `user/` config directory -source = ./user/hyprland.conf diff --git a/config/hypr/hyprpaper.conf b/config/hypr/hyprpaper.conf deleted file mode 100644 index 7b89e41b..00000000 --- a/config/hypr/hyprpaper.conf +++ /dev/null @@ -1,8 +0,0 @@ -# default hyprpaper config from colorshell -# hypr-chan is a mascot of Hyprland, it's not made by retrozinndev - -$wallpaper = ~/wallpapers/Default Hypr-chan.jpg - -preload = $wallpaper -wallpaper = , $wallpaper -splash = true diff --git a/config/hypr/scripts/change-wallpaper.sh b/config/hypr/scripts/change-wallpaper.sh deleted file mode 100644 index 23a4c96d..00000000 --- a/config/hypr/scripts/change-wallpaper.sh +++ /dev/null @@ -1,68 +0,0 @@ -#!usr/bin/env bash - -# Prompts the user with dmenu(or dmenu-like app, see hypr/scripts/get-dmenu.sh) -# to choose an image file inside defined $WALLPAPERS. If the user selects -# an entry, it automatically writes changes to the hyprpaper.conf file and -# hot-reloads if hyprpaper is running. -# -------------- -# Licensed under the BSD 3-Clause License -# Made by retrozinndev (João Dias) -# From https://github.com/retrozinndev/colorshell - -style="lighten" # lighten / darken -WALLPAPERS=`[[ -z "$WALLPAPERS" ]] && echo -n "$HOME/wallpapers" || echo -n "$WALLPAPERS"` - -function Write_changes() { - echo "[LOG] Writing to hyprpaper config file" - - echo \ -'$wallpaper'" = $wall - -splash = true -preload = "'$wallpaper'" -wallpaper = , "'$wallpaper'"" | sed -e "s/^(\\[n])//g" > $XDG_CONFIG_HOME/hypr/hyprpaper.conf -} - -function Reload_wallpaper() { - echo "[LOG] Hot-reloading wallpaper" - hyprctl hyprpaper unload all - hyprctl hyprpaper preload $wall - hyprctl hyprpaper wallpaper ", $wall" -} - -function Reload_pywal() { - echo "[LOG] Reloading pywal colorscheme" - wal -t --cols16 $style -i "$wall" -} - -if [[ -z $(ls -A -w1 $WALLPAPERS) ]]; then - notify-send -u normal -a "Wallpaper" "Wallpapers not found" "Couldn't find any wallpaper inside \`~/wallpapers\`, try putting an image you like in there to choose it!" - exit 1 -fi - -if [[ -z $@ ]]; then - # Prompt wallpaper list - selection=`ls -w1 "$WALLPAPERS" | wofi --show drun` - - # Check if input wallpaper is empty - if [[ -z $selection ]]; then - echo "No wallpaper has been selected by user!" - if [[ $RANDOM_WALLPAPER_WHEN_EMPTY == true ]]; then - wall="$WALLPAPERS/$(ls $WALLPAPERS | shuf -n 1)" - echo "Selected random from $WALLPAPERS: $wall" - else - echo "Skipping hyprpaper changes and exiting." - exit 0 - fi - else - wall="$WALLPAPERS/$selection" # wofi if no wallpaper specified - fi -else - wall=$@ -fi - -Reload_pywal -Reload_wallpaper -Write_changes - -exit 0 diff --git a/config/hypr/scripts/color-picker.sh b/config/hypr/scripts/color-picker.sh deleted file mode 100644 index 3af2ce88..00000000 --- a/config/hypr/scripts/color-picker.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -function send_notification() { - (notify-send -u normal -a "color-picker" "$1" "$2" > /dev/null 2>&1) || \ - (echo "$1: $2") -} - -# Check if hyprpicker is installed -if ! command -v hyprpicker > /dev/null; then - send_notification "An error occurred" "Looks like you don't have hyprpicker installed! Try installing it before using the Color Picker tool." - exit 1 -fi - -raw_output=`hyprpicker -al 2> /dev/null` -selected_color=`echo $raw_output | xargs | sed -e 's/ //g'` - -if ! [[ -z $selected_color ]]; then - send_notification "Selected Color" "The selected color is $selected_color, it was also copied to your clipboard!" -fi diff --git a/config/hypr/scripts/exec.sh b/config/hypr/scripts/exec.sh deleted file mode 100644 index af2a3f45..00000000 --- a/config/hypr/scripts/exec.sh +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env bash - -# This script executes the provided program with UWSM -# if active, or else normally. -# --------------- -# Licensed under the BSD 3-Clause License -# Made by retrozinndev (João Dias) -# From: https://github.com/retrozinndev/colorshell - - -if uwsm check is-active; then - exec uwsm-app -- "$@" - exit 0 -fi - -if [[ $1 =~ [.]desktop$ ]]; then - gio launch $@ - exit 0 -fi - -exec "$@" diff --git a/config/hypr/scripts/gen-pywal.sh b/config/hypr/scripts/gen-pywal.sh deleted file mode 100644 index b7d9c417..00000000 --- a/config/hypr/scripts/gen-pywal.sh +++ /dev/null @@ -1,19 +0,0 @@ -#!/usr/bin/env bash - -# This script loads/generate color schemes from current -# wallpaper using pywal16. -# ---------- -# Licensed under the BSD 3-Clause License -# Made by retrozinndev (João Dias) -# From https://github.com/retrozinndev/colorshell - -if ! [[ -f "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" ]]; then - echo "[error] wallpaper file not found!" - exit 1 -fi - -raw=`cat "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" | grep '$wallpaper =' | sed -e 's/^$wallpaper = //'` -wallpaper=${raw/\~/"$HOME"} -[[ -d "$XDG_CACHE_HOME/wal" ]] && wal -R || sh $XDG_CONFIG_HOME/hypr/scripts/change-wallpaper.sh "$wallpaper" - -sleep .5 && hyprctl reload diff --git a/config/hypr/scripts/screenshot.sh b/config/hypr/scripts/screenshot.sh deleted file mode 100644 index 56dfd7ca..00000000 --- a/config/hypr/scripts/screenshot.sh +++ /dev/null @@ -1,24 +0,0 @@ -#!/usr/bin/env bash - -# This script handles taking a screenshot using the -# hyprshot tool. -# -------------- -# Licensed under the BSD 3-Clause License -# Made by retrozinndev (João Dias) -# From https://github.com/retrozinndev/colorshell - - -# exit slurp and quit if slurp(region selection) is running -killall slurp && exit 0 - -if [[ -z $(command -v hyprshot) ]]; then - echo "[err] you don't have hyprshot installed, please install it first" - exit 1 -fi - -if [[ "$1" == "full" ]]; then - hyprshot -m active -m output -o "$(xdg-user-dir PICTURES)/Screenshots" - exit 0 -fi - -hyprshot -m region -o "$(xdg-user-dir PICTURES)/Screenshots" diff --git a/config/hypr/shell/autostart.conf b/config/hypr/shell/autostart.conf deleted file mode 100644 index 41ecd843..00000000 --- a/config/hypr/shell/autostart.conf +++ /dev/null @@ -1,21 +0,0 @@ - -# colorshell configuration, please don't modify unless you know what you're doing! - -# Daemons -exec-once = $exec ibus start # use ibus as input method (fixes character compose not working in gtk apps) -exec-once = systemctl enable --user --now hyprpolkitagent # Hyprland's PolKit -exec-once = $exec /usr/lib/polkit-gnome/polkit-gnome-authentication-agent-1 # GNOME PolKit (activates only if hyprpolkitagent doesn't work/is not installed) -exec-once = systemctl enable --user --now hypridle -exec-once = systemctl enable --user --now gnome-keyring-daemon -exec-once = $exec wl-paste --type text --watch cliphist store -exec-once = $exec wl-paste --type image --watch cliphist store - -# Tools -exec-once = systemctl enable --user --now hyprsunset -exec-once = systemctl enable --user --now hyprpaper - -# Scripts -exec-once = sh $XDG_CONFIG_HOME/hypr/scripts/gen-pywal.sh - -# Shell -exec-once = $exec colorshell.desktop diff --git a/config/hypr/shell/bindings.conf b/config/hypr/shell/bindings.conf deleted file mode 100644 index 8acf3bbf..00000000 --- a/config/hypr/shell/bindings.conf +++ /dev/null @@ -1,97 +0,0 @@ -# colorshell-specific configuration, please don't modify unless you know what you're doing! -# some commands don't need $exec (uwsm) if they're just process communication tools - -bind = $mainMod, SPACE, exec, colorshell runner -bind = $mainMod, F11, fullscreen - -bind = , Print, exec, $exec $scripts/screenshot.sh -bind = $mainMod, Print, exec, $exec $scripts/screenshot.sh full - -# restarts colorshell -bind = $mainMod, F7, exec, timeout 0.6s colorshell quit && $exec colorshell.desktop || kill $(cat $XDG_RUNTIME_DIR/colorshell/.pid) ; $exec colorshell - -bind = $mainMod, K, exec, colorshell run %terminal -bind = $mainMod, Q, killactive -bind = $mainMod, E, exec, colorshell run %file_manager -bind = $mainMod, F, togglefloating -bind = $mainMod, P, pseudo, -bind = $mainMod, J, togglesplit -bind = $mainMod, N, exec, colorshell toggle control-center -bind = $mainMod, M, exec, colorshell toggle center-window -bind = $mainMod, L, exec, $exec hyprlock -bind = $mainMod, V, exec, colorshell runner '>' -bind = $mainMod, W, exec, colorshell runner '\##' - -# bind = $mainMod, $mainMod_L, exec, colorshell toggle apps-window -bind = $mainMod, $mainMod_l, exec, colorshell peek-workspace-num - -binde = , XF86AudioLowerVolume, exec, colorshell volume sink-decrease 5 # Decrease volume -binde = , XF86AudioRaiseVolume, exec, colorshell volume sink-increase 5 # Increase volume -bind = , XF86AudioMute, exec, colorshell volume sink-mute # Mute -bind = , XF86AudioPrev, exec, colorshell media previous # Previous media -bind = , XF86AudioNext, exec, colorshell media next # Next media -bind = , XF86AudioPlay, exec, colorshell media play-pause # Toggle Play/Pause media - -bind = , XF86MonBrightnessDown, exec, brightnessctl -c backlight s 5%- # Lower monitor brightness -bind = , XF86MonBrightnessUp, exec, brightnessctl -c backlight s +5% # Increase monitor brightness - -# Move focus with mainMod + arrow keys -bind = $mainMod, left, movefocus, l -bind = $mainMod, right, movefocus, r -bind = $mainMod, up, movefocus, u -bind = $mainMod, down, movefocus, d - - -# Move windows with keyboard keys -bind = $mainMod SHIFT, left, movewindow, l -bind = $mainMod SHIFT, right, movewindow, r -bind = $mainMod SHIFT, up, movewindow, u -bind = $mainMod SHIFT, down, movewindow, d -bind = $mainMod SHIFT, C, centerwindow - - -# Resize windows with arrow keys / hjkl -binde = $mainMod ALT, left, resizeactive, -60 0 -binde = $mainMod ALT, down, resizeactive, 0 60 -binde = $mainMod ALT, up, resizeactive, 0 -60 -binde = $mainMod ALT, right, resizeactive, 60 0 - -binde = $mainMod ALT, H, resizeactive, -60 0 -binde = $mainMod ALT, J, resizeactive, 0 60 -binde = $mainMod ALT, K, resizeactive, 0 -60 -binde = $mainMod ALT, L, resizeactive, 60 0 - - -# Switch workspaces with mainMod + [0-9] -bind = $mainMod, 1, workspace, 1 -bind = $mainMod, 2, workspace, 2 -bind = $mainMod, 3, workspace, 3 -bind = $mainMod, 4, workspace, 4 -bind = $mainMod, 5, workspace, 5 -bind = $mainMod, 6, workspace, 6 -bind = $mainMod, 7, workspace, 7 -bind = $mainMod, 8, workspace, 8 -bind = $mainMod, 9, workspace, 9 -bind = $mainMod, 0, workspace, 10 - -# Move active window to a workspace with mainMod + SHIFT + [0-9] -bind = $mainMod SHIFT, 1, movetoworkspace, 1 -bind = $mainMod SHIFT, 2, movetoworkspace, 2 -bind = $mainMod SHIFT, 3, movetoworkspace, 3 -bind = $mainMod SHIFT, 4, movetoworkspace, 4 -bind = $mainMod SHIFT, 5, movetoworkspace, 5 -bind = $mainMod SHIFT, 6, movetoworkspace, 6 -bind = $mainMod SHIFT, 7, movetoworkspace, 7 -bind = $mainMod SHIFT, 8, movetoworkspace, 8 -bind = $mainMod SHIFT, 9, movetoworkspace, 9 -bind = $mainMod SHIFT, 0, movetoworkspace, 10 - -bind = CTRL $mainMod, right, workspace, e+1 -bind = CTRL $mainMod, left, workspace, e-1 - -bind = $mainMod, S, togglespecialworkspace, special -bind = $mainMod SHIFT, S, movetoworkspace, special:special - -# Move/resize windows with mainMod + LMB/RMB and dragging -bindm = $mainMod, mouse:272, movewindow -bindm = $mainMod, mouse:273, resizewindow diff --git a/config/hypr/shell/environment.conf b/config/hypr/shell/environment.conf deleted file mode 100644 index 47aa23af..00000000 --- a/config/hypr/shell/environment.conf +++ /dev/null @@ -1,32 +0,0 @@ - -# color-shell configuration, please don't modify unless you know what you're doing! - -# add colorshell to PATH by default -env = PATH, $PATH:$HOME/.local/bin - -# XDG Vars -env = XDG_CONFIG_HOME, $HOME/.config -env = XDG_CACHE_HOME, $HOME/.cache -env = XDG_DATA_HOME, $HOME/.local/share -env = XDG_STATE_HOME, $HOME/.local/state -env = XDG_CURRENT_DESKTOP, Hyprland -env = XDG_SESSION_TYPE, wayland - -# Cursor -env = XCURSOR_THEME, Adwaita -env = XCURSOR_SIZE, 24 -env = HYPRCURSOR_THEME, Adwaita -env = HYPRCURSOR_SIZE, 24 - -# Wayland stuff -env = MOZ_ENABLE_WAYLAND, 1 -env = ELECTRON_OZONE_PLATFORM_HINT, auto - -# QT -env = QT_QPA_PLATFORM, wayland -env = QT_QPA_PLATFORMTHEME, qt5ct -env = QT_AUTO_SCREEN_SCALE_FACTOR, 1 - -# Others -env = ADW_DISABLE_PORTAL, 1 # Fixes prefer-dark setting in some gtk flatpak apps -env = WALLPAPERS, $HOME/wallpapers diff --git a/config/hypr/shell/hyprland.conf b/config/hypr/shell/hyprland.conf deleted file mode 100644 index fafafa98..00000000 --- a/config/hypr/shell/hyprland.conf +++ /dev/null @@ -1,10 +0,0 @@ - -# color-shell configuration, please don't modify unless you know what you're doing! - -source = ./variables.conf -source = ./environment.conf -source = ./bindings.conf -source = ./decorations.conf -source = ./autostart.conf -source = ./rules.conf -source = ./layout.conf diff --git a/config/hypr/shell/layout.conf b/config/hypr/shell/layout.conf deleted file mode 100644 index f8614df6..00000000 --- a/config/hypr/shell/layout.conf +++ /dev/null @@ -1,8 +0,0 @@ - -# color-shell configuration, please don't modify unless you know what you're doing! - -dwindle { - pseudotile = false - preserve_split = true - smart_resizing = true -} diff --git a/config/hypr/shell/variables.conf b/config/hypr/shell/variables.conf deleted file mode 100644 index a1dc98d5..00000000 --- a/config/hypr/shell/variables.conf +++ /dev/null @@ -1,13 +0,0 @@ - -# colorshell configuration, please don't modify unless you know what you're doing! - -############### -## VARIABLES ## -############### -# Wiki: https://wiki.hyprland.org/Hypr-Ecosystem/hyprlang#defining-variables - -$mainMod = SUPER -$scripts = sh $XDG_CONFIG_HOME/hypr/scripts - -# Use this variable to execute apps dinamically (runs with uwsm if being used by compositor) -$exec = $scripts/exec.sh diff --git a/config/hypr/user/autostart.conf b/config/hypr/user/autostart.conf deleted file mode 100644 index c50f3c76..00000000 --- a/config/hypr/user/autostart.conf +++ /dev/null @@ -1,6 +0,0 @@ - -############### -## AUTOSTART ## -############### -# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#executing - diff --git a/config/hypr/user/bindings.conf b/config/hypr/user/bindings.conf deleted file mode 100644 index 61daad40..00000000 --- a/config/hypr/user/bindings.conf +++ /dev/null @@ -1,8 +0,0 @@ -############## -## BINDINGS ## -############## -# Wiki: https://wiki.hyprland.org/Configuring/Binds - - -# Uncomment if you want to press SUPER to launch application search -# bind = $mainMod, $mainMod_L, exec, astal toggle apps-window diff --git a/config/hypr/user/decorations.conf b/config/hypr/user/decorations.conf deleted file mode 100644 index 5600fee1..00000000 --- a/config/hypr/user/decorations.conf +++ /dev/null @@ -1,6 +0,0 @@ -################ -## DECORATION ## -################ -# Wiki: https://wiki.hyprland.org/Configuring/Variables - - diff --git a/config/hypr/user/environment.conf b/config/hypr/user/environment.conf deleted file mode 100644 index e7e2aa07..00000000 --- a/config/hypr/user/environment.conf +++ /dev/null @@ -1,6 +0,0 @@ -################# -## ENVIRONMENT ## -################# -# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#setting-the-environment - - diff --git a/config/hypr/user/hyprland.conf b/config/hypr/user/hyprland.conf deleted file mode 100644 index 13dee9d8..00000000 --- a/config/hypr/user/hyprland.conf +++ /dev/null @@ -1,15 +0,0 @@ - -######################## -## USER CONFIGURATION ## -######################## - -# This sources all user configuration files - -source = ./monitors.conf -source = ./environment.conf -source = ./input.conf -source = ./bindings.conf -source = ./layout.conf -source = ./decorations.conf -source = ./autostart.conf -source = ./rules.conf diff --git a/config/hypr/user/input.conf b/config/hypr/user/input.conf deleted file mode 100644 index 9f43fa2d..00000000 --- a/config/hypr/user/input.conf +++ /dev/null @@ -1,13 +0,0 @@ -########### -## INPUT ## -########### - -# Wiki: https://wiki.hyprland.org/Configuring/Keywords/#per-device-input-configs - - -############## -## GESTURES ## -############## - -# Wiki: https://wiki.hyprland.org/Configuring/Variables/#gestures - diff --git a/config/hypr/user/layout.conf b/config/hypr/user/layout.conf deleted file mode 100644 index 8d1b8810..00000000 --- a/config/hypr/user/layout.conf +++ /dev/null @@ -1,4 +0,0 @@ -############ -## LAYOUT ## -############ -# Wiki: https://wiki.hyprland.org/Configuring/Dwindle-Layout/#config diff --git a/config/hypr/user/monitors.conf b/config/hypr/user/monitors.conf deleted file mode 100644 index d3553224..00000000 --- a/config/hypr/user/monitors.conf +++ /dev/null @@ -1,17 +0,0 @@ -############## -## MONITORS ## -############## - -# Wiki: https://wiki.hyprland.org/Configuring/Monitors - - -# Monitor -# arg0 -> monitor name(you can get monitor names with `hyprctl monitors`); -# arg1 -> resolution@hertz; -# arg2 -> positioning from the top-left corner; -# arg3 -> scaling; -# arg4 -> variable refresh rate(optional); -# - arg40 -> 1: vrr, 0: no vrr. - -# Example configuration: -# monitor = HDMI-A-1, 1920x1080@60, 0x0, 1, vrr, 0 diff --git a/config/hypr/user/rules.conf b/config/hypr/user/rules.conf deleted file mode 100644 index 2dd99a6f..00000000 --- a/config/hypr/user/rules.conf +++ /dev/null @@ -1,8 +0,0 @@ - -############################ -## WINDOW & LAYER RULES ## -############################ - -# See https://wiki.hyprland.org/Configuring/Window-Rules for -# more information on how to do this - diff --git a/config/kitty/kitty.conf b/config/kitty/kitty.conf deleted file mode 100644 index d96eb711..00000000 --- a/config/kitty/kitty.conf +++ /dev/null @@ -1,10 +0,0 @@ -# Default colorshell's kitty configuration, -# please use the user.conf available in this -# same directory. - -include $XDG_CACHE_HOME/wal/colors-kitty.conf -include ./user.conf - -# Dinamically update colorscheme with pywal16 -allow_remote_control yes -listen_on unix:@kitty diff --git a/config/kitty/user.conf b/config/kitty/user.conf deleted file mode 100644 index 060ca578..00000000 --- a/config/kitty/user.conf +++ /dev/null @@ -1,2 +0,0 @@ -# User configuration -# Add your settings for kitty here diff --git a/install.sh b/install.sh index ec2abf2d..3074ef19 100755 --- a/install.sh +++ b/install.sh @@ -3,11 +3,11 @@ trap "printf \"\nOk, quitting beacuse you entered an exit signal. (SIGINT).\n\"; exit 1" SIGINT trap "printf \"\nOh noo!! Some application just killed the script! (SIGTERM)\"; exit 2" SIGTERM -XDG_DATA_HOME=`[[ -z "$XDG_DATA_HOME" ]] && echo -n "$HOME/.local/share" || echo -n "$XDG_DATA_HOME"` -XDG_CACHE_HOME=`[[ -z "$XDG_CACHE_HOME" ]] && echo -n $HOME/.cache || echo -n $XDG_CACHE_HOME` -XDG_CONFIG_HOME=`[[ -z "$XDG_CONFIG_HOME" ]] && echo -n "$HOME/.config" || echo -n "$XDG_CONFIG_HOME"` -BIN_HOME=`[[ -z "$BIN_HOME" ]] && echo -n "$HOME/.local/bin" || echo -n "$BIN_HOME"` -APPS_HOME=`[[ -z "$APPS_HOME" ]] && echo -n "$XDG_DATA_HOME/applications" || echo -n "$APPS_HOME"` +XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"} +XDG_CACHE_HOME=${XDG_CACHE_HOME:-"$HOME/.cache"} +XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-"$HOME/.config"} +BIN_HOME=${BIN_HOME:-"$HOME/.local/bin"} +APPS_HOME=${APPS_HOME:-"$XDG_DATA_HOME/applications"} skip_prompts=`[[ "$1" == -y ]] && echo -n true` is_standalone=`(git remote -v > /dev/null 2>&1) || echo -n true` @@ -45,16 +45,7 @@ sleep .5 echo "Welcome to the colorshell installation script!" # Warn user of possible issues -Send_log warn "!! By running this script, you assume total responsability for any issues that may occur with your filesystem" - -[[ -z $skip_prompts ]] && \ - Send_log warn "Your current Hyprland and kitty configuration will be overwritten, accept the backup prompt if you still want them" - -[[ -z $skip_prompts ]] && \ - Ask "Do you want to backup what is going to be modified/overwritten?" - -[[ $answer == y ]] || [[ $skip_prompts ]] && \ - Backup_config $repo_directory +Send_log warn "!! By running this, you're assuming total responsability for any issues that may occur with your filesystem" [[ -z "$skip_prompts" ]] && \ Ask "Do you want to start the shell installation?" @@ -65,10 +56,10 @@ if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then rm -rf $repo_directory 2> /dev/null Send_log "Cloning repository in \`$repo_directory\`..." if [[ -d $repo_directory/.git ]]; then - Send_log "repo is already cloned! let's just fetch the latest changes..." + Send_log "Repo is already cloned! let's just fetch the latest changes..." git -C "$repo_directory" stash # if there are changes, let's just stash them git -C "$repo_directory" checkout ryo - git -C "$repo_directory" fetch && git -C "$repo_directory" pull --rebase + git -C "$repo_directory" fetch && git -C "$repo_directory" pull --rebase # rebase just in case else git clone https://github.com/retrozinndev/colorshell.git "$repo_directory" fi @@ -77,7 +68,7 @@ if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then Ask "Nice! Do you want to use the stable version instead of the unstable(latest commit)?" if [[ -z "$skip_prompts" ]] && [[ "$answer" == y ]]; then - Send_log "fetching latest release from colorshell repository" + Send_log "Fetching latest release from colorshell repository" latest_tag=`curl -s "$repo_api_url/releases" | jq -r '. | select(.[].prerelease == false) | .[0].tag_name'` Send_log "Done fetching" @@ -85,23 +76,9 @@ if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then git -C "$repo_directory" checkout $latest_tag > /dev/null 2>&1 fi - Send_log "Starting installation..." - - Send_log "Installing default configurations" - for dir in $(ls -A -w1 "$repo_directory/config"); do - dest=$XDG_CONFIG_HOME/$dir - - Send_log "Installing $dir in $dest" - mkdir -p `dirname "$dest"` # create parents - - [[ -d "$dest" ]] || [[ -f "$dest" ]] && \ - rm -rf $dest - - cp -rf $repo_directory/config/$dir "$dest" # copy - done - + Send_log "Starting build process..." Send_log "Updating dependencies" - pnpm -C "$repo_directory" i && pnpm -C "$repo_directory" update + pnpm -C "$repo_directory" i && pnpm -C "$repo_directory" update > /dev/null Send_log "Building colorshell" pnpm -C "$repo_directory" build:release @@ -119,10 +96,28 @@ if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then mkdir -p $APPS_HOME cp -f $repo_directory/build/release/colorshell.desktop $APPS_HOME + # Check if user has a Hyprland config file + if [[ ! -f "$XDG_CONFIG_HOME/hypr/hyprland.conf" ]] && [[ -f "/usr/share/hypr/hyprland.conf" ]]; then + Send_log "Looks like Hyprland wasn't launched yet! Copying default config file..." + mkdir -p "$XDG_CONFIG_HOME/hypr" + cp -f "/usr/share/hypr/hyprland.conf" "$XDG_CONFIG_HOME/hypr/hyprland.conf" + Send_log "Adding exec for colorshell in Hyprland config..." + echo -ne "\nexec-once = ~/.local/bin/colorshell" >> "$XDG_CONFIG_HOME/hypr/hyprland.conf" + else + Ask "Do you want to autostart colorshell with Hyprland?" + if [ "$answer" == "y" ]; then + Send_log "Adding exec-once for colorshell to Hyprland..." + echo -ne "\nexec-once = ~/.local/bin/colorshell" >> "$XDG_CONFIG_HOME/hypr/hyprland.conf" + fi + fi - Send_log "Adding default wallpaper in ~/wallpapers" - mkdir -p $HOME/wallpapers - cp -f $repo_directory/resources/wallpaper_default.jpg "$HOME/wallpapers/Default Hypr-chan.jpg" + # Check for wallpaper configuration + if [[ ! -f "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" ]]; then + Send_log "No hyprpaper config found, using colorshell's default..." + mkdir -p $HOME/wallpapers + cp -f $repo_directory/resources/wallpaper_default.jpg "$HOME/wallpapers/Default Hypr-chan.jpg" + cp -f $repo_directory/resources/config/hyprpaper.conf "$XDG_CONFIG_HOME/hypr/hyprpaper.conf" + fi if [[ -z "$skip_prompts" ]]; then echo "Colorshell is installed! :D" diff --git a/resources.gresource.xml b/resources.gresource.xml index dec2c2bc..34d6657a 100644 --- a/resources.gresource.xml +++ b/resources.gresource.xml @@ -27,4 +27,22 @@ icons/shield-safe-symbolic.svg icons/user-trash-symbolic.svg + + + + config/hyprlock.conf + + + + + config/hyprland/.last-updated + config/hyprland/environment.conf + config/hyprland/decorations.conf + config/hyprland/bindings.conf + config/hyprland/rules.conf + + diff --git a/resources/config/hyprland/.last-updated b/resources/config/hyprland/.last-updated new file mode 100644 index 00000000..26182fe5 --- /dev/null +++ b/resources/config/hyprland/.last-updated @@ -0,0 +1 @@ +february 09, 21:57 diff --git a/resources/config/hyprland/bindings.conf b/resources/config/hyprland/bindings.conf new file mode 100644 index 00000000..716f1171 --- /dev/null +++ b/resources/config/hyprland/bindings.conf @@ -0,0 +1,33 @@ +# colorshell-specific configuration, please don't modify unless you know what you're doing! + +bind = SUPER, SPACE, exec, colorshell runner + +bind = , Print, exec, colorshell screenshot +bind = ALT, Print, exec, colorshell screenshot active +bind = SUPER, Print, exec, colorshell screenshot full + +# Restart colorshell +bind = SUPER, F7, exec, colorshell reload + +unbind = SUPER, N +bind = SUPER, N, exec, colorshell toggle control-center +unbind = SUPER, M +bind = SUPER, M, exec, colorshell toggle center-window +unbind = SUPER, L +bind = SUPER, L, exec, colorshell lock +unbind = SUPER, V +bind = SUPER, V, exec, colorshell runner '>' +unbind = SUPER, W +bind = SUPER, W, exec, colorshell runner '\##' + +bind = SUPER, SUPER_l, exec, colorshell peek-workspace-num + +binde = , XF86AudioLowerVolume, exec, colorshell volume sink-decrease 5 # Decrease volume +binde = , XF86AudioRaiseVolume, exec, colorshell volume sink-increase 5 # Increase volume +bind = , XF86AudioMute, exec, colorshell volume sink-mute # Mute +bind = , XF86AudioPrev, exec, colorshell media previous # Previous media +bind = , XF86AudioNext, exec, colorshell media next # Next media +bind = , XF86AudioPlay, exec, colorshell media play-pause # Toggle Play/Pause media + +bind = , XF86MonBrightnessDown, exec, brightnessctl -c backlight s 5%- # Lower monitor brightness +bind = , XF86MonBrightnessUp, exec, brightnessctl -c backlight s +5% # Increase monitor brightness diff --git a/config/hypr/shell/decorations.conf b/resources/config/hyprland/decorations.conf similarity index 91% rename from config/hypr/shell/decorations.conf rename to resources/config/hyprland/decorations.conf index 6164bd6a..3552edaa 100644 --- a/config/hypr/shell/decorations.conf +++ b/resources/config/hyprland/decorations.conf @@ -1,7 +1,9 @@ -# color-shell configuration, please don't modify unless you know what you're doing! +# colorshell configuration, please don't modify unless you know what you're doing! -source = ~/.cache/wal/colors-hyprland.conf +# hyprlang noerror true + source = ~/.cache/wal/colors-hyprland.conf +# hyprlang noerror false general { gaps_in = 6 diff --git a/resources/config/hyprland/environment.conf b/resources/config/hyprland/environment.conf new file mode 100644 index 00000000..d04716fb --- /dev/null +++ b/resources/config/hyprland/environment.conf @@ -0,0 +1,13 @@ +# colorshell configuration, please don't modify unless you know what you're doing! + +# XDG Vars +env = XDG_CONFIG_HOME, $HOME/.config +env = XDG_CACHE_HOME, $HOME/.cache +env = XDG_DATA_HOME, $HOME/.local/share +env = XDG_STATE_HOME, $HOME/.local/state + +# Input methods +env = QT_IM_MODULE, fcitx +env = QT_IM_MODULES, wayland;fcitx +env = SDL_IM_MODULE, fcitx +env = XMODIFIERS, @im=fcitx diff --git a/config/hypr/shell/rules.conf b/resources/config/hyprland/rules.conf similarity index 66% rename from config/hypr/shell/rules.conf rename to resources/config/hyprland/rules.conf index 97ea94c7..283b0ae4 100644 --- a/config/hypr/shell/rules.conf +++ b/resources/config/hyprland/rules.conf @@ -1,35 +1,9 @@ - # colorshell configuration, please don't modify unless you know what you're doing! -# Float -windowrule = match:class nm-connection-editor, float on -windowrule = match:class org.pulseaudio.pavucontrol, float on -windowrule = match:class xdg-desktop-portal.*, float on -windowrule = match:class io.github.kaii_lb.Overskride, float on - - -# Resize -windowrule = match:class org.pulseaudio.pavucontrol, size 50% 50% -windowrule = match:class io.github.kaii_lb.Overskride, size 50% 50% -windowrule = match:class xdg-desktop-portal.*, size 68% 65% - - -# Position -windowrule = match:class org.pulseaudio.pavucontrol, move 49.27% 7.28% -windowrule = match:class io.github.kaii_lb.Overskride, move 49.27% 7.28% - - -# Workspace -windowrule = match:class org.pulseaudio.pavucontrol, workspace e - # Animations windowrule = match:class hyprpolkitagent, animation gnomed -windowrule = match:class org.pulseaudio.pavucontrol, animation slide right -windowrule = match:class io.github.kaii_lb.Overskride, animation slide right windowrule = match:class xdg-desktop-portal.*, animation gnomed -windowrule = match:class hyprsysteminfo, animation gnomed - layerrule = match:namespace selection, animation fade layerrule = match:namespace hyprpicker, animation fade layerrule = match:namespace hyprpaper, animation fade @@ -43,7 +17,6 @@ layerrule = match:namespace background-window-blur, animation fade layerrule = match:namespace .*-popup, animation fade layerrule = match:namespace floating-notifications, no_anim on - # Blur windowrule { name = fix-chromium-xwayland-popup-blur diff --git a/config/hypr/hyprlock.conf b/resources/config/hyprlock.conf similarity index 98% rename from config/hypr/hyprlock.conf rename to resources/config/hyprlock.conf index 1dec9919..ed95a563 100644 --- a/config/hypr/hyprlock.conf +++ b/resources/config/hyprlock.conf @@ -9,8 +9,8 @@ source = ~/.cache/wal/colors-hyprland.conf # Variables -$font = Cantarell Regular -$clockFont = Cantarell Black +$font = Adwaita Sans Regular +$clockFont = Adwaita Sans Black $minimalFont = Noto Sans Mono $getActivePlayer = colorshell media bus-name | sed -e 's/^org.mpris.MediaPlayer2.//' diff --git a/resources/config/hyprpaper.conf b/resources/config/hyprpaper.conf new file mode 100644 index 00000000..2bbdeb9c --- /dev/null +++ b/resources/config/hyprpaper.conf @@ -0,0 +1,9 @@ +# Default hyprpaper configuration for colorshell +# Added by the install.sh script, because there was no wallpaper set before + +splash = true +wallpaper { + monitor = * + path = $WALL_PATH + fit_mode = cover +} diff --git a/resources/styles/_bar.scss b/resources/styles/_bar.scss index 2a7461c0..c1bca924 100644 --- a/resources/styles/_bar.scss +++ b/resources/styles/_bar.scss @@ -92,7 +92,9 @@ $color-hover: colors.$bg-primary; } .focused-client { + margin: 3px 0; padding: 0 6px; + border-radius: 16px; & image { margin-right: 6px; @@ -114,6 +116,10 @@ $color-hover: colors.$bg-primary; margin-top: -2px; } } + + &:hover { + background: rgba(colors.$fg-primary, .3); + } } .clock.open { diff --git a/scripts/socket.sh b/scripts/socket.sh index 883d1b75..d9c4138d 100644 --- a/scripts/socket.sh +++ b/scripts/socket.sh @@ -5,8 +5,8 @@ if gdbus introspect --session \ --object-path /io/github/retrozinndev/colorshell > /dev/null 2>&1; then if command -v socat > /dev/null 2>&1; then - echo "$@" | socat - "${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/colorshell.sock" - exit 0 + echo "$@" | socat - "${XDG_RUNTIME_DIR:-"/run/user/$(id -u)"}/colorshell/.sock" + exit ${?:-"0"} else echo "[warn] \`socat\` not installed, falling back to remote instance communication" fi diff --git a/scripts/sync-config.sh b/scripts/sync-config.sh deleted file mode 100644 index e4ba8a72..00000000 --- a/scripts/sync-config.sh +++ /dev/null @@ -1,60 +0,0 @@ -source ./scripts/utils.sh - -config_dirs=( - "hypr/scripts" - "hypr/shell" - "hypr/hyprlock.conf" - "hypr/hyprland.conf" - "hypr/hypridle.conf" - "kitty/kitty.conf" -) -outdir="./config" - -Clean_local() { - Send_log "info" "Cleaning local config..." - for dir in ${config_dirs[@]}; do - rm -rf $outdir/$dir - done -} - -Update_local() { - mkdir -p $outdir - for dir in ${config_dirs[@]}; do - if [[ -d "$XDG_CONFIG_HOME/$dir" ]] || [[ -f "$XDG_CONFIG_HOME/$dir" ]]; then - Send_log "Copying ${dir^}" - mkdir -p `dirname "$outdir/$dir"` - cp -r $XDG_CONFIG_HOME/$dir $outdir/$dir - else - Send_log "warn" "Looks like the ${dir^} dir is in fault! Skipping..." - fi - done -} - -Print_header - -printf "\n" -echo "!!WARNING!! Running this script may override all configuration data in current repo with host ones." -echo "This script is intended to be used only by the repository owner" -printf "\n" - -echo "Please run this script in it's current directory to avoid issues" -echo "Tip: Press [Ctrl] + [C] to stop script at any time" - -printf "\n" - -Ask "Update local repository with host configurations?" -if [[ ! $answer == y ]]; then - Send_log "Exiting" - exit 1 -fi - -printf "\n" - -Clean_local -Update_local - -if command -v git > /dev/null; then - git status -fi - -exit 0 diff --git a/scripts/utils.sh b/scripts/utils.sh index 834d08e1..0e152ea9 100644 --- a/scripts/utils.sh +++ b/scripts/utils.sh @@ -56,7 +56,7 @@ function Print_header() { function Ask() { read -n 1 -p "$@ [y/n] " answer printf '\n' - if [[ ! $answer =~ [yn] ]]; then + if [[ ! $answer =~ [ynYN] ]]; then Ask "$@" # restart if different from accepted chars fi diff --git a/src/app.ts b/src/app.ts index bb56682c..e516aeb4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,6 +1,5 @@ import "ags/overrides"; // thanks Aylur!! import "./config"; -import "./compositors"; import { PluginApps, PluginClipboard, @@ -22,10 +21,12 @@ import { createBinding, createComputed, createRoot, getScope, onCleanup, Scope } import { OSDModes, triggerOSD } from "./window/osd"; import { programArgs, programInvocationName } from "system"; import { setConsoleLogDomain } from "console"; -import { createScopedConnection, createSubscription, encoder, secureBaseBinding } from "./modules/utils"; +import { createScopedConnection, createSubscription, encoder, makeDirectory, secureBaseBinding } from "./modules/utils"; import { exec } from "ags/process"; import { NightLight } from "./modules/nightlight"; import { Backlights } from "./modules/backlight"; +import { initCompositor } from "./compositors"; +import { Input } from "./modules/input"; import GObject, { register } from "ags/gobject"; import Media from "./modules/media"; @@ -34,6 +35,9 @@ import Gio from "gi://Gio?version=2.0"; import Adw from "gi://Adw?version=1"; import AstalWp from "gi://AstalWp"; +Gio._promisify(Gio.DBus, "get", "get_finish"); +Gio._promisify(Gio.DBusProxy, "new", "new_finish"); +Gio._promisify(Gio.DBusConnection.prototype, "call", "call_finish"); const runnerPlugins: Array = [ PluginApps, @@ -47,19 +51,26 @@ const runnerPlugins: Array = [ const defaultWindows: Array = [ "bar" ]; -GLib.unsetenv("LD_PRELOAD"); +GLib.unsetenv("LD_PRELOAD"); // so child processes won't use gtk-layer-shell by default @register({ GTypeName: "Shell" }) export class Shell extends Adw.Application { private static instance: Shell; + public static runtimeDir: Gio.File = Gio.File.new_for_path(`${ + GLib.get_user_runtime_dir() ?? `/run/user/${exec("id -u").trim()}`}/colorshell`); + public static dataDir: Gio.File = Gio.File.new_for_path(`${ + GLib.get_user_data_dir() ?? `${GLib.get_home_dir()}/.local/share`}/colorshell`); + public static cacheDir: Gio.File = Gio.File.new_for_path(`${ + GLib.get_user_cache_dir() ?? `${GLib.get_home_dir()}/.cache`}/colorshell`); + #scope!: Scope; #connections = new Map | number>(); #providers: Array = []; - #gresource: Gio.Resource|null = null; #socketService!: Gio.SocketService; #socketFile!: Gio.File; + #pid: number|null = null; get scope() { return this.#scope; } @@ -175,38 +186,112 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re }); } + private async getAppPID(): Promise { + if(this.#pid != null) + return this.#pid; + + const session = await (Gio.DBus.get(Gio.BusType.SESSION, null) as unknown as Promise); + const proxy = await (Gio.DBusProxy.new( + session, + Gio.DBusProxyFlags.NONE, + null, + "io.github.retrozinndev.colorshell", + "/io/github/retrozinndev/colorshell", + "org.freedesktop.Application", + null + ) as unknown as Promise); + + const owner = proxy.get_name_owner(); + + if(!owner) + throw new Error("DBus: Couldn't get name owner to retrieve PID"); + + // @ts-ignore + const params = GLib.Variant.new_tuple([GLib.Variant.new_string(owner)]); + + const pidVariant: GLib.Variant<"(u)"> = await session.call( + "org.freedesktop.DBus", + "/org/freedesktop/DBus", + "org.freedesktop.DBus", + "GetConnectionUnixProcessID", + params, + GLib.VariantType.new("(u)"), + Gio.DBusCallFlags.NONE, + 300, + null + ); + + if(!pidVariant) + throw new Error("DBus: call to org.freedesktop.DBus.GetConnectionUnixProcessID returned a nullish value"); + + this.#pid = pidVariant.get_child_value(0).get_uint32(); + + return this.#pid; + } + private init(): void { - console.log(`Colorshell: Initializing things`); - Adw.init(); + console.log("Colorshell: Initializing things"); + + !Shell.runtimeDir.query_exists(null) && + Shell.runtimeDir.make_directory_with_parents(null); + + const pidFile = Gio.File.new_for_path(`${Shell.runtimeDir.peek_path()!}/.pid`); + this.getAppPID().then((pid) => { + try { + pidFile.replace_contents(encoder.encode( + pid.toString() + ), null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null); + } catch(e) { + console.error(e); + } + }).catch(e => { + console.error(e); + }); // load gresource from build-defined path try { - const gresourcesPath: string = GRESOURCES_FILE.startsWith('/') ? GRESOURCES_FILE : (GRESOURCES_FILE.split('/').filter(s => - s !== "" - ).map(path => { - // support environment variables at runtime - if(/^\$/.test(path)) { - const env = GLib.getenv(path.replace(/^\$/, "")); - if(env === null) - throw new Error(`Couldn't get environment variable: ${path}`); - - return env; - } - return path; - }).join('/')); - this.#gresource = Gio.Resource.load(gresourcesPath); - Gio.resources_register(this.#gresource); - - // add icons - Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!) - .add_resource_path("/io/github/retrozinndev/colorshell/icons") + const gresourcesPath: string = !/^\//.test(GRESOURCES_FILE) ? + (GRESOURCES_FILE.split('/').filter(s => s !== "").map(path => { + // support environment variables at runtime + if(/^\$/.test(path)) { + const env = GLib.getenv(path.replace(/^\$/, "")); + if(env === null) + throw new Error(`Couldn't get environment variable: ${path}`); + + return env; + } + return path; + }).join('/')) + : GRESOURCES_FILE; + + const gresource = Gio.Resource.load(gresourcesPath); + Gio.resources_register(gresource); } catch(_e) { const e = _e as Error; console.error(`Error: couldn't load gresource! Stderr: ${e.message}\n${e.stack}`); } - this.#socketFile = Gio.File.new_for_path(`${GLib.get_user_runtime_dir() ?? - `/run/user/${exec("id -u").trim()}`}/colorshell.sock`); + // add icons + Gtk.IconTheme.get_for_display(Gdk.Display.get_default()!) + .add_resource_path("/io/github/retrozinndev/colorshell/icons") + + makeDirectory(`${Shell.runtimeDir.peek_path()!}/config`); + Gio.resources_enumerate_children( + "/io/github/retrozinndev/colorshell/config", + Gio.ResourceLookupFlags.NONE + ).forEach(name => { + if(!/\..*$/.test(name)) + return; + + const data = Gio.resources_lookup_data(`/io/github/retrozinndev/colorshell/config/${name}`, null), + file = Gio.File.new_for_path(`${Shell.runtimeDir.peek_path()!}/config/${name}`); + + file.replace_contents_bytes_async(data, null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null, null); + }); + + initCompositor(); + + this.#socketFile = Gio.File.new_for_path(`${Shell.runtimeDir.peek_path()!}/.sock`); if(this.#socketFile.query_exists(null)) { console.log(`Colorshell: Deleting previous instance's socket`); @@ -278,8 +363,8 @@ you should use the socket in the XDG_RUNTIME_DIR/colorshell.sock for a faster re this.#scope = getScope(); this.#connections.set(this, this.connect("shutdown", () => this.#scope.dispose())); + Input.getDefault(); NightLight.getDefault(); - Media.getDefault(); Clipboard.getDefault(); diff --git a/src/cli/modules/lock.ts b/src/cli/modules/lock.ts new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/src/cli/modules/lock.ts @@ -0,0 +1 @@ +// TODO diff --git a/src/cli/modules/screenshot.ts b/src/cli/modules/screenshot.ts new file mode 100644 index 00000000..70b786d1 --- /dev/null +++ b/src/cli/modules/screenshot.ts @@ -0,0 +1 @@ +// TODO diff --git a/src/compositors.ts b/src/compositors.ts index eb1f17c5..8e76f157 100644 --- a/src/compositors.ts +++ b/src/compositors.ts @@ -4,16 +4,17 @@ import { Compositor } from "./modules/compositors"; import { CompositorHyprland } from "./modules/compositors/hyprland"; -const desktopName = GLib.getenv("XDG_CURRENT_DESKTOP")?.toLowerCase(); -switch(desktopName) { - case "hyprland": - Compositor.instance = new CompositorHyprland(); - break; +export function initCompositor(): void { + const desktopName = GLib.getenv("XDG_CURRENT_DESKTOP")?.toLowerCase(); + switch(desktopName) { + case "hyprland": + Compositor.instance = new CompositorHyprland(); + break; - default: - console.error(`This compositor(${desktopName}) is not yet implemented to colorshell. \ -Please contribute by implementing it if you can! :)`); - // TODO implement a common wayland compositor support using the proposed AstalWl library - break; + default: + console.error(`This compositor(${desktopName}) is not yet implemented to colorshell. \ + Please contribute by implementing it if you can! :)`); + // TODO implement a common wayland compositor support using the proposed AstalWl library + break; + } } - diff --git a/src/config.ts b/src/config.ts index d0c89f16..23f548cd 100644 --- a/src/config.ts +++ b/src/config.ts @@ -38,6 +38,11 @@ const generalConfigDefaults = { save_on_shutdown: true }, + screen_recording: { + /** include desktop audio output when screen-recording */ + include_audio: true + }, + wallpaper: { /** wallpaper positioning mode (hyprpaper) */ positioning: "cover" satisfies WallpaperPositioning, diff --git a/src/modules/apps.ts b/src/modules/apps.ts index 46e0fba4..d6f3dc86 100644 --- a/src/modules/apps.ts +++ b/src/modules/apps.ts @@ -64,6 +64,10 @@ export function getIconByAppName(appName: string): (string|undefined) { if(lookupIcon(appName.toLowerCase())) return appName.toLowerCase(); + + const nameNoSpecialChars = appName.toLowerCase().replace(/[!?:,~]/g, ""); // solution for cases like "osu!" + if(lookupIcon(nameNoSpecialChars)) + return nameNoSpecialChars; const nameReverseDNS = appName.split('.'); const lastItem = nameReverseDNS[nameReverseDNS.length - 1]; @@ -103,6 +107,17 @@ export function getAppIcon(app: (string|AstalApps.Application)): (string|undefin export function getSymbolicIcon(app: (string|AstalApps.Application)): (string|undefined) { const icon = getAppIcon(app); + if(icon === undefined) + return undefined; + + const commonSymbolic = `${icon}-symbolic`; + if(lookupIcon(commonSymbolic)) + return commonSymbolic; + + const nameNoSpecialChars = `${icon.replace(/[!?:,~]/g, "")}-symbolic`; + if(lookupIcon(nameNoSpecialChars)) + return nameNoSpecialChars; + return (icon && lookupIcon(`${icon}-symbolic`)) ? `${icon}-symbolic` : undefined; diff --git a/src/modules/arg-handler.ts b/src/modules/arg-handler.ts index f0ccf8b9..0cf86f07 100644 --- a/src/modules/arg-handler.ts +++ b/src/modules/arg-handler.ts @@ -2,17 +2,18 @@ import { Gtk } from "ags/gtk4"; import { Wireplumber } from "./volume"; import { Windows } from "../windows"; import { restartInstance } from "./reload-handler"; -import { timeout } from "ags/time"; import { Runner } from "../runner/Runner"; import { showWorkspaceNumber } from "../window/bar/widgets/Workspaces"; import { playSystemBell } from "./utils"; import { Shell } from "../app"; +import { Screenshot } from "./screenshot"; import { generalConfig } from "../config"; import { execApp } from "./apps"; +import { exec } from "ags/process"; import Media from "./media"; -import AstalIO from "gi://AstalIO"; import AstalMpris from "gi://AstalMpris"; +import GLib from "gi://GLib?version=2.0"; export type RemoteCaller = { @@ -20,7 +21,7 @@ export type RemoteCaller = { print_literal: (message: string) => void }; -let wsTimeout: AstalIO.Time|undefined; +let wsPeekTimeout: GLib.Source|undefined; const help = `Manage Astal Windows and do more stuff. From retrozinndev's colorshell, \ made using GTK4, AGS, Gnim and Astal libraries by Aylur. @@ -42,14 +43,16 @@ ${DEVEL ? ` Development Tools: dev: tools to help debugging colorshell ` : ""} -Other options: +Others: runner [initial_text]: open the application runner, optionally add an initial search. run app[.desktop] [client_modifiers]: run applications from the cli, see "run help". + lock: quick-lock your user with hyprlock. + screenshot [full|active]: select an area to screenshot(add "full" to take a full screenshot or "active" to take from the active client). peek-workspace-num [millis]: peek the workspace numbers on bar window. v, version: display current colorshell version. h, help: shows this help message. -2025 (c) retrozinndev's colorshell, licensed under the BSD 3-Clause License. +2026 (c) colorshell, licensed under the BSD 3-Clause License. https://github.com/retrozinndev/colorshell `.trim(); @@ -110,18 +113,43 @@ export function handleArguments(cmd: RemoteCaller, args: Array): number return 0; case "peek-workspace-num": - if(wsTimeout) { + if(wsPeekTimeout) { cmd.print_literal("Workspace numbers are already showing"); return 0; } showWorkspaceNumber(true); - wsTimeout = timeout(Number.parseInt(args[1]) || 2200, () => { + wsPeekTimeout = setTimeout(() => { showWorkspaceNumber(false); - wsTimeout = undefined; - }); + wsPeekTimeout = undefined; + }, Number.parseInt(args[1]) || 2200); cmd.print_literal("Toggled workspace numbers"); return 0; + + case "lock": + execApp( + `hyprlock --config ${Shell.runtimeDir.peek_path()!}/config/hyprlock.conf` + ); + return 0; + + case "screenshot": + try { + exec("killall slurp"); // kill any active selection layer + } catch(_) {} + + if(args[1] !== undefined) { + if(/^f(ull)?$/.test(args[1])) { + Screenshot.getDefault().take(Screenshot.Mode.FULL).catch(e => console.error(e)); + return 0; + } + if(/^a(ctive)?$/.test(args[1])) { + Screenshot.getDefault().take(Screenshot.Mode.ACTIVE_WINDOW).catch(e => console.error(e)); + return 0; + } + } + + Screenshot.getDefault().take().catch(e => console.error(e)); // take screenshot in default mode + return 0; } cmd.printerr_literal("Error: command not found! try checking help"); diff --git a/src/modules/auth.ts b/src/modules/auth.ts index 3332a706..f1ed75eb 100644 --- a/src/modules/auth.ts +++ b/src/modules/auth.ts @@ -9,7 +9,7 @@ import GLib from "gi://GLib?version=2.0"; import AstalAuth from "gi://AstalAuth?version=0.1"; -@register({ GTypeName: "AuthAgent" }) +@register({ GTypeName: "ClshPolKit" }) export class Auth extends PolkitAgent.Listener { private static instance: Auth; #handle: any; diff --git a/src/modules/backlight.ts b/src/modules/backlight.ts index 2fa160b3..f755dc3b 100644 --- a/src/modules/backlight.ts +++ b/src/modules/backlight.ts @@ -1,104 +1,97 @@ import { monitorFile, readFile } from "ags/file"; import { exec } from "ags/process"; import GObject, { getter, ParamSpec, register, setter, signal } from "ags/gobject"; - import Gio from "gi://Gio?version=2.0"; -export namespace Backlights { - - const BacklightParamSpec = (name: string, flags: GObject.ParamFlags) => - GObject.ParamSpec.jsobject(name, null, null, flags) as ParamSpec; +@register({ GTypeName: "ClshBacklights" }) +export class Backlights extends GObject.Object { + private static instance: Backlights; - let instance: Backlights; + #backlights: Array = []; + #default: Backlights.Backlight|null = null; + #available: boolean = false; - export function getDefault(): Backlights { - if(!instance) - instance = new Backlights(); - return instance; - } + @getter(Array as unknown as ParamSpec>) + get backlights() { return this.#backlights; } - @register({ GTypeName: "Backlights" }) - class _Backlights extends GObject.Object { + @getter(GObject.Object) + get default() { return this.#default!; } - #backlights: Array = []; - #default: Backlight|null = null; - #available: boolean = false; - + /** true if there are any backlights available */ + @getter(Boolean) + get available() { return this.#available; } - @getter(Array as unknown as ParamSpec>) - get backlights() { return this.#backlights; } + public scan(): Array { + const dir = Gio.File.new_for_path(`/sys/class/backlight`), + backlights: Array = []; - @getter(BacklightParamSpec) - get default() { return this.#default!; } + let fileEnum: Gio.FileEnumerator; - /** true if there are any backlights available */ - @getter(Boolean) - get available() { return this.#available; } + try { + fileEnum = dir.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, null); + for(const backlight of fileEnum) { + try { + backlights.push(new Backlights.Backlight(backlight.get_name())); + } catch(_) {} + } + } catch(_) { + return []; + } - public scan(): Array { - const dir = Gio.File.new_for_path(`/sys/class/backlight`), - backlights: Array = []; + if(backlights.length < 1) { + if(this.#available) { + this.#available = false; + this.notify("available"); + } - let fileEnum: Gio.FileEnumerator; + this.#default = null; + this.notify("default"); + } - try { - fileEnum = dir.enumerate_children("standard::*", Gio.FileQueryInfoFlags.NONE, null); - for(const backlight of fileEnum) { - try { - backlights.push(new Backlight(backlight.get_name())); - } catch(_) {} - } - } catch(_) { - return []; + if(backlights.length > 0) { + if(this.#backlights.length < 1) { + this.#available = true; + this.notify("available"); } - if(backlights.length < 1) { - if(this.#available) { - this.#available = false; - this.notify("available"); - } - - this.#default = null; + if(!this.#default || !backlights.filter(bk => bk.path === this.#default?.path)[0]) { + this.#default = backlights[0]; this.notify("default"); } + } - if(backlights.length > 0) { - if(this.#backlights.length < 1) { - this.#available = true; - this.notify("available"); - } + this.#backlights = backlights; + this.notify("backlights"); - if(!this.#default || !backlights.filter(bk => bk.path === this.#default?.path)[0]) { - this.#default = backlights[0]; - this.notify("default"); - } - } + return backlights; + } - this.#backlights = backlights; - this.notify("backlights"); + public setDefault(bk: Backlights.Backlight): void { + this.#default = bk; + this.notify("default"); + } - return backlights; - } + constructor(scan: boolean = true) { + super(); + scan && this.scan(); + } - public setDefault(bk: Backlight): void { - this.#default = bk; - this.notify("default"); - } + public static getDefault(): Backlights { + if(!this.instance) + this.instance = new Backlights(); - constructor(scan: boolean = true) { - super(); - scan && this.scan(); - } + return this.instance; } +} + +export namespace Backlights { @register({ GTypeName: "Backlight" }) - class _Backlight extends GObject.Object { + export class Backlight extends GObject.Object { - declare $signals: GObject.Object.SignalSignatures & { - "brightness-changed": (value: number) => void - }; + declare $signals: Backlight.SignalSignatures; readonly #name: string; #path: string; @@ -116,7 +109,7 @@ export namespace Backlights { get path() { return this.#path; } @getter(Boolean) - get isDefault() { return this.path === getDefault().default?.path; } + get isDefault() { return this.path === Backlights.getDefault().default?.path; } @getter(Number) get brightness() { return this.#brightness; }; @@ -142,7 +135,7 @@ export namespace Backlights { throw new Error(`Brightness: Couldn't find brightness for "${name}"`); // notify :is-default on default backlight change - this.#conn = getDefault().connect("notify::default", () => + this.#conn = Backlights.getDefault().connect("notify::default", () => this.notify("is-default")); this.#name = name; @@ -184,7 +177,7 @@ export namespace Backlights { vfunc_dispose(): void { this.#monitor.cancel(); - getDefault().disconnect(this.#conn); + Backlights.getDefault().disconnect(this.#conn); } public emit( @@ -202,8 +195,9 @@ export namespace Backlights { } } - export const Backlights = _Backlights; - export const Backlight = _Backlight; - export type Backlight = InstanceType; - export type Backlights = InstanceType; + export namespace Backlight { + export interface SignalSignatures extends GObject.Object.SignalSignatures { + "brightness-changed": (value: number) => void; + } + } } diff --git a/src/modules/clipboard.ts b/src/modules/clipboard.ts index bd292b21..1f7f7708 100644 --- a/src/modules/clipboard.ts +++ b/src/modules/clipboard.ts @@ -8,42 +8,19 @@ import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; -export enum ClipboardItemType { - TEXT = 0, - IMAGE = 1 -} - -export class ClipboardItem { - id: number; - type: ClipboardItemType; - preview: string; - - constructor(id: number, type: ClipboardItemType, preview: string) { - this.id = id; - this.type = type; - this.preview = preview; - } -} - -export { Clipboard }; - /** Cliphist Manager and event listener * This only supports wipe and store events from cliphist */ @register({ GTypeName: "Clipboard" }) -class Clipboard extends GObject.Object { +export class Clipboard extends GObject.Object { private static instance: Clipboard; - declare $signals: GObject.Object.SignalSignatures & { - "copied": Clipboard["copied"]; - "wiped": Clipboard["wiped"]; - }; - #dbFile: Gio.File; #dbMonitor: Gio.FileMonitor; #updateDone: boolean = false; - #history = new Array; + #history = new Array; #changesTimeout: (AstalIO.Time|undefined); #ignoreChanges: boolean = false; + #procs: Array = []; @signal(GObject.TYPE_JSOBJECT) copied(_item: object) {} @signal() wiped() {}; @@ -55,6 +32,17 @@ class Clipboard extends GObject.Object { constructor() { super(); + this.#procs = [ + Gio.Subprocess.new( + ["wl-paste", "--type", "text", "--watch", "cliphist", "store"], + Gio.SubprocessFlags.STDOUT_SILENCE + ), + Gio.Subprocess.new( + ["wl-paste", "--type", "image", "--watch", "cliphist", "store"], + Gio.SubprocessFlags.STDOUT_SILENCE + ) + ]; + this.#dbFile = this.getCliphistDatabase(); this.#dbMonitor = monitorFile(this.#dbFile.get_path()!, () => { @@ -115,7 +103,7 @@ stderr for more info.`); return proc.get_exit_status() === 0; } - public async selectItem(itemToSelect: number|ClipboardItem): Promise { + public async selectItem(itemToSelect: number|Clipboard.Item): Promise { const item = await this.getItemContent(itemToSelect); let res: boolean = true; @@ -127,7 +115,7 @@ stderr for more info.`); /** Gets history item's content by its ID. * @returns the clipboard item's content */ - public async getItemContent(item: number|ClipboardItem): Promise { + public async getItemContent(item: number|Clipboard.Item): Promise { const id = (typeof item === "number") ? item : item.id; @@ -171,10 +159,10 @@ stderr for more info.`); return Gio.File.new_for_path(`${GLib.get_user_cache_dir()}/cliphist/db`); } - private getContentType(preview: string): ClipboardItemType { + private getContentType(preview: string): Clipboard.ItemType { return /^\[\[.*binary data.*x.*\]\]$/u.test(preview) ? - ClipboardItemType.IMAGE - : ClipboardItemType.TEXT; + Clipboard.ItemType.IMAGE + : Clipboard.ItemType.TEXT; } public async wipeHistory(noExec?: boolean): Promise { @@ -220,7 +208,7 @@ stderr for more info.`); id: Number.parseInt(id), preview, type: this.getContentType(preview) - } as ClipboardItem; + } as Clipboard.Item; this.#history.unshift(clipItem); @@ -238,7 +226,7 @@ stderr for more info.`); id: Number.parseInt(id), preview, type: this.getContentType(preview) - } as ClipboardItem; + } as Clipboard.Item; this.#history.push(clipItem); @@ -258,3 +246,27 @@ stderr for more info.`); return this.instance; } } + +export namespace Clipboard { + export enum ItemType { + TEXT = 0, + IMAGE = 1 + } + + export class Item { + id: number; + type: ItemType; + preview: string; + + constructor(id: number, type: ItemType, preview: string) { + this.id = id; + this.type = type; + this.preview = preview; + } + } + + export interface SignalSignatures extends GObject.Object.SignalSignatures { + "copied": (item: Clipboard.Item) => void; + "wiped": () => void; + } +} diff --git a/src/modules/compositors/hyprland.ts b/src/modules/compositors/hyprland.ts index 0a2deb8c..c4b0a9af 100644 --- a/src/modules/compositors/hyprland.ts +++ b/src/modules/compositors/hyprland.ts @@ -5,11 +5,16 @@ import AstalHyprland from "gi://AstalHyprland"; import GLib from "gi://GLib?version=2.0"; import { Socket } from "../socket"; import { exec } from "ags/process"; +import Gio from "gi://Gio?version=2.0"; +import { decoder, makeDirectory, playSystemBell } from "../utils"; +import { Shell } from "../../app"; @register({ GTypeName: "ClshCompositorHyprland" }) export class CompositorHyprland extends Compositor { #eventSock: Socket; + #configDir: Gio.File = Gio.File.new_for_path(`${Shell.runtimeDir.peek_path()!}/config/hyprland`); + #ignoreConfigReload: boolean = false; hyprland: AstalHyprland.Hyprland = AstalHyprland.get_default(); constructor() { @@ -19,6 +24,8 @@ export class CompositorHyprland extends Compositor { if(instSignature === null || instSignature.trim() === "") throw new Error("Compositor: Hyprland: Couldn't get instance signature"); + this.loadConfigs(); + this.#eventSock = new Socket( Socket.Type.CLIENT, `${GLib.get_user_runtime_dir()}/hypr/${instSignature}/.socket2.sock`, @@ -52,7 +59,7 @@ export class CompositorHyprland extends Compositor { info = undefined; } - //console.log(event, info); // debugging + //console.log(`${event}:`, info); // debugging this.handleEvents(event, data); }); } @@ -69,7 +76,8 @@ export class CompositorHyprland extends Compositor { class: focusedClient.class ?? "", initialClass: focusedClient.initialClass ?? "", mapped: focusedClient.mapped, - position: [focusedClient.at[0], focusedClient.at[1]], + position: focusedClient.at, + size: focusedClient.size, title: focusedClient.title ?? "" }); @@ -80,6 +88,20 @@ export class CompositorHyprland extends Compositor { this._focusedClient = null; this.notify("focused-client"); break; + + case "configreloaded": + if(!this.#ignoreConfigReload) { + this.#ignoreConfigReload = true; + this.source(); + return; + } + + this.#ignoreConfigReload &&= false; + break; + + case "bell": + playSystemBell(); + break; } } @@ -87,6 +109,78 @@ export class CompositorHyprland extends Compositor { return (JSON.parse(exec("hyprctl clients -j")) as Array); } + + private source(): void { + const names = Gio.resources_enumerate_children( + "/io/github/retrozinndev/colorshell/config/hyprland", + Gio.ResourceLookupFlags.NONE + ); + exec("hyprctl reload"); + names.forEach(name => { + if(!name.endsWith(".conf")) + return; + + try { + const out = exec( + `hyprctl keyword source ${this.#configDir.peek_path()!}/${name}` + ); + !/^ok.*$/.test(out) && console.log(out) + } catch(e) { + console.error(e); + } + }); + } + + /** load necessary hyprland configs from gresource */ + private loadConfigs(): void { + const userLastUpdatedFile = Gio.File.new_for_path(`${this.#configDir.peek_path()!}/.last-updated`); + const names = Gio.resources_enumerate_children( + "/io/github/retrozinndev/colorshell/config/hyprland", + null + ); + + if(userLastUpdatedFile.query_exists(null)) { + // check if data is different + const configlastUpdated = decoder.decode(Gio.resources_lookup_data( + "/io/github/retrozinndev/colorshell/config/hyprland/.last-updated", null + ).toArray()); + + const userLastUpdated = decoder.decode(userLastUpdatedFile.read(null).read_bytes(32, null).toArray()); + + if(configlastUpdated === userLastUpdated) { + this.source(); + return; // no need to update, since it's unchanged/same + } + } + + const files = names.map(name => [ + name, + Gio.resources_lookup_data( + `/io/github/retrozinndev/colorshell/config/hyprland/${name}`, + null + ) + ] satisfies [string, GLib.Bytes]); + + makeDirectory(`${Shell.runtimeDir.peek_path()!}/config/hyprland`); + files.forEach(([name, data]) => { + const file = Gio.File.new_for_path(`${this.#configDir.peek_path()!}/${name}`); + + try { + !file.query_exists(null) && + file.create(null, null); + + file.replace_contents( + data.toArray(), null, false, Gio.FileCreateFlags.REPLACE_DESTINATION, null + ); + } catch(e) { + console.error(`Compositor: Hyprland: Failed to write config file "${name}": ${(e as Error).message}`); + console.debug(e); + } + }); + + this.source(); + } + private getActiveClient(): CompositorHyprland.Client|null { const client = JSON.parse(exec("hyprctl -j activewindow")) as CompositorHyprland.Client|{}; @@ -101,10 +195,12 @@ export namespace CompositorHyprland { export type Event = "activewindow" | "activewindowv2" | "workspace" + | "configreloaded" | "windowtitle" | "windowtitlev2" | "workspacev2" | "focusedmon" + | "bell" | "focusedmonv2"; export type Client = { diff --git a/src/modules/compositors/index.ts b/src/modules/compositors/index.ts index bdb75eea..3770daf2 100644 --- a/src/modules/compositors/index.ts +++ b/src/modules/compositors/index.ts @@ -121,6 +121,7 @@ export namespace Compositor { #title: string = ""; #mapped: boolean = true; #position: [number, number] = [0, 0]; + #size: [number, number] = [1, 1]; #xwayland: boolean = false; @getter(gtype(String)) @@ -144,6 +145,9 @@ export namespace Compositor { @getter(Boolean) get mapped() { return this.#mapped; } + @getter(Array) + get size() { return this.#size; } + constructor(props: { address?: string; title?: string; @@ -152,6 +156,8 @@ export namespace Compositor { initialClass?: string; /** [x, y] */ position?: [number, number]; + /** [width, height] */ + size?: [number, number]; }) { super(); @@ -169,6 +175,9 @@ export namespace Compositor { if(props.position !== undefined) this.#position = props.position; + if(props.size !== undefined) + this.#size = props.size; + this.#initialClass = props.initialClass !== undefined ? props.initialClass : props.class; diff --git a/src/modules/config.ts b/src/modules/config.ts index 9718683f..177ab664 100644 --- a/src/modules/config.ts +++ b/src/modules/config.ts @@ -250,7 +250,7 @@ export class Config extends GObject.Object { )); } - private _getProperty(path: string, entries: Record, expectType?: ValueTypes): (any|undefined) { + private _getProperty(path: string, entries: Record, expectType?: ValueTypes, ignoreUndefined: boolean = false): (any|undefined) { let property: any = entries; const pathArray = path.split('.').filter(str => str); @@ -260,20 +260,9 @@ export class Config extends GObject.Object { property = property[currentPath as keyof typeof property]; } - if(expectType !== "any" && typeof property !== expectType) { - // return default value if not defined by user - property = this.defaults; - - for(let i = 0; i < pathArray.length; i++) { - const currentPath = pathArray[i]; - - property = property[currentPath as keyof typeof property]; - } - } - - if(expectType !== "any" && typeof property !== expectType) { - console.debug(`Config: property with path \`${path}\` not found in defaults/user-entries, returning \`undefined\``); - property = undefined; + if(!ignoreUndefined && property === undefined) { + const defaultValue = this._getProperty(path, this.defaults, expectType, true); + return defaultValue; } return property; diff --git a/src/modules/idle.ts b/src/modules/idle.ts new file mode 100644 index 00000000..9ffd3b67 --- /dev/null +++ b/src/modules/idle.ts @@ -0,0 +1 @@ +// TODO: configurable idle daemon diff --git a/src/modules/input.ts b/src/modules/input.ts new file mode 100644 index 00000000..7ce82a82 --- /dev/null +++ b/src/modules/input.ts @@ -0,0 +1,81 @@ +import { exec } from "ags/process"; +import Gio from "gi://Gio?version=2.0"; +import { Notifications } from "./notifications"; +import { getPID, killProc } from "./utils"; + + +export class Input { + private static instance: Input; + + #proc: Gio.Subprocess|null = null; + + /** how many times the IME has crashed */ + protected attempts: number = 0; + /** if the IME crashes this much, this instance won't attempt to start it again */ + public maxAttempts: number = 5; + + + constructor() { + const pid = getPID("fcitx5"); + if(pid != null) + killProc(pid); + + this.restart(); + } + + public static getDefault(): Input { + if(!this.instance) + this.instance = new Input(); + + return this.instance; + } + + /** force the IME daemon to quit */ + public exit(): void { + try { + exec("fcitx5-remote --check -e"); + } catch(e) { + // so we throw a prettier error + throw new Error("Input: Fcitx5: Failed to quit the daemon. Is it running?"); + } + } + + /** @param keep whether to restart the IME daemon after a crash/exit (restart attempts are limited by {@link maxIbusAttempts}) */ + public restart(keep: boolean = true): void { + if(this.#proc) + return; + + this.#proc = Gio.Subprocess.new( + ["fcitx5", "-r"], + Gio.SubprocessFlags.STDOUT_SILENCE | Gio.SubprocessFlags.STDERR_SILENCE + ); + + this.#proc.wait_async(null, (_, res) => { + try { + this.#proc!.wait_finish(res); + } catch(e) { + if(this.attempts === this.maxAttempts) { + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Failed to restore IME", + body: `Attempted to restart the IME Daemon, but it crashed in all the ${this.maxAttempts + } tries, try checking for a config error or a dependency in fault.` + }); + + return; + } + + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "IME crashed", + body: `An error occurred and the Daemon had to exit: ${(e as Error).message + }${keep ? ". The daemon will be automatically restarted" : ""}` + }); + + keep && this.restart(keep); + } + + console.log("Input: Fcitx5: Exited normally"); + }); + } +} diff --git a/src/modules/nightlight.ts b/src/modules/nightlight.ts index 05ca022d..ee9a5f43 100644 --- a/src/modules/nightlight.ts +++ b/src/modules/nightlight.ts @@ -3,9 +3,11 @@ import { userData } from "../config"; import GObject, { getter, register, setter } from "ags/gobject"; import GLib from "gi://GLib?version=2.0"; +import Gio from "gi://Gio?version=2.0"; +import { getPID, killProc } from "./utils"; -@register({ GTypeName: "NightLight" }) +@register({ GTypeName: "ClshNightLight" }) export class NightLight extends GObject.Object { private static instance: NightLight; @@ -14,10 +16,11 @@ export class NightLight extends GObject.Object { public static readonly identityTemperature = 6000; public static readonly maxGamma = 100; - #watchInterval: GLib.Source; + #watchInterval?: GLib.Source; #temperature: number = NightLight.identityTemperature; #gamma: number = NightLight.maxGamma; #identity: boolean = false; + #proc: Gio.Subprocess|null = null; @getter(Number) public get temperature() { return this.#temperature; } @@ -44,12 +47,23 @@ export class NightLight extends GObject.Object { constructor() { super(); - this.loadData(); - this.#watchInterval = setInterval(() => this.syncData(), 10000); + const pid = getPID("hyprsunset"); + if(pid != null) + killProc(pid); + + // we wait, because the process can take some time to quit + setTimeout(() => { + this.restartDaemon().then(() => { + this.loadData(); + this.#watchInterval = setInterval(() => this.syncData(), 10000); + }).catch(e => { + console.error("Night Light: Failed to initialize daemon(is it installed?):", e); + }); + }, 500); } vfunc_dispose(): void { - this.#watchInterval.destroy(); + this.#watchInterval?.destroy(); } public static getDefault(): NightLight { @@ -59,6 +73,34 @@ export class NightLight extends GObject.Object { return this.instance; } + public async quitDaemon(): Promise { + if(!this.#proc) + return false; + + return new Promise((resolve, reject) => { + this.#proc!.wait_async(null, (_, res) => { + let result!: boolean; + try { + result = this.#proc!.wait_finish(res); + } catch(e) { + reject(e); + return; + } + + resolve(result); + }); + + this.#proc!.force_exit(); + }); + } + + public async restartDaemon(): Promise { + if(this.#proc) + await this.quitDaemon(); + + this.#proc = Gio.Subprocess.new(["hyprsunset"], Gio.SubprocessFlags.STDOUT_SILENCE); + } + private syncData(): void { execAsync("hyprctl hyprsunset temperature").then(t => { if(t.trim() !== "" && t.trim().length <= 5) { diff --git a/src/modules/recording.ts b/src/modules/recording.ts index ddb82ebb..1f3973fe 100644 --- a/src/modules/recording.ts +++ b/src/modules/recording.ts @@ -2,13 +2,14 @@ import { execAsync } from "ags/process"; import { getter, register, signal } from "ags/gobject"; import { Gdk } from "ags/gtk4"; import { createRoot, getScope, Scope } from "ags"; -import { makeDirectory } from "./utils"; +import { createSubscription, makeDirectory } from "./utils"; import { Notifications } from "./notifications"; import { time } from "./utils"; import GObject from "ags/gobject"; import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; +import { generalConfig } from "../config"; @register({ GTypeName: "Recording" }) @@ -24,7 +25,6 @@ export class Recording extends GObject.Object { /** Default extension: mp4(h264) */ #extension: string = "mp4"; - #recordAudio: boolean = false; #area: (Gdk.Rectangle|null) = null; #startedAt: number = -1; #process: (Gio.Subprocess|null) = null; @@ -82,19 +82,10 @@ export class Recording extends GObject.Object { /** Recording output file name. null if screen is not being recorded */ public get output() { return this.#output; } - /** Currently unsupported property */ - public get recordAudio() { return this.#recordAudio; } - public set recordAudio(newValue: boolean) { - if(this.recording) return; - - this.#recordAudio = newValue; - this.notify("record-audio"); - } constructor() { super(); - const videosDir = GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS); - if(videosDir) this.#path = `${videosDir}/Recordings`; + this.#path = `${GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_VIDEOS)}/Recordings`; } public static getDefault() { @@ -122,6 +113,7 @@ export class Recording extends GObject.Object { this.#process = Gio.Subprocess.new([ "wf-recorder", ...(area ? [ `-g`, areaString ] : []), + ...(generalConfig.getProperty("screen_recording.include_audio") ? ["-a"] : []), "-f", `${this.path}/${this.output!}` ], Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE); @@ -133,11 +125,7 @@ export class Recording extends GObject.Object { this.#startedAt = time.get().to_unix(); this.notify("started-at"); - const timeSub = time.subscribe(() => { - this.notify("recording-time"); - }); - - this.#recordingScope.onCleanup(timeSub); + createSubscription(time, () => this.notify("recording-time")); }); } @@ -168,11 +156,15 @@ export class Recording extends GObject.Object { onAction: () => { execAsync(["xdg-open", `${path}/${output}`]); } + }, { + text: "Open file directory", + id: "view-directory", + onAction: () => execAsync(`xdg-open '${path}'`) } ], - appName: "Screen Recording", - summary: "Screen Recording saved", - body: `Saved as ${path}/${output}` + appName: "colorshell", + summary: "Screen Recording", + body: `Saved recording as "${path}/${output}"` }); } }; diff --git a/src/modules/reload-handler.ts b/src/modules/reload-handler.ts index 18161f25..7f686f17 100644 --- a/src/modules/reload-handler.ts +++ b/src/modules/reload-handler.ts @@ -1,15 +1,8 @@ -import { uwsmIsActive } from "./apps"; - -import Gio from "gi://Gio?version=2.0"; +import { execApp } from "./apps"; import { Shell } from "../app"; export function restartInstance(): void { - Gio.Subprocess.new( - ( uwsmIsActive ? - [ "uwsm", "app", "--", "colorshell" ] - : [ "colorshell" ]), - Gio.SubprocessFlags.STDOUT_PIPE | Gio.SubprocessFlags.STDERR_PIPE - ); + execApp("colorshell"); Shell.getDefault().quit(); } diff --git a/src/modules/screenshot.ts b/src/modules/screenshot.ts new file mode 100644 index 00000000..4e4eb99c --- /dev/null +++ b/src/modules/screenshot.ts @@ -0,0 +1,210 @@ +import { gtype, property, register, signal } from "ags/gobject"; +import { execAsync } from "ags/process"; +import { Compositor } from "./compositors"; +import { Notifications } from "./notifications"; +import GObject from "gi://GObject?version=2.0"; +import Gio from "gi://Gio?version=2.0"; +import GLib from "gi://GLib?version=2.0"; + + +/** screen-shotting tool for colorshell, based on grim and slurp */ +@register({ GTypeName: "ClshScreenshotTool" }) +export class Screenshot extends GObject.Object { + private static instance: Screenshot; + declare $signals: Screenshot.SignalSignatures; + + @signal(String) + tookScreenshot(_: string) {} + + /** default screenshot mode to fallback to if none is specified */ + @property(gtype(Number)) + mode: Screenshot.Mode = Screenshot.Mode.SELECT; + + /** include mouse cursors in the screenshot */ + @property(Boolean) + includeCursors: boolean = false; + + /** whether to copy the screenshot image to clipboard */ + @property(Boolean) + copyToClipboard: boolean = true; + + @property(Gio.File) + outputDir: Gio.File = Gio.File.new_for_path(`${ + GLib.get_user_special_dir(GLib.UserDirectory.DIRECTORY_PICTURES) ?? + `${GLib.get_home_dir()}/Pictures` + }/Screenshots`); + + + public static getDefault(): Screenshot { + if(!this.instance) + this.instance = new Screenshot(); + + return this.instance; + } + + /** takes a screenshot using grim+slurp with the specified geometry and mode. + * @returns the screenshot image output path; or null if the screenshot was cancelled\ + * (only possible in SELECT mode) */ + async take(mode: Screenshot.Mode = this.mode, area?: Screenshot.Area): Promise { + const output = `${this.outputDir.peek_path()!}/${this.genFileName()}_screenshot.png`; + + if(mode === Screenshot.Mode.SELECT) { + const selection = area ?? await this.select(); + + if(selection === null) + return null; + + try { + await this.grim(output, selection); + } catch(e) { + console.error(e); + return null; + } + + return output; + } + + if(mode === Screenshot.Mode.ACTIVE_WINDOW) { + const activeClient = Compositor.getDefault().focusedClient; + + if(activeClient) { + const clientArea = new Screenshot.Area({ + x: activeClient.position[0], + y: activeClient.position[1], + width: activeClient.size[0], + height: activeClient.size[1] + }); + + // TODO: implement Workspace access via Client object to change to its workspace if not currently in it + + try { + await this.grim(output, clientArea); + } catch(e) { + console.error(e); + return null; + } + return output; + } + } + + try { + await this.grim(output); + return output; + } catch(e) { + console.error(e); + } + + return null; + } + + /** creates a new area selection layer for the user to interact with. + * + * @returns a `Screenshot.Area` object, containing the selected area geometry; or \ + * null if the user cancelled the selection */ + async select(): Promise { + let region!: string; + try { + region = await execAsync("slurp"); + } catch(e) { + return null; // if the user cancelled the selection + } + + return this.slurpToArea(region); + } + + protected async grim(output: string, area?: Screenshot.Area): Promise { + try { + await execAsync(`grim ${ + area ? `-g "${this.areaToSlurp(area)}"` : "" + } ${this.includeCursors ? "-c" : ""} ${output}`); + execAsync(`sh -c "cat '${output}' | wl-copy"`).catch(e => + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Failed to copy Screenshot", + body: `The recent screenshot you took couldn't be automatically copied because of an error: ${(e as Gio.IOErrorEnum).message}` + }) + ); + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Screenshot", + body: `The screenshot was saved as ${output}`, + image: output, + actions: [{ + id: "view", + text: "View", + onAction: () => execAsync(`xdg-open "${output}"`).catch(e => + Notifications.getDefault().sendNotification({ + appName: "colorshell", + summary: "Failed to open screenshot", + body: `Failed to open image with xdg-open: ${(e as Error).message}`, + }) + ) + }] + }); + } catch(e) { + throw new Error(`Screenshot: Failed to take a screenshot. Stderr: ${(e as Error).message}`); + } + } + + /** generates a screenshot file name based in the current local time */ + protected genFileName(): string { + return GLib.DateTime.new_now_local().format("%Y-%m-%d-%H-%M-%S")!; + } + + /** translates a slurp geometry string to a `Screenshot.Area` object */ + protected slurpToArea(geometry: string): Screenshot.Area { + if(!/^(\d)+,(\d)+ (\d)+x(\d)+$/.test(geometry)) + throw new Error("Screenshot: the provided slurp geometry is invalid and could not be translated"); + + const [pos, size] = geometry.split(' '); + const [x, y] = pos.split(',').map(s => Number.parseInt(s)), + [width, height] = size.split('x').map(s => Number.parseInt(s)); + + return new Screenshot.Area({ x, y, width, height }); + } + + /** translates a `Screenshot.Area` object to a valid slurp geometry */ + protected areaToSlurp(area: Screenshot.Area): string { + return `${area.x},${area.y} ${area.width}x${area.height}`; + } +} + +export namespace Screenshot { + export enum Mode { + /** open a selection layer to select the screenshot area */ + SELECT = 0, + /** full screenshot */ + FULL = 1, + /** take a screenshot of the active client(window); if there's none, fallback to FULL */ + ACTIVE_WINDOW = 2 + } + + export class Area { + public readonly x: number; + public readonly y: number; + public readonly width: number; + public readonly height: number; + + constructor(props: { + x: number, + y: number, + width: number, + height: number + }) { + this.x = props.x; + this.y = props.y; + this.width = props.width; + this.height = props.height; + } + } + + export interface SignalSignatures extends GObject.Object.SignalSignatures { + /** triggered when a screenshot is finished. + * @param mode the screenshot area mode + * @param path the output file of the screenshot + * */ + "took-screenshot": (mode: Screenshot.Mode, path: string) => void; + "notify::mode": () => void; + "notify::include-cursors": () => void; + } +} diff --git a/src/modules/socket.ts b/src/modules/socket.ts index 5e0d62e8..58f7a2f0 100644 --- a/src/modules/socket.ts +++ b/src/modules/socket.ts @@ -28,8 +28,8 @@ export class Socket extends GObject. protected server: Gio.SocketService|null = null; protected address: Gio.UnixSocketAddress|null = null; protected scope: Scope = createRoot(() => getScope()); - protected encoder: TextEncoder|null = null; - protected decoder: TextDecoder|null = null; + protected readonly encoder = new TextEncoder(); + protected readonly decoder = new TextDecoder("utf-8"); @signal(String) protected received(_: string) {} @@ -37,8 +37,8 @@ export class Socket extends GObject. @signal(String) protected sent(_: string) {} - @signal(Gio.IOErrorEnum) - protected panic(_: Gio.IOErrorEnum) {} + @signal(Object) + protected panic(_: Error) {} /** @param listen whether to listen to the socket as soon as the class is instantiated * @param contentSeparator server response separator, by default it's a line-break */ @@ -76,7 +76,7 @@ export class Socket extends GObject. try { str = data.read_upto_finish(res)[0]; } catch(e) { - this.emit("panic", e as Gio.IOErrorEnum); + this.emit("panic", e as Error); return; } @@ -108,58 +108,123 @@ export class Socket extends GObject. return; } - const sock = conn.get_socket(); - sock.set_timeout(0); - sock.set_blocking(false); - sock.set_keepalive(true); // keep listening to the socket - - GLib.io_add_watch( - GLib.IOChannel.unix_new(sock.get_fd()), - GLib.PRIORITY_DEFAULT, - GLib.IOCondition.IN | GLib.IOCondition.PRI | GLib.IOCondition.HUP, - (_, cond: GLib.IOCondition) => { - if(cond === GLib.IOCondition.HUP) { - conn.close(); - console.log(`Socket: Connection was hang up`); - return false; - } + this.watchOutput(conn, (data) => { + // separate messages by line-break + data.split(outputSeparator).filter(s => s.trim() !== "").forEach(msg => + this.emit("received", msg) + ); + }); + }); + } + /** watch a socket for new messages using `GIOWatch`. + * @param socket the socket or connection to watch + * @param callback called when the watch triggers one of the `conditions` + * @param conditions `GIOConditions` to watch the socket IO for. default: `IN`|`PRI`|`HUP` + * (when `HUP`, the watch is internally removed, so you just need to disconnect from the socket) + * + * @returns a `GCancellable`, which can be used to stop the `GIOWatch` of the method */ + protected watchSocket( + socket: Gio.Socket|Gio.SocketConnection, + callback?: (condition: GLib.IOCondition) => void, + conditions: GLib.IOCondition = GLib.IOCondition.IN|GLib.IOCondition.PRI|GLib.IOCondition.HUP + ): Gio.Cancellable { + socket = socket instanceof Gio.SocketConnection ? + socket.get_socket() + : socket; + + const cancellable = Gio.Cancellable.new(); + const chaneru = GLib.IOChannel.unix_new(socket.get_fd()); + const source = GLib.io_create_watch(chaneru, conditions); + + source.set_priority(GLib.PRIORITY_LOW); + source.set_callback(((_: GLib.IOChannel, condition: GLib.IOCondition) => { + if(condition === GLib.IOCondition.HUP) { + cancellable.cancel(); + callback?.(condition); + return false; + } - if(conn.is_closed()) { - console.log("Socket: The listening input stream has been closed, ignoring current call"); - return false; - } + if(callback) + return callback(condition); - if(conn.inputStream.is_closed()) { - console.log("Socket: The input stream got closed, the i/o watch will be removed"); - return false; - } + return true; + }) as never); + source.attach(null); + + const id = GObject.Object.prototype.connect.call(cancellable, "cancelled", () => { + cancellable.disconnect(id); + source.destroy(); + }); - if(conn.outputStream.is_closed()) { - console.log("Socket: The input stream got closed, the i/o watch will be removed"); - return false; - } + return cancellable; + } - conn.inputStream.read_bytes_async(4096, GLib.PRIORITY_DEFAULT, null, (_, res) => { - let str!: string; + /** watch a socket connection for output + * when `timeout` is reached, the `GIOWatch` will be destroyed and `GCancellable::cancelled` eimtted. + * + * @param conn the `GSocketConnection` to watch for output + * @param callback called when the connection receives data + * @param timeout maximum time to wait for a response in milliseconds. set to -1 to run indefinitely. + * + * @returns a `GCancellable`, which can be used to cancel the `GIOWatch` */ + protected watchOutput( + conn: Gio.SocketConnection, + callback?: (data: string) => void, + timeout: number = -1, + onTimeout?: () => void + ): Gio.Cancellable { + let pending: boolean = false; + const cancellable = this.watchSocket(conn, (cond) => { + if(cond === GLib.IOCondition.HUP) { + conn.close(null); + console.log("Socket: IO and Socket Connection were closed"); + cancellable.cancel(); + return false; + } - try { - str = (this.decoder ?? (this.decoder = new TextDecoder)) - .decode(conn.inputStream.read_bytes_finish(res).toArray()); - } catch(e) { - console.error(`Socket: An error occurred while reading the input stream bytes: ${(e as Error).message}`); - console.debug(e); - return; - } + if(conn.is_closed()) { + console.warn("Socket: The Socket connection is closed"); + cancellable.cancel(); + return false; + } - str.split(outputSeparator).filter(s => s.trim() !== "").forEach(msg => - this.emit("received", msg) - ); - }); - return true; + if(conn.inputStream.is_closed() || conn.outputStream.is_closed()) { + console.warn("Socket: One of the streams got closed, the IO watch will be removed"); + cancellable.cancel(); + return false; + } + + if(pending) { + console.debug("Socket: Stream: There already is a read on its way, this call was ignored"); + cancellable.cancel(); + return true; + } + + pending = true; + conn.inputStream.read_bytes_async(2048, GLib.PRIORITY_DEFAULT, null, (_, res) => { + let str!: string; + pending = false; + + try { + str = this.decoder.decode(conn.inputStream.read_bytes_finish(res).toArray()); + } catch(e) { + console.error("Socket: An error occurred while reading the input stream bytes:", e); + return; } - ); + + callback?.(str); + }); + return true; }); + + if(timeout >= 0) + setTimeout(() => { + cancellable.cancel(); + onTimeout?.(); + }, timeout); + + return cancellable; } /** allows to create a connection paired with the instance scope. @@ -179,12 +244,13 @@ export class Socket extends GObject. * sends a message to the socket using a GSocketConnection. * * @param message contents to send to the socket - * @param wait whether to wait for the socket to finish the connection + * @param wait whether to wait for the socket to send a response * * @returns a `string` promise, that returns the socket's response to the message, can be null. */ async simpleSend(message: string, wait: boolean = false): Promise { return new Promise((resolve, reject) => { const client = Gio.SocketClient.new(); + client.set_family(Gio.SocketFamily.UNIX); client.set_socket_type(Gio.SocketType.STREAM); client.connect_async(this.address!, null, (_, res) => { @@ -197,8 +263,8 @@ export class Socket extends GObject. } conn.outputStream.write_bytes_async( - (this.encoder ?? (this.encoder = new TextEncoder())).encode(message), - GLib.PRIORITY_DEFAULT, + this.encoder.encode(message), + GLib.PRIORITY_LOW, null, (_, res) => { try { @@ -210,43 +276,20 @@ export class Socket extends GObject. return; } - let output: string = ""; - const chaneru = GLib.IOChannel.unix_new(conn.get_socket().get_fd()); - GLib.io_add_watch( - chaneru, - GLib.PRIORITY_DEFAULT, - GLib.IOCondition.IN | GLib.IOCondition.PRI | GLib.IOCondition.HUP, - (_, cond) => { - if(cond === GLib.IOCondition.HUP) { - resolve(output); - conn.close(); - return false; - } - - const data = Gio.DataInputStream.new(conn.inputStream); - data.read_upto_async('\x00', -1, GLib.PRIORITY_DEFAULT, null, (_, res) => { - let out!: string; - try { - out = data.read_upto_finish(res)[0]; - output = out; - } catch(e) { - resolve(output); - return; - } - - }); - - if(wait) { - resolve(output); - conn.close(); - return false; - } - - return true; - } - ); + if(!wait) { + resolve(null); + return; + } + + this.watchOutput(conn, (data) => { + resolve(data); + conn.close(); + }, 10000, () => { + const err = new Error("Socket: 10 Seconds timeout reached"); + reject(err); + this.emit("panic", err); + }); } catch(e) { - this.emit("panic", e as Gio.IOErrorEnum); } } ); @@ -275,7 +318,7 @@ export class Socket extends GObject. } conn.outputStream.write_bytes_async( - (this.encoder ?? (this.encoder = new TextEncoder())).encode(message), + this.encoder.encode(message), GLib.PRIORITY_DEFAULT, null, (_, res) => { @@ -290,7 +333,7 @@ export class Socket extends GObject. resolve(conn.inputStream); } catch(e) { - this.emit("panic", e as Gio.IOErrorEnum); + this.emit("panic", e as Error); } } ); @@ -324,7 +367,7 @@ export namespace Socket { /** client-only signal. emitted when a message is sent to the server */ "sent": (content: string) => void; /** emitted when a stream error occurs, if it ever happen */ - "panic": (error: Gio.IOErrorEnum) => void; + "panic": (error: Error) => void; } export enum Type { diff --git a/src/modules/utils.ts b/src/modules/utils.ts index a59b328b..bbc689c0 100644 --- a/src/modules/utils.ts +++ b/src/modules/utils.ts @@ -83,6 +83,38 @@ export function getChildren(widget: Gtk.Widget): Array { return children; } +/** search for a process by its name. (exact match) + * @returns the pid for the first search result, `undefined` if no process was found */ +export function getPID(search: string): number|undefined { + let result!: string; + + try { + result = exec(`pgrep -x "${search}"`).trim().replaceAll('\n', ''); + } catch(e) { + return undefined; + } + + const pid = Number.parseInt(result); + + if(!isNaN(pid)) + return pid; + + return undefined; +} + +/** forces a process to quit with the desired signal. + * @param pid the process id + * @param signal process signal code. default: `2` */ +export function killProc(pid: number, signal: number = 2): boolean { + try { + exec(`kill -s ${signal} ${pid}`); + } catch(_) { + return false; + } + + return true; +} + export function omitObjectKeys(obj: ObjT, keys: keyof ObjT|Array): object { const finalObject = { ...obj }; diff --git a/src/modules/wallpaper.ts b/src/modules/wallpaper.ts index 7cdea08c..95e5a9c2 100644 --- a/src/modules/wallpaper.ts +++ b/src/modules/wallpaper.ts @@ -4,7 +4,7 @@ import GObject, { register, getter, gtype, property, setter } from "ags/gobject" import Gio from "gi://Gio?version=2.0"; import GLib from "gi://GLib?version=2.0"; -import { createSubscription, encoder } from "./utils"; +import { createSubscription, encoder, getPID, killProc } from "./utils"; import { Notifications } from "./notifications"; import { generalConfig } from "../config"; import { createRoot, getScope, Scope } from "ags"; @@ -54,6 +54,7 @@ export class Wallpaper extends GObject.Object { #splash: boolean = true; #hyprpaperFile: Gio.File; #wallpapersPath: string; + #proc: Gio.Subprocess|null = null; @getter(Boolean) public get splash() { return this.#splash; } @@ -88,7 +89,14 @@ export class Wallpaper extends GObject.Object { this.getWallpaper().then((wall) => { if(wall?.trim()) this.#wallpaper = wall.trim(); - }); + }); + + const pid = getPID("hyprpaper"); + + if(pid != null) + killProc(pid); + + this.restartDaemon(); createRoot(() => { this.#scope = getScope(); @@ -164,13 +172,12 @@ export class Wallpaper extends GObject.Object { Notifications.getDefault().sendNotification({ appName: "colorshell", summary: "Wallpaper configuration", - body: "This setting will only take effect after a hyprpaper restart. If you're using systemd, \ -click the action to restart hyprpaper", + body: "This change will only take effect after a hyprpaper restart. Click the \"restart\" button to restart the wallpaper daemon", actions: [{ - text: "Restart service", - id: "restart-service", + text: "Restart", + id: "restart-daemon", onAction: () => { - execAsync("systemctl --user restart hyprpaper").catch((e) => { + this.restartDaemon().catch(e => { Notifications.getDefault().sendNotification({ appName: "colorshell", summary: "Failed to restart service", @@ -196,6 +203,38 @@ click the action to restart hyprpaper", return this.instance; } + + /** tries to kill the wallpaper daemon. + * @returns `true` on success, or else, `false` */ + public async quitDaemon(): Promise { + if(!this.#proc) + return false; + + return new Promise((resolve, reject) => { + // wait for it to close, so we can resolve() the Promise + this.#proc!.wait_async(null, (_, res) => { + let result!: boolean; + try { + result = this.#proc!.wait_finish(res); + } catch(e) { + reject(e); + return; + } + + resolve(result); + }); + + this.#proc!.force_exit(); + }); + } + + public async restartDaemon(): Promise { + if(this.#proc) + await this.quitDaemon(); + + this.#proc = Gio.Subprocess.new(["hyprpaper"], Gio.SubprocessFlags.STDOUT_SILENCE); + } + private writeChanges(): void { this.#hyprpaperFile.replace_contents_async(encoder.encode( `# This file was automatically generated by colorshell diff --git a/src/window/control-center/widgets/QuickActions.tsx b/src/window/control-center/widgets/QuickActions.tsx index 869316ba..e1a7b888 100644 --- a/src/window/control-center/widgets/QuickActions.tsx +++ b/src/window/control-center/widgets/QuickActions.tsx @@ -4,7 +4,8 @@ import { Wallpaper } from "../../../modules/wallpaper"; import { execApp } from "../../../modules/apps"; import { Accessor } from "ags"; import { createPoll } from "ags/time"; - +import { Shell } from "../../../app"; +import { Screenshot } from "../../../modules/screenshot"; import GLib from "gi://GLib?version=2.0"; import Gio from "gi://Gio?version=2.0"; @@ -16,7 +17,7 @@ function LockButton(): Gtk.Button { return { Windows.getDefault().close("control-center"); - execApp("hyprlock"); + execApp(`hyprlock --config ${Shell.runtimeDir.peek_path()!}/config/hyprlock.conf`); }} /> as Gtk.Button; } @@ -34,7 +35,8 @@ function ScreenshotButton(): Gtk.Button { return { Windows.getDefault().close("control-center"); - execApp(`sh ${GLib.get_user_config_dir()}/hypr/scripts/screenshot.sh`); + setTimeout(() => Screenshot.getDefault().take(), 1000); + }} /> as Gtk.Button; } diff --git a/update.sh b/update.sh index 61aed3da..4ec54c50 100755 --- a/update.sh +++ b/update.sh @@ -3,25 +3,17 @@ trap "printf \"\nOk, quitting beacuse you entered an exit signal. (SIGINT).\n\"; exit 1" SIGINT trap "printf \"\nOh noo!! Some application just killed the script! (SIGTERM)\"; exit 2" SIGTERM -XDG_DATA_HOME=`[[ -z "$XDG_DATA_HOME" ]] && echo -n "$HOME/.local/share" || echo -n "$XDG_DATA_HOME"` -XDG_CACHE_HOME=`[[ -z "$XDG_CACHE_HOME" ]] && echo -n $HOME/.cache || echo -n $XDG_CACHE_HOME` -XDG_CONFIG_HOME=`[[ -z "$XDG_CONFIG_HOME" ]] && echo -n "$HOME/.config" || echo -n "$XDG_CONFIG_HOME"` -BIN_HOME=`[[ -z "$BIN_HOME" ]] && echo -n "$HOME/.local/bin" || echo -n "$BIN_HOME"` -APPS_HOME=`[[ -z "$APPS_HOME" ]] && echo -n "$XDG_DATA_HOME/applications" || echo -n "$APPS_HOME"` +XDG_DATA_HOME=${XDG_DATA_HOME:-"$HOME/.local/share"} +XDG_CACHE_HOME=${XDG_CACHE_HOME:-"$HOME/.cache"} +XDG_CONFIG_HOME=${XDG_CONFIG_HOME:-"$HOME/.config"} +BIN_HOME=${BIN_HOME:-"$HOME/.local/bin"} +APPS_HOME=${APPS_HOME:-"$XDG_DATA_HOME/applications"} skip_prompts=`[[ "$1" == -y ]] && echo -n true` is_standalone=`(git remote -v > /dev/null 2>&1) || echo -n true` temp_dir="$XDG_CACHE_HOME/colorshell-installer" repo_directory=`[[ "$is_standalone" ]] && echo -n "$temp_dir/repo" || echo -n "."` -shell_configs=( - "hypr/shell" - "hypr/hypridle.conf" - "hypr/scripts" - "hypr/hyprland.conf" - "hypr/hyprlock.conf" - "kitty/kitty.conf" -) # source utils script before installation @@ -60,10 +52,9 @@ else fi # Warn user of possible issues -Send_log warn "!! By running this, you assume total responsability for \ -issues that can occur to your filesystem" -Send_log "The updater will only change shell configs, like hypr/shell \ -and kitty/kitty.conf, not user ones(hypr/user, kitty/user.conf)" +Send_log warn "!! By running this, you're assuming total responsability for any \ +issues that may occur to your filesystem" +Send_log "The updater won't modify your config files, it'll only update colorshell" [[ -z $skip_prompts ]] && \ Ask "Do you want to update colorshell?" @@ -82,34 +73,20 @@ if [[ "$answer" == y ]] || [[ "$skip_prompts" ]]; then fi fi - Ask "Nice! Update to latest stable version instead of unstable(latest commit)?" + Send_log "Fetching latest release from colorshell repository" + # use `head -n1` because for some reason, github api shows the same release 3 times :'( + latest_tag=`curl -s "$repo_api_url/releases" | jq -r '. | select(.[].prerelease == false) | .[0].tag_name' | head -n1` + + Send_log "Done fetching" + Ask "Nice! Use stable $latest_tag instead of the unstable/pre-release version?" if [[ -z "$skip_prompts" ]] && [[ "$answer" == y ]]; then - Send_log "fetching latest release from colorshell repository" - # use `head -n1` because for some reason, github api shows the same release 3 times :'( - latest_tag=`curl -s "$repo_api_url/releases" | jq -r '. | select(.[].prerelease == false) | .[0].tag_name' | head -n1` - - Send_log "Done fetching" - Send_log "Checking out latest non-pre-release version: $latest_tag" + Send_log "Checking out $latest_tag" git -C "$repo_directory" checkout $latest_tag > /dev/null 2>&1 fi - Send_log "Updating..." - - Send_log "Updating configurations" - for dir in ${shell_configs[@]}; do - dest=$XDG_CONFIG_HOME/$dir - - Send_log "Installing $dir in $dest" - mkdir -p `dirname "$dest"` # create parents - - [[ -d "$dest" ]] || [[ -f "$dest" ]] && \ - rm -rf $dest - - cp -rf $repo_directory/config/$dir "$dest" # copy - done - - Send_log "Updating dependencies" + Send_log "Starting update process..." + Send_log "Updating colorshell dependencies" pnpm -C "$repo_directory" i && pnpm -C "$repo_directory" update Send_log "Building colorshell"