better structure

This commit is contained in:
2025-10-19 00:14:19 +02:00
parent 057afc086e
commit 8733656ed9
630 changed files with 81 additions and 137 deletions

230
scripts/change-colortheme Executable file
View File

@@ -0,0 +1,230 @@
#!/usr/bin/env python3
import os
import sys
from pathlib import Path
import argparse
PALETTES = {
"catppuccin-mocha": {
"rosewater": "f5e0dc",
"flamingo": "f2cdcd",
"pink": "f5c2e7",
"mauve": "cba6f7",
"red": "f38ba8",
"maroon": "eba0ac",
"peach": "fab387",
"yellow": "f9e2af",
"green": "a6e3a1",
"teal": "94e2d5",
"sky": "89dceb",
"sapphire": "74c7ec",
"blue": "89b4fa",
"lavender": "b4befe",
},
}
# CONFIG_DIR = ${dotfiles_root}/config
CONFIG_DIR = Path(__file__).resolve().parent.resolve().parent.resolve() / "config"
# An application may have multiple scripts (e.g. due to config-switch)
SCRIPTS = {
"eww": [CONFIG_DIR / "eww" / "apply-color"],
"fastfetch": [CONFIG_DIR / "fastfetch" / "apply-color"],
"fuzzel": [CONFIG_DIR / "fuzzel" / "apply-color"],
"hypr": [CONFIG_DIR / "hypr" / "apply-color"],
"kvantum": [CONFIG_DIR / "fish" / "apply-color-kvantum"], # borrowing fish's directory
"mako": [CONFIG_DIR / "mako" / "apply-color"],
"niri": [CONFIG_DIR / "niri" / "apply-color"],
"oh-my-posh": [CONFIG_DIR / "fish" / "apply-color-omp"], # borrowing fish's directory
"quickshell": [CONFIG_DIR / "quickshell" / "apply-color"],
"rofi": [CONFIG_DIR / "rofi" / "apply-color"],
"waybar": [CONFIG_DIR / "waybar" / "apply-color"],
"wlogout": [CONFIG_DIR / "wlogout" / "apply-color", CONFIG_DIR / "wlogout-niri" / "apply-color"],
"yazi": [CONFIG_DIR / "yazi" / "apply-color"],
}
# or simply `find ${CONFIG_DIR} -type f -iname 'apply-color*'` to get all available scripts,
# but I need the exact application names anyway, so hardcoding does make some sense
def hex2rgb(hex_color: str) -> tuple[int, int, int]:
return tuple(int(hex_color[i:i + 2], 16) for i in (0, 2, 4)) # type: ignore
def extract_color(image_path: str) -> str:
from colorthief import ColorThief
return "#{:02x}{:02x}{:02x}".format(*ColorThief(image_path).get_color(quality=10))
def match_color(color: str, palette: dict[str, str]) -> str:
""" Matches a given color (rrggbb hex) to the closest color in the palette."""
# HUE distance of the given and returned color must no<t exceed this value
HUE_THRESHOLD = 60.0 # degrees
color = color.lower().strip().removeprefix('#')
# weigh by CCIR 601 luminosity
fr, fg, fb = 0.299 / 255 / 255, 0.587 / 255 / 255, 0.114 / 255 / 255
lfr, lfg, lfb = 0.299 / 255, 0.587 / 255, 0.114 / 255
def color_distance(c1: str, c2: str) -> float:
r1, g1, b1 = hex2rgb(c1)
r2, g2, b2 = hex2rgb(c2)
diff_l = (lfr * (r1 - r2) + lfg * (g1 - g2) + lfb * (b1 - b2))
diff_r = fr * (r1 - r2) ** 2
diff_g = fg * (g1 - g2) ** 2
diff_b = fb * (b1 - b2) ** 2
return (diff_r + diff_g + diff_b) * 0.75 + diff_l ** 2
def color_distance_hue(c1: str, c2: str) -> float:
def rgb2hue(r, g, b) -> float:
r, g, b = r / 255.0, g / 255.0, b / 255.0
mx = max(r, g, b)
mn = min(r, g, b)
diff = mx - mn
if diff == 0:
return 0.0
if mx == r:
hue = (g - b) / diff + (6 if g < b else 0)
elif mx == g:
hue = (b - r) / diff + 2
else:
hue = (r - g) / diff + 4
return hue * 60
r1, g1, b1 = hex2rgb(c1)
r2, g2, b2 = hex2rgb(c2)
return abs(rgb2hue(r1, g1, b1) - rgb2hue(r2, g2, b2))
closest_color = min(palette.keys(), key=lambda k: color_distance(color, palette[k]))
print(f"Matched color {color} to {closest_color}")
# if the hue distance is too large, rematch
if color_distance_hue(color, palette[closest_color]) > HUE_THRESHOLD:
print(f"Color {color} is too far from {closest_color}, rematching'")
else:
return closest_color
closest_color = min(palette.keys(), key=lambda k: color_distance_hue(color, palette[k]))
print(f"Rematched color {color} to {closest_color}")
return closest_color
def pick_flavor(palette: dict[str, str]) -> str:
def is_interactive() -> bool:
return sys.stdin.isatty() and sys.stdout.isatty()
def is_truecolor() -> bool:
colorterm = os.environ.get('COLORTERM', '')
term = os.environ.get('TERM', '')
return (
'truecolor' in colorterm or
'24bit' in colorterm or
term.endswith('-256color')
)
if is_interactive():
isTruecolor = is_truecolor()
print("Available flavors:")
for i, flavor in enumerate(palette.keys(), 1):
r, g, b = hex2rgb(palette[flavor])
if isTruecolor:
print(f"\033[38;2;{r};{g};{b}m█ {i}. {flavor}: #{palette[flavor]}\033[0m")
else:
print(f"{i}. {flavor}")
while True:
choice = input("Pick a flavor by number: ")
if choice.isdigit() and 1 <= int(choice) <= len(palette):
return list(palette.keys())[int(choice) - 1]
print("Invalid choice. Try again.")
else:
print("No flavor specified.")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(description="Change color theme for various applications.")
parser.add_argument('-i', '--image', type=str, help="Path to the image")
parser.add_argument('-f', '--flavor', type=str, help="Flavor to apply")
parser.add_argument('-c', '--color', type=str, help="Color to match from the palette")
parser.add_argument('arguments', nargs='*',
help="'app1 !app2' to include(only) / exclude(all but) specific applications. "
"Available apps: " + ', '.join(SCRIPTS.keys()))
arguments = parser.parse_args()
# for future use, probably
def parse_palette_name() -> str:
return "catppuccin-mocha"
def parse_flavor(palette: dict[str, str]) -> str:
if arguments.flavor:
if arguments.flavor not in palette:
print(f"Unknown flavor: {arguments.flavor}. Available flavors: {', '.join(palette.keys())}")
sys.exit(1)
flavor = arguments.flavor
elif arguments.color:
flavor = match_color(arguments.color, palette)
print(f"Matched color: {flavor}")
elif arguments.image:
if not Path(arguments.image).exists():
print(f"Image file {arguments.image} does not exist.")
sys.exit(1)
color = extract_color(arguments.image)
print(f"Extracted color {color} from image {arguments.image}")
flavor = match_color(color, palette)
print(f"Matched color: {flavor}")
else:
flavor = pick_flavor(palette)
return flavor
def parse_apps() -> tuple[set[str], set[str]]:
includes = set()
excludes = set()
for arg in arguments.arguments:
if arg.startswith('!'):
excludes.add(arg[1:])
else:
includes.add(arg)
return includes, excludes
palette_name = parse_palette_name()
palette = PALETTES[palette_name]
flavor = parse_flavor(palette)
includes, excludes = parse_apps()
apps = set()
if includes:
print(f"Including only: {', '.join(includes)}")
for app in includes:
if app in SCRIPTS:
apps.add(app)
else:
print(f"Unknown application: {app}. Available applications: {', '.join(SCRIPTS.keys())}")
sys.exit(1)
else:
apps = set(SCRIPTS.keys())
if excludes:
print(f"Excluding: {', '.join(excludes)}")
apps -= excludes
print(f"Applying flavor '{flavor}' for {len(apps)} applications.")
for app in apps:
for script in SCRIPTS[app]:
print(f"Running {script}:")
ret = os.system(f'"{script}" {palette_name} {flavor} {palette[flavor]}')
print(f"{script} exited with code {ret}")
print("")
os.system(
f'notify-send -a "change-colortheme" "Colortheme Changed" "Palette: {palette_name};\nFlavor: {flavor};\nApplied to {len(apps)} applications."')
if __name__ == "__main__":
main()

104
scripts/change-wallpaper Executable file
View File

@@ -0,0 +1,104 @@
#!/usr/bin/env bash
# Select image file if none is provided
if [ -z "$1" ]; then
image=$(zenity --file-selection --title="Open File" --file-filter="*.jpg *.jpeg *.png *.webp *.bmp *.jfif *.tiff *.avif *.heic *.heif")
else
image="$1"
fi
[ -z "$image" ] && exit 1
[ ! -f "$image" ] && exit 1
# $HOME/.config/wallpaper-chooser/config.json:
# ```json
# "sort": {
# "type": "date",
# "reverse": true
# }
# ```
# So in order to let the most recently used wallpapers appear first:
touch "$image"
# Copy image to local wallpaper directory
ext=${image##*.}
random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16)
current_dir="$HOME/.local/share/wallpaper/current"
image_copied="$current_dir/wallpaper-${random_name}.${ext}"
mkdir -p "$current_dir" || (
echo "Could not create directory $current_dir"
exit 1
)
temp_img=$(mktemp --suffix=."$ext") || exit 1
trap 'rm -f "$temp_img"' EXIT
cp "$image" "$temp_img" || exit 1
rm -f "${current_dir:?}"/wallpaper-*
cp -f "$temp_img" "$image_copied" || (
echo "Could not copy image to $current_dir"
exit 1
)
# Generate blurred wallpaper
blur_dir="$HOME/.local/share/wallpaper/blurred"
mkdir -p "$blur_dir" || (
echo "Could not create cache directory"
exit 1
)
rm -f "${blur_dir:?}"/blurred-*
blurred_image="$blur_dir/blurred-${random_name}.$ext"
## Time consuming task (magick -blur) in background
(
# notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..."
sigma=$(magick identify -format "%w %h" "$image_copied" | awk -v f=0.01 '{
m=($1>$2)?$1:$2;
s=m*f;
if(s<2) s=2;
if(s>200) s=200;
printf "%.2f", s
}')
### use a temporary file to avoid incomplete file being used
temp_blurred=$(mktemp --suffix=."$ext") || exit 1
trap 'rm -f "${temp_blurred}"' EXIT
magick "$image_copied" -blur 0x"$sigma" "$temp_blurred" || (
echo "Could not create blurred image"
exit 1
)
mv -f "$temp_blurred" "$blurred_image" || (
echo "Could not move blurred image to cache directory"
exit 1
)
if [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
swww img -n backdrop "$blurred_image" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
fi
notify-send -a "change-wallpaper" "Blurred Wallpaper Generated" "$blurred_image" -i "$blurred_image"
) &
# Apply wallpaper
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$image_copied"
change-colortheme -i "$image_copied" || exit 1
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
swww img -n background "$image_copied" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$image_copied"
change-colortheme -i "$image_copied" || exit 1
else
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
exit 1
fi

21
scripts/config-switch Executable file
View File

@@ -0,0 +1,21 @@
#!/usr/bin/env bash
path="$(dirname "$(readlink -f "$0")")"
if [ -z "$1" ]; then
desktop="$XDG_CURRENT_DESKTOP"
else
desktop="$1"
fi
for item in "kitty" "ghostty" "wlogout"; do
[ -L "$HOME/.config/$item" ] || exit 1
rm "$HOME/.config/$item"
if [ "$desktop" = "niri" ]; then
ln -s "$path/../config/$item-niri" "$HOME/.config/$item"
else
ln -s "$path/../config/$item" "$HOME/.config/$item"
fi
done

157
scripts/fetch-weather Executable file
View File

@@ -0,0 +1,157 @@
#!/usr/bin/env bash
## Collect data
cache_dir="$HOME/.cache/eww/weather"
cache_weather_stat=${cache_dir}/weather-stat
cache_weather_degree=${cache_dir}/weather-degree
cache_weather_hex=${cache_dir}/weather-hex
cache_weather_icon=${cache_dir}/weather-icon
cache_weather_updatetime=${cache_dir}/weather-updatetime
if [[ -z "$OPENWEATHER_API_KEY" ]]; then
echo "Please set the OPENWEATHER_API_KEY environment variable."
exit 1
fi
if [[ -z "$OPENWEATHER_LAT" ]]; then
echo "Please set the OPENWEATHER_LAT environment variable."
exit 1
fi
if [[ -z "$OPENWEATHER_LON" ]]; then
echo "Please set the OPENWEATHER_LON environment variable."
exit 1
fi
## Weather data
KEY=$OPENWEATHER_API_KEY
LAT=$OPENWEATHER_LAT
LON=$OPENWEATHER_LON
UNITS=metric
## Make cache dir
if [[ ! -d "$cache_dir" ]]; then
mkdir -p ${cache_dir}
fi
## Get data
get_weather_data() {
weather=`curl -sf "http://api.openweathermap.org/data/3.0/onecall?lat=${LAT}&lon=${LON}&exclude=minutely,hourly,daily&appid=${KEY}&units=${UNITS}"`
echo ${weather} >&2
weather=$(echo "$weather" | jq -r ".current")
if [ ! -z "$weather" ]; then
weather_temp=`echo "$weather" | jq ".temp" | cut -d "." -f 1`
weather_icon_code=`echo "$weather" | jq -r ".weather[].icon" | head -1`
weather_description=`echo "$weather" | jq -r ".weather[].description" | head -1 | sed -e "s/\b\(.\)/\u\1/g"`
#Big long if statement of doom
if [ "$weather_icon_code" == "50d" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "50n" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "01d" ]; then
weather_icon=" "
weather_hex="#e0af68"
elif [ "$weather_icon_code" == "01n" ]; then
weather_icon=" "
weather_hex="#c0caf5"
elif [ "$weather_icon_code" == "02d" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "02n" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "03d" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "03n" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "04d" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "04n" ]; then
weather_icon=" "
weather_hex="#7aa2f7"
elif [ "$weather_icon_code" == "09d" ]; then
weather_icon=""
weather_hex="#7dcfff"
elif [ "$weather_icon_code" == "09n" ]; then
weather_icon=""
weather_hex="#7dcfff"
elif [ "$weather_icon_code" == "10d" ]; then
weather_icon=""
weather_hex="#7dcfff"
elif [ "$weather_icon_code" == "10n" ]; then
weather_icon=""
weather_hex="#7dcfff"
elif [ "$weather_icon_code" == "11d" ]; then
weather_icon=""
weather_hex="#ff9e64"
elif [ "$weather_icon_code" == "11n" ]; then
weather_icon=""
weather_hex="#ff9e64"
elif [ "$weather_icon_code" == "13d" ]; then
weather_icon=" "
weather_hex="#c0caf5"
elif [ "$weather_icon_code" == "13n" ]; then
weather_icon=" "
weather_hex="#c0caf5"
elif [ "$weather_icon_code" == "40d" ]; then
weather_icon=" "
weather_hex="#7dcfff"
elif [ "$weather_icon_code" == "40n" ]; then
weather_icon=" "
weather_hex="#7dcfff"
else
weather_icon=" "
weather_hex="#c0caf5"
fi
echo "$weather_icon" > ${cache_weather_icon}
echo "$weather_description" > ${cache_weather_stat}
echo "$weather_temp""°C" > ${cache_weather_degree}
echo "$weather_hex" > ${cache_weather_hex}
date "+%Y-%m-%d %H:%M:%S" | tee ${cache_weather_updatetime} >/dev/null
else
echo "Weather Unavailable" > ${cache_weather_stat}
echo " " > ${cache_weather_icon}
echo "-" > ${cache_weather_degree}
echo "#adadff" > ${cache_weather_hex}
date "+%Y-%m-%d %H:%M:%S" | tee ${cache_weather_updatetime} >/dev/null
fi
}
check_network() {
local max=12
local cnt=0
while [ $cnt -lt $max ]; do
if ping -c1 8.8.8.8 &>/dev/null || ping -c1 1.1.1.1 &>/dev/null; then
return 0
fi
echo "Waiting for network connection... (attempt: $((cnt + 1))/$max)" >&2
sleep 5
((cnt++))
done
echo "Network connection failed after $max attempts." >&2
return 1
}
## Execute
if [[ -z "$1" ]]; then
if check_network; then
get_weather_data
fi
elif [[ "$1" == "--icon" ]]; then
cat ${cache_weather_icon}
elif [[ "$1" == "--temp" ]]; then
cat ${cache_weather_degree}
elif [[ "$1" == "--hex" ]]; then
tail -F ${cache_weather_hex}
elif [[ "$1" == "--stat" ]]; then
cat ${cache_weather_stat}
elif [[ "$1" == "--updatetime" ]]; then
cat ${cache_weather_updatetime}
fi

43
scripts/hypr-sdrbrightness Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env python3
# new_brightness="$1"
# [ -z "$1" ] && new_brightness=1
# sed -i "/sdrbrightness/c\ sdrbrightness = $new_brightness" <filename>
import sys
import os
if __name__ == "__main__":
if len(sys.argv) != 2:
new_brightness = 1
else:
try:
new_brightness = float(sys.argv[1])
if new_brightness < 1 or new_brightness > 1.5:
raise ValueError()
except Exception as e:
new_brightness = 1
print(f"Setting SDR brightness to: {new_brightness}\n")
config_path = os.path.expanduser("~/.config/hypr/hyprland/monitors.conf")
if not os.path.exists(config_path):
print(f"Configuration file {config_path} does not exist.")
sys.exit(1)
with open(config_path, 'r') as file:
lines = file.readlines()
for line in lines:
if "sdrbrightness" in line:
old_line = line.strip()
new_line = f" sdrbrightness = {new_brightness}\n"
lines[lines.index(line)] = new_line
print(f"Updated: {old_line} to {new_line.strip()}\n")
break
with open(config_path, 'w') as file:
file.writelines(lines)
print(f"New {config_path} content: \n")
with open(config_path, 'r') as file:
print(file.read())

23
scripts/issu Executable file
View File

@@ -0,0 +1,23 @@
#!/bin/sh
if [ "$(id -u)" -eq 0 ]; then
exit 0
fi
if [ -n "$SUDO_USER" ]; then
exit 0
fi
if [ "$LOGNAME" != "$USER" ]; then
exit 0
fi
ppid=$(ps -o ppid= -p $$ 2>/dev/null)
if [ -n "$ppid" ]; then
parent_comm=$(ps -o comm= -p "$ppid" 2>/dev/null)
if [ "$parent_comm" = "su" ]; then
exit 0
fi
fi
exit 1

27
scripts/lyrics-widgets Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/sh
LYRICS=$(eww active-windows | grep "lyrics:")
LYRICS_SINGLE=$(eww active-windows | grep "lyrics-single:")
# both are closed
if [ -z "$LYRICS" ] && [ -z "$LYRICS_SINGLE" ]; then
eww open lyrics
# only lyrics is open
elif [ -n "$LYRICS" ] && [ -z "$LYRICS_SINGLE" ]; then
eww close lyrics
# if waybar is running, open lyrics-single
if pgrep -x "waybar" -u "$USER" > /dev/null; then
sleep 0.5
eww open lyrics-single
fi
# only lyrics-single is open
elif [ -z "$LYRICS" ] && [ -n "$LYRICS_SINGLE" ]; then
eww close lyrics-single
# both are open
elif [ -n "$LYRICS" ] && [ -n "$LYRICS_SINGLE" ]; then
eww close lyrics
eww close lyrics-single
fi

5
scripts/mpv-hdr Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
export ENABLE_HDR_WSI=1
mpv --vo=gpu-next --target-colorspace-hint --gpu-api=vulkan --gpu-context=waylandvk "$@"

5
scripts/mpv-sm Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
export NVPRESENT_ENABLE_SMOOTH_MOTION=1
mpv --vo=gpu-next --gpu-api=vulkan --gpu-context=waylandvk --video-sync=audio "$@"

7
scripts/pacman-reflector Executable file
View File

@@ -0,0 +1,7 @@
#!/usr/bin/env bash
[ -z "$COUNTRY" ] && COUNTRY="Germany"
sudo cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak || exit 1
sudo reflector --country "$COUNTRY" --age 12 --protocol https --sort rate --save /etc/pacman.d/mirrorlist

58
scripts/record-script Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# https://github.com/end-4/dots-hyprland/blob/main/.config/ags/scripts/record-script.sh
[ -z "$codec" ] && codec="av1_nvenc"
[ -z "$pixel_format" ] && pixel_format="p010le"
[ -z "$frame_rate" ] && frame_rate="60"
[ -z "$codec_params" ] && codec_params=\
"preset=p5 rc=vbr cq=18 \
b:v=80M maxrate=120M bufsize=160M \
color_range=tv"
[ -z "$filter_args" ] && filter_args=""
getdate() {
date '+%Y-%m-%d_%H.%M.%S'
}
getaudiooutput() {
pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
}
getactivemonitor() {
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
niri msg focused-output | head -n 1 | sed -n 's/.*(\(.*\)).*/\1/p'
fi
}
recorder_args=(
--codec "$codec"
--pixel-format "$pixel_format"
--framerate "$frame_rate"
-f './recording_'"$(getdate)"'.mkv'
)
for param in $codec_params; do
recorder_args+=(-p "$param")
done
for filter in $filter_args; do
recorder_args+=(-F "$filter")
done
mkdir -p "$(xdg-user-dir VIDEOS)"
cd "$(xdg-user-dir VIDEOS)" || exit
if pgrep -x wf-recorder -u "$USER" > /dev/null; then
notify-send "Recording Stopped" "Stopped" -a 'record-script' &
pkill -x wf-recorder -u "$USER"
else
notify-send "Starting recording" 'recording_'"$(getdate)"'.mkv' -a 'record-script'
if [[ "$1" == "--sound" ]]; then
wf-recorder --geometry "$(slurp)" --audio="$(getaudiooutput)" "${recorder_args[@]}" & disown
elif [[ "$1" == "--fullscreen-sound" ]]; then
wf-recorder -o "$(getactivemonitor)" --audio="$(getaudiooutput)" "${recorder_args[@]}" & disown
elif [[ "$1" == "--fullscreen" ]]; then
wf-recorder -o "$(getactivemonitor)" "${recorder_args[@]}" & disown
else
wf-recorder --geometry "$(slurp)" "${recorder_args[@]}" & disown
fi
fi

5
scripts/rofi-cliphist Executable file
View File

@@ -0,0 +1,5 @@
#!/bin/sh
cliphist list | rofi -dmenu -config ~/.config/rofi/dmenu.rasi -display-columns 2 -i | \
cliphist decode | wl-copy

1870
scripts/rofi-emoji Executable file
View File

File diff suppressed because it is too large Load Diff

129
scripts/screenshot Executable file
View File

@@ -0,0 +1,129 @@
#!/usr/bin/env python3
import argparse
import os
from datetime import datetime
from enum import Enum
import time
from pathlib import Path
# autopep8: off
import gi
gi.require_version("Notify", "0.7")
from gi.repository import Notify, GLib
# autopep8: on
class ScreenshotType(Enum):
FULL = "full"
AREA = "area"
WINDOW = "window"
SCREENSHOT_DIR = Path.home() / "Pictures" / "Screenshots"
def wait_until_file_exists(filepath: Path, timeout: int = 5):
"""Wait until a file exists or timeout."""
start_time = time.time()
while not filepath.exists():
if time.time() - start_time > timeout:
return False
time.sleep(0.1)
return True
def take_screenshot(filepath: Path, typeStr: str):
type = ScreenshotType(typeStr)
currentDesktop = os.environ.get("XDG_CURRENT_DESKTOP", "")
if "Hyprland" in currentDesktop:
cmd = {
ScreenshotType.FULL: f"hyprshot -z -m output -m active -o {SCREENSHOT_DIR} -f ", # since I only have one monitor
ScreenshotType.AREA: f"hyprshot -z -m region -o {SCREENSHOT_DIR} -f ",
ScreenshotType.WINDOW: f"hyprshot -z -m window -o {SCREENSHOT_DIR} -f ",
}
os.system(f"{cmd[type]}{filepath.name}")
wait_until_file_exists(filepath)
elif "niri" in currentDesktop:
cmd = {
ScreenshotType.FULL: f"niri msg action screenshot-screen",
ScreenshotType.AREA: f"niri msg action screenshot",
ScreenshotType.WINDOW: f"niri msg action screenshot-window",
}
niriScreenshotPath = SCREENSHOT_DIR / ".niri_screenshot.png"
if niriScreenshotPath.exists():
niriScreenshotPath.unlink()
os.system(cmd[type])
wait_until_file_exists(niriScreenshotPath)
if niriScreenshotPath.exists():
niriScreenshotPath.rename(filepath)
wait_until_file_exists(filepath)
else:
print("Unsupported desktop environment.")
exit(1)
def edit_screenshot(filepath: Path):
os.system(f"gradia {filepath}")
def file_name(dir: Path, prefix="screenshot", ext=".png"):
timestamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
return f"{prefix}_{timestamp}{ext}"
if __name__ == "__main__":
parser = argparse.ArgumentParser(description="Take screenshots with hyprshot.")
parser.add_argument(
"type",
choices=[t.value for t in ScreenshotType],
help="Type of screenshot to take.",
)
args = parser.parse_args()
# file path
SCREENSHOT_DIR.mkdir(parents=True, exist_ok=True)
filename = file_name(SCREENSHOT_DIR)
filepath = SCREENSHOT_DIR / filename
# take screenshot
take_screenshot(filepath, args.type)
# check if successful
if not filepath.exists():
print("Failed to take screenshot.")
exit(1)
# create loop instance
loop = GLib.MainLoop()
editing = False
# callback on default action (edit)
def edit_callback(n, action, user_data):
global editing
editing = True
edit_screenshot(filepath)
n.close()
loop.quit()
# callback on close
def close_callback(n):
global editing
if not editing:
loop.quit()
# notification
Notify.init("Screenshot Utility")
n = Notify.Notification.new(
"Screenshot taken",
"Click to edit"
)
n.add_action(
"default",
"Default",
edit_callback,
None
)
n.connect("closed", close_callback)
n.show()
loop.run()

6
scripts/set-brightness Executable file
View File

@@ -0,0 +1,6 @@
#!/bin/sh
[ -z "$1" ] && exit 1
[ -z "$BRIGHTNESSCTL_DEVICE" ] && BRIGHTNESSCTL_DEVICE="auto"
brightnessctl -d "$BRIGHTNESSCTL_DEVICE" set "$1"

4
scripts/sl-wrap Executable file
View File

@@ -0,0 +1,4 @@
#!/bin/sh
pgrep -f spotify-lyrics -u "$USER" && (killall spotify-lyrics -u "$USER" || exit 1)
spotify-lyrics "$@"

24
scripts/smb-mount Executable file
View File

@@ -0,0 +1,24 @@
#!/bin/sh
issu && {
echo "Do not run this script in sudo mode."
exit 1
}
[ -z "$SMB_CREDENTIALS" ] && SMB_CREDENTIALS="$HOME/.smbcredentials"
[ ! -f "$SMB_CREDENTIALS" ] && exit 1
[ -z "$SMB_HOST" ] && SMB_HOST="10.8.0.1"
[ -z "$SMB_DIR" ] && SMB_DIR="share"
[ -z "$SMB_MOUNT_POINT" ] && SMB_MOUNT_POINT="/mnt/smb"
[ -z "$SMB_UID" ] && SMB_UID=$(id -u)
[ -z "$SMB_GID" ] && SMB_GID=$(id -g)
[ ! -d "$SMB_MOUNT_POINT" ] && sudo mkdir -p "$SMB_MOUNT_POINT"
if sudo mount -t cifs //"$SMB_HOST"/"$SMB_DIR" "$SMB_MOUNT_POINT" -o credentials="$SMB_CREDENTIALS",uid="$SMB_UID",gid="$SMB_GID"; then
echo "Mounted $SMB_HOST/$SMB_DIR at $SMB_MOUNT_POINT"
else
echo "Failed to mount $SMB_HOST/$SMB_DIR at $SMB_MOUNT_POINT"
exit 1
fi

9
scripts/smb-unmount Executable file
View File

@@ -0,0 +1,9 @@
#!/bin/bash
[ -z "$SMB_MOUNT_POINT" ] && SMB_MOUNT_POINT="/mnt/smb"
if sudo umount "$SMB_MOUNT_POINT" && sudo rmdir "$SMB_MOUNT_POINT"; then
echo "Unmounted and removed mount point $SMB_MOUNT_POINT"
else
exit 1
fi

26
scripts/ssh-init Executable file
View File

@@ -0,0 +1,26 @@
#!/bin/sh
# shellcheck disable=SC1091,SC1090
# `eval "$(ssh-init)"` to set up environment
# variables for ssh agent in the current shell.
# TIPS: `bass "$(ssh-init)"` case in fish
mkdir -p "$HOME/.local/state"
agent_file="$HOME/.local/state/ssh-agent"
if [ -z "$SSH_AUTH_SOCK" ]; then
if [ -f "$agent_file" ] && [ -r "$agent_file" ]; then
. "$agent_file" > /dev/null 2>&1
# check if the socket is actually working
if [ "$(ssh-add -l > /dev/null 2>&1; echo $?)" -eq 2 ]; then
unset SSH_AUTH_SOCK
fi
fi
if [ -z "$SSH_AUTH_SOCK" ]; then
rm -f "$agent_file"
eval "$(ssh-agent -s | tee "$agent_file")" > /dev/null 2>&1
fi
[ -f "$agent_file" ] && cat "$agent_file"
fi

16
scripts/truecolor-test Executable file
View File

@@ -0,0 +1,16 @@
#!/bin/bash
# Based on: https://gist.github.com/XVilka/8346728
awk -v term_cols="${width:-$(tput cols || echo 80)}" 'BEGIN{
s="/\\";
for (colnum = 0; colnum<term_cols; colnum++) {
r = 255-(colnum*255/term_cols);
g = (colnum*510/term_cols);
b = (colnum*255/term_cols);
if (g>255) g = 510-g;
printf "\033[48;2;%d;%d;%dm", r,g,b;
printf "\033[38;2;%d;%d;%dm", 255-r,255-g,255-b;
printf "%s\033[0m", substr(s,colnum%2+1,1);
}
printf "\n";
}'

358
scripts/wallpaper-daemon Executable file
View File

@@ -0,0 +1,358 @@
#!/usr/bin/env python3
import socket
import json
import subprocess
import threading
from sys import exit
from time import sleep
from os import environ, getuid
from pathlib import Path
from watchdog.observers import Observer
from watchdog.events import FileSystemEventHandler
NORMAL_WALLPAPER_DIR = Path("~/.local/share/wallpaper/current").expanduser()
BLURRED_WALLPAPER_DIR = Path("~/.local/share/wallpaper/blurred").expanduser()
def getFirstFile(dir: Path, pattern: str = "*") -> Path | None:
'''`find $dir -type f | head -n 1`'''
return next(dir.glob(pattern), None)
def getNiriSocket():
return environ['NIRI_SOCKET']
def _log(msg: str):
print(msg)
# logFIle = Path("/tmp/niri-autoblur.log")
# try:
# with logFIle.open("a") as f:
# f.write(msg + "\n")
# except Exception:
# pass
pass
def swwwLoadImg(namespace: str, wallpaper: Path):
cmd = [
"swww",
"img",
"-n",
namespace,
str(wallpaper),
"--transition-type",
"fade",
"--transition-duration",
"0.5",
]
_log(f"[SWWW] {" ".join(cmd)}")
ret = 0
try:
ret = subprocess.run(cmd, check=True).returncode
except Exception as e:
_log(f"[SWWW] failed to set wallpaper: {e}")
return False
if ret != 0:
_log(f"[SWWW] failed to set wallpaper, exit code: {ret}")
return False
return True
def swwwStartDaemon(namespace: str):
# Check if daemon is already running
cmd = ["pgrep", "-f", f"swww daemon -n {namespace}"], "-u", str(getuid())
try:
output = subprocess.check_output(cmd, text=True)
pids = output.strip().splitlines()
if pids:
_log(f"[SWWW] daemon already running with PIDs: {', '.join(pids)}")
return True
except subprocess.CalledProcessError:
# pgrep returns non-zero exit code if no process is found
pass
except Exception as e:
_log(f"[SWWW] failed to check if daemon is running: {e}")
pass
try:
subprocess.Popen(["swww-daemon", "-n", namespace], stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
_log(f"[SWWW] daemon started for namespace: {namespace}")
return True
except Exception as e:
_log(f"[SWWW] failed to start daemon non-blockingly: {e}")
return False
class AutoBlur:
_interval: float
_normalDir: Path
_blurredDir: Path
_isBlurred = threading.Event()
_thread: threading.Thread | None = None
_lastWallpaer: Path | None = None
_isFirst = True
def __init__(self, normalDir, blurredDir, interval=0.2):
self._interval = interval
self._normalDir = normalDir
self._blurredDir = blurredDir
# Niri will send "WindowsChanged" event on connect, so no need to init here
# init state
# self.setBlurred(AutoBlur.initIsBlurred())
# Start watching dirs
self.addWatchDir()
class WatchdogHandler(FileSystemEventHandler):
_callback = None
def __init__(self, callback):
if callback is None:
raise ValueError("callback cannot be None")
super().__init__()
self._callback = callback
def on_created(self, event):
if not event.is_directory:
src_path = str(event.src_path)
path = Path(src_path)
_log(f"[Watchdog] file created: {path}")
self._callback(path) # type: ignore
def on_moved(self, event):
if not event.is_directory:
dest_path = str(event.dest_path)
path = Path(dest_path)
_log(f"[Watchdog] file moved to: {path}")
self._callback(path) # type: ignore
def addWatchDir(self):
normalHandler = self.WatchdogHandler(self._onNormalDirEvent)
blurredHandler = self.WatchdogHandler(self._onBlurredDirEvent)
observer = Observer()
observer.schedule(normalHandler, str(self._normalDir), recursive=False)
observer.schedule(blurredHandler, str(self._blurredDir), recursive=False)
observer.start()
_log(f"[Watchdog] watching dirs: {self._normalDir}, {self._blurredDir}")
def _onNormalDirEvent(self, path: Path):
if not self._isBlurred.is_set():
self._apply(path)
def _onBlurredDirEvent(self, path: Path):
if self._isBlurred.is_set():
self._apply(path)
@staticmethod
def initIsBlurred() -> bool:
'''[ $(niri msg focused-window | wc -l) -gt 1 ]'''
cmd = ["niri", "msg", "focused-window"]
try:
output = subprocess.check_output(cmd, text=True)
lines = output.strip().splitlines()
return len(lines) > 1
except Exception as e:
_log(f"[initIsBlurred] failed to check focused window, assuming none: {e}")
return False
def setBlurred(self, isBlurred: bool) -> None:
# Cache state, avoid starting thread unnecessarily
if not self._isFirst and self._isBlurred.is_set() == isBlurred:
_log("[AutoBlur] state unchanged")
return
self._isFirst = False
if isBlurred:
self._isBlurred.set()
_log("[AutoBlur] set to blurred")
else:
self._isBlurred.clear()
_log("[AutoBlur] set to normal")
if self._thread is None or not self._thread.is_alive():
self._thread = threading.Thread(target=self._run, daemon=True)
self._thread.start()
def _run(self) -> None:
'''Wait until wallpapers are ready & apply the correct one according to the current state'''
while True:
if self._isBlurred.is_set():
wallpaper = getFirstFile(self._blurredDir)
else:
wallpaper = getFirstFile(self._normalDir)
if wallpaper is not None and wallpaper.exists():
if self._apply(wallpaper):
break
sleep(self._interval)
def _apply(self, wallpaper: Path) -> bool:
if wallpaper == self._lastWallpaer:
return True
if not swwwLoadImg("background", wallpaper):
return False
self._lastWallpaer = wallpaper
return True
autoBlurInst = AutoBlur(NORMAL_WALLPAPER_DIR, BLURRED_WALLPAPER_DIR)
def handleEvent(event_name, payload):
if event_name == "WindowFocusChanged":
_log(f"[EventHandler] WindowFocusChanged event received")
id = payload.get("id", "")
if isinstance(id, int):
_log(f"[EventHandler] focused window id: {id}")
autoBlurInst.setBlurred(True)
elif isinstance(id, str) and id == "None":
_log("[EventHandler] no focused window")
autoBlurInst.setBlurred(False)
else:
_log(f"[EventHandler] unknown id: {id}, assuming no focused window")
autoBlurInst.setBlurred(False)
elif event_name == "WindowsChanged":
_log(f"[EventHandler] WindowsChanged event received")
windows = payload.get("windows", [])
for window in windows:
if window.get("is_focused", False):
_log(f"[EventHandler] found focused window")
autoBlurInst.setBlurred(True)
return
_log("[EventHandler] no focused window found")
autoBlurInst.setBlurred(False)
elif event_name == "WindowOpenedOrChanged":
_log(f"[EventHandler] WindowOpenedOrChanged event received")
window = payload.get("window", {})
if window.get("is_focused", False):
_log(f"[EventHandler] opened/changed focused window")
autoBlurInst.setBlurred(True)
else:
_log(f"[EventHandler] unhandled event: {event_name}")
def printEvent(eventName, payload):
_log(f"[EventHandler] event: {eventName}, payload:\n{json.dumps(payload, indent=2, ensure_ascii=False)}")
def connectNiri(niriSocket: str, handler) -> bool:
sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
try:
sock.connect(niriSocket)
except Exception as e:
sock.close()
_log(f"[Socket] failed to connect to {niriSocket}: {e}")
return False
f = sock.makefile("rwb")
try:
f.write(b'"EventStream"\n')
f.flush()
first = f.readline()
if not first:
# raise RuntimeError("connection closed by server before handshake")
_log("[Socket] connection closed by server before handshake")
return False
try:
resp = json.loads(first.decode())
except Exception:
resp = first.decode().strip()
_log(f"[Socket] handshake response: {resp}")
while True:
line = f.readline()
if not line:
_log("[Socket] socket closed by server")
break
s = line.decode().strip()
if s == "":
continue
try:
obj = json.loads(s)
except Exception as e:
_log(f"[Socket] failed to parse line as JSON: {s}, error: {e}")
continue
keys = list(obj.keys())
if keys:
event_name = keys[0]
payload = obj[event_name]
else:
event_name = "<unknown>"
payload = obj
handler(event_name, payload)
finally:
try:
f.close()
except Exception:
pass
try:
sock.close()
except Exception:
return False
return True
if __name__ == "__main__":
# connectNiri(getNiriSocket(), printEvent)
# exit(0)
desktop = environ.get("XDG_CURRENT_DESKTOP", "")
if desktop == "niri":
_log("[Main] running in Niri")
_log("[Main] starting swww daemons")
if not swwwStartDaemon("background"):
exit(1)
if not swwwStartDaemon("backdrop"):
exit(1)
sleep(1) # give some time to start
_log("[Main] loading initial wallpapers")
# Init wallpaper for backdrop
blurred = getFirstFile(BLURRED_WALLPAPER_DIR)
if blurred:
swwwLoadImg("backdrop", blurred)
# Init wallpaper for background
normal = getFirstFile(NORMAL_WALLPAPER_DIR)
if normal:
swwwLoadImg("background", normal)
# Connect to Niri socket
_log(f"[Main] connecting to Niri socket")
niri_socket = getNiriSocket()
if not niri_socket:
_log("[Main] NIRI_SOCKET environment variable is not set.")
exit(1)
if not connectNiri(niri_socket, handleEvent):
exit(1)
elif desktop == "Hyprland":
_log("[Main] running in Hyprland")
_log("[Main] starting swww daemon")
if not swwwStartDaemon("background"):
exit(1)
sleep(1) # similarly
_log("[Main] loading initial wallpaper")
normal = getFirstFile(NORMAL_WALLPAPER_DIR)
if normal:
swwwLoadImg("background", normal)
# Wait indefinitely
while True:
sleep(3600)
else:
_log(f"[Main] unsupported desktop environment: {desktop}")
exit(1)

43
scripts/waybar-toggle Executable file
View File

@@ -0,0 +1,43 @@
#!/usr/bin/env bash
lyrics_widget_closed=0
function close() {
killall -q waybar
# Also close the lyrics widget if open
if eww active-windows | grep -q "lyrics-single"; then
eww close lyrics-single
lyrics_widget_closed=1
fi
}
function open() {
# the system tray will not work with kded6 started
if killall -q -9 "kded6"; then
while pgrep -u "$USER" -x "kded6" >/dev/null; do
sleep 0.2
done
fi
nohup waybar >/dev/null 2>/dev/null &
# Reopen the lyrics widget if it was previously closed
if [ $lyrics_widget_closed -eq 1 ]; then
eww open lyrics-single
fi
}
if [ "$1" = "restart" ]; then
close
while pgrep -u "$USER" -x "waybar" >/dev/null; do
sleep 0.2
done
open
elif pgrep -u "$USER" -x "waybar" >/dev/null; then
close
elif ! pgrep -u "$USER" -x "waybar" >/dev/null; then
open
else
echo "Usage: $0 [restart]"
exit 1
fi

15
scripts/waydroid-reload Executable file
View File

@@ -0,0 +1,15 @@
#!/usr/bin/env bash
function checkReturn {
echo "Executing: $*"
if ! "$@"; then
echo "Error runnning command"
exit 1
fi
}
checkReturn waydroid session stop
# checkReturn sudo waydroid upgrade # since I'm not using the default image
checkReturn sudo waydroid init -f
checkReturn sudo systemctl restart waydroid-container
checkReturn waydroid show-full-ui

11
scripts/workspace-new Executable file
View File

@@ -0,0 +1,11 @@
#!/usr/bin/env bash
# get highest workspace ID
max_id=$(hyprctl workspaces | grep '^workspace ID ' | awk '{print $3}' | sort -n | tail -1)
# case not found default to 0
if [ -z "$max_id" ]; then
max_id=0
fi
hyprctl dispatch workspace $((max_id + 1))

8
scripts/wp-vol Executable file
View File

@@ -0,0 +1,8 @@
#!/bin/sh
# Get the volume level and convert it to a percentage
volume=$(wpctl get-volume @DEFAULT_AUDIO_SINK@)
volume=$(echo "$volume" | awk '{print $2}')
volume=$(echo "( $volume * 100 ) / 1" | bc)
notify-send -t 1000 -a 'wp-vol' -h int:value:$volume "Volume: ${volume}%"

33
scripts/wsl-mount Executable file
View File

@@ -0,0 +1,33 @@
#!/bin/sh
issu && {
echo "Do not run this script in sudo mode."
exit 1
}
[ -z "$1" ] && echo "Usage: $0 <VHDX_PATH>" && exit 1
vhdx_path="$1"
[ -z "$2" ] && mount_point="/mnt/wsl" || mount_point="$2"
[ -d "$mount_point" ] || sudo mkdir -p "$mount_point"
username=$(whoami)
sudo chown "$username:$username" "$mount_point"
export LIBGUESTFS_BACKEND=direct
# replay log
# qemu-img check -r all "$VHDX_PATH" || {
# echo "Failed to check VHDX file."
# exit 1
# }
guestmount --add "$vhdx_path" --inspector --ro "$mount_point" || {
echo "Failed to mount VHDX file."
exit 1
}
echo "Successfully mounted $vhdx_path to $mount_point"

15
scripts/wsl-unmount Executable file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
[ -z "$1" ] && mount_point="/mnt/wsl" || mount_point="$1"
sudo umount "$mount_point" || {
echo "Failed to unmount $mount_point. It may not be mounted or you may not have the necessary permissions."
exit 1
}
sudo rmdir "$mount_point" || {
echo "Failed to remove the mount point directory $mount_point. It may not be empty or you may not have the necessary permissions."
exit 1
}
echo "Successfully unmounted and removed $mount_point."

8
scripts/xdph-nuclear Executable file
View File

@@ -0,0 +1,8 @@
#!/usr/bin/env bash
sleep 1
killall -e xdg-desktop-portal-hyprland
killall -e xdg-desktop-portal-wlr
killall xdg-desktop-portal
/usr/lib/xdg-desktop-portal-hyprland -v &
sleep 2
/usr/lib/xdg-desktop-portal &