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"