This commit is contained in:
2025-10-26 16:50:08 +01:00
parent f10af1ca02
commit 428de73f48
444 changed files with 254 additions and 67 deletions

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 = Path("~/.config").expanduser()
# 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
"nwg-look": [CONFIG_DIR / "fish" / "apply-color-nwg-look"], # 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()

View File

@@ -0,0 +1,126 @@
#!/usr/bin/env bash
# Required tools:
# - zenity (for file selection dialog)
# - imagemagick (for image processing)
# - swww (wallpaper daemon)
# - notify-send (for notifications)
# - change-colortheme (from scripts/change-colortheme)
# - flock (usually part of util-linux)
# Lock
exec {LOCK_FD}>/tmp/"$(basename "$0")".lock || {
echo "Failed to open lock file"
exit 1
}
flock -n "$LOCK_FD" || {
echo "Another instance is running. Exiting."
notify-send -a "change-wallpaper" "Error" "Another instance is running. Exiting."
exit 1
}
# Open a file selection dialog if no argument 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

View File

@@ -0,0 +1,27 @@
#!/usr/bin/env bash
path="$(dirname "$(readlink -f "$0")")"
if [ -z "$1" ]; then
desktop="$XDG_CURRENT_DESKTOP"
else
desktop="$1"
fi
config_path="$path/../../.."
for item in "kitty" "ghostty" "wlogout"; do
for target in "$item" "$item-niri"; do
if [ ! -L "$HOME/.config/$target" ] && [ -e "$HOME/.config/$target" ]; then
echo "Error: $HOME/.config/$target exists and is not a symlink." >&2
exit 1
fi
stow -t "$HOME" -d "$config_path" -D "$target"
done
if [ "$desktop" = "niri" ] || [ "$desktop" = "GNOME" ]; then
stow -t "$HOME" -d "$config_path" "$item-niri"
else
stow -t "$HOME" -d "$config_path" "$item"
fi
done

View File

@@ -0,0 +1,34 @@
#!/bin/sh
path="$(dirname "$(readlink -f "$0")")"
backupDir="$HOME/.config/config-backup/$(date +%Y%m%d-%H%M%S)"
backupDirCreated=0
sources=""
if [ -z "$1" ]; then
sources=$(find "$path/../config/" -maxdepth 1 -not -path "$path/../config")
else
for arg in "$@"; do
src="$path/../config/$arg"
if [ ! -e "$src" ]; then
echo "Error: Config '$arg' does not exist." >&2
exit 1
fi
sources="$sources $path/../config/$arg"
done
fi
for src in $sources; do
name="$(basename "$src")"
dest="$HOME/.config/$name"
if [ -e "$dest" ] || [ -L "$dest" ]; then
[ "$backupDirCreated" -eq 0 ] && {
mkdir -pv "$backupDir"
backupDirCreated=1
}
mv -vf "$dest" "$backupDir/"
fi
ln -sv "$(realpath --relative-to="$HOME/.config" "$src")" "$dest"
done

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

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())

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

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

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 "$@"

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 "$@"

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

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

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

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,136 @@
#!/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 ",
}
if os.system(f"{cmd[type]}{filepath.name}"):
print("Failed to take screenshot.")
exit(1)
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()
if os.system(cmd[type]):
print("Failed to take screenshot.")
exit(1)
wait_until_file_exists(niriScreenshotPath)
if niriScreenshotPath.exists():
niriScreenshotPath.rename(filepath)
else:
print("Failed to take screenshot.")
exit(1)
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",
"Open in Editor",
edit_callback,
None
)
n.connect("closed", close_callback)
n.show()
loop.run()

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

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

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

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";
}'

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)

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

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

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))

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}%"

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"

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."

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 &

View File

@@ -0,0 +1,46 @@
[ "$(basename "$0")" = "apply-color-helper" ] && {
echo "This script is meant to be sourced, not executed directly."
exit 1
}
[ -z "$1" ] && exit 1
palette="$1"
[ -z "$2" ] && exit 1
colorName="$2"
[ -z "$3" ] && exit 1
colorHex="$3"
function log_error {
printf "\033[0;31mError:\033[0m $1\n" >&2
}
function log_info {
printf "\033[0;32mInfo:\033[0m $1\n" >&2
}
function color_ansi {
colorHex="$1"
local r=$((16#${colorHex:0:2}))
local g=$((16#${colorHex:2:2}))
local b=$((16#${colorHex:4:2}))
# 24-bit true color ANSI escape code
printf "\033[38;2;%d;%d;%dm" $r $g $b
}
# remove leading '#' if present
if [[ $colorHex == \#* ]]; then
colorHex="${colorHex#\#}"
fi
# check if hex
if ! [[ $colorHex =~ ^[0-9A-Fa-f]{6}$ ]]; then
log_error "Invalid color hex: $colorHex"
exit 1
fi
function log_success {
log_info "Applied palette \033[1;34m${palette}\033[0m with primary color $(color_ansi $colorHex)${colorName} (#${colorHex})\033[0m to \033[1;34m$1\033[0m"
}

View File

@@ -0,0 +1,11 @@
[Unit]
Description=Fetch Weather
After=network.target
[Service]
Type=oneshot
ExecStart=/home/Username_PLACEHOLDER/.config/eww/Main/scripts/weather --getdata
WorkingDirectory=/home/Username_PLACEHOLDER/.config/eww/Main/scripts
Environment=OPENWEATHER_API_KEY="Onecall_3.0_APIKey_PLACEHOLDER"
Environment=OPENWEATHER_LAT="Latitude_PLACEHOLDER"
Environment=OPENWEATHER_LON="Longitude_PLACEHOLDER"

View File

@@ -0,0 +1,10 @@
[Unit]
Description=Fetch weather information every hour
Requires=fetch-weather.service
[Timer]
OnCalendar=hourly
Persistent=true
[Install]
WantedBy=timers.target

View File

@@ -0,0 +1,69 @@
#!/usr/bin/env bash
# AMD -> Nvidia -> Intel
prefer_order=(amd nvidia intel)
# Get vendor and path of each GPU
default_dri_path="$(find /dev/dri/card* 2>/dev/null | head -n 1)"
[[ -z "$default_dri_path" ]] && default_dri_path="/dev/dri/card0"
intel_path=""
nvidia_path=""
amd_path=""
for link in /dev/dri/by-path/*-card; do
[[ -e "$link" ]] || continue
card="$(readlink -f "$link")"
vfile="/sys/class/drm/$(basename "$card")/device/vendor"
[[ -r "$vfile" ]] || continue
vendor="$(cat "$vfile")"
case "$vendor" in
0x10de) nvidia_path="$card" ;;
0x8086) intel_path="$card" ;;
0x1002) amd_path="$card" ;;
esac
done
# Specify device for brightnessctl
# Only tested on my laptop with Intel iGPU & Nvidia dGPU
BRIGHTNESSCTL_DEVICE="auto"
if [[ -n "$intel_path" ]]; then
BRIGHTNESSCTL_DEVICE="intel_backlight"
elif [[ -n "$nvidia_path" ]]; then
BRIGHTNESSCTL_DEVICE="nvidia_0"
fi
export BRIGHTNESSCTL_DEVICE
# AQ_DRM_DEVICES allows multiple entries separated by colon
devices=""
for who in "${prefer_order[@]}"; do
case "$who" in
nvidia) [[ -n "$nvidia_path" ]] && devices="${devices:+$devices:}$nvidia_path" ;;
intel) [[ -n "$intel_path" ]] && devices="${devices:+$devices:}$intel_path" ;;
amd) [[ -n "$amd_path" ]] && devices="${devices:+$devices:}$amd_path" ;;
esac
done
HYPR_AQ_DRM_DEVICES="${devices:-$default_dri_path}"
export HYPR_AQ_DRM_DEVICES
# But niri only supports choosing one preferred render device
primary_device="$default_dri_path"
for who in "${prefer_order[@]}"; do
case "$who" in
nvidia) [[ -n "$nvidia_path" ]] && { primary_device="$nvidia_path"; break; } ;;
intel) [[ -n "$intel_path" ]] && { primary_device="$intel_path"; break; } ;;
amd) [[ -n "$amd_path" ]] && { primary_device="$amd_path"; break; } ;;
esac
done
# Update niri config
for file in "$HOME/.config/niri/config.kdl" "$HOME/.config/niri/config.kdl.template"; do
[[ -f "$file" ]] || continue
if grep -qE '^\s*render-drm-device\s+"[^"]+"' "$file"; then
current="$(grep -E '^\s*render-drm-device\s+"[^"]+"' "$file" | sed -E 's/^\s*render-drm-device\s+"([^"]+)".*/\1/')"
[[ "$current" == "$primary_device" ]] && continue
sed -i -E "s|^(\s*render-drm-device\s+)\"[^\"]+\"|\1\"$primary_device\"|" "$file"
else
printf '\ndebug {\nrender-drm-device "%s"\n}\n' "$primary_device" >> "$file"
fi
done

View File

@@ -0,0 +1,3 @@
export __NV_PRIME_RENDER_OFFLOAD=1
export __GLX_VENDOR_LIBRARY_NAME=nvidia
export __VK_LAYER_NV_optimus=NVIDIA_only

View File

@@ -0,0 +1,4 @@
# __NV_PRIME_RENDER_OFFLOAD=1 __GLX_VENDOR_LIBRARY_NAME=nvidia __VK_LAYER_NV_optimus=NVIDIA_only
unset __NV_PRIME_RENDER_OFFLOAD
unset __GLX_VENDOR_LIBRARY_NAME
unset __VK_LAYER_NV_optimus