danshari!
This commit is contained in:
+194
@@ -0,0 +1,194 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# Change the desktop wallpaper and generate a blurred version.
|
||||
#
|
||||
# Requirs:
|
||||
# - 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)
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Lock
|
||||
|
||||
exec {LOCK_FD}>/tmp/"$(basename "$0")".lock
|
||||
|
||||
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
|
||||
|
||||
# Obtain screen resolution
|
||||
|
||||
screen_width=${2-}
|
||||
screen_height=${3-}
|
||||
|
||||
[ -z "$screen_width" ] && {
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||
screen_width=$(hyprctl -j monitors | jq '.[0].resolution.x')
|
||||
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
screen_width=$(niri msg focused-output | grep 'Current mode' | awk '{print $3}' | cut -d'x' -f1)
|
||||
fi
|
||||
}
|
||||
|
||||
[ -z "$screen_height" ] && {
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||
screen_height=$(hyprctl -j monitors | jq '.[0].resolution.y')
|
||||
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
screen_height=$(niri msg focused-output | grep 'Current mode' | awk '{print $3}' | cut -d'x' -f2)
|
||||
fi
|
||||
}
|
||||
|
||||
## Default to 2k
|
||||
screen_width=${screen_width:-2560}
|
||||
screen_height=${screen_height:-1440}
|
||||
|
||||
# $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" 2>/dev/null || true # ignore errors
|
||||
|
||||
# Copy image to local wallpaper directory
|
||||
|
||||
## Format of current and cached wallpaper
|
||||
wallpaper_ext="png"
|
||||
## Generate a random name for the current wallpaper
|
||||
set +o pipefail # SIGPIPE is expected here
|
||||
random_name=$(tr -dc 'a-zA-Z0-9' </dev/urandom | head -c 16)
|
||||
set -o pipefail
|
||||
## Directory to store current wallpaper
|
||||
current_dir="$HOME/.local/share/wallpaper/current"
|
||||
## Path to current wallpaper image
|
||||
wallpaper_image="$current_dir/wallpaper-${random_name}.${wallpaper_ext}"
|
||||
|
||||
mkdir -p "$current_dir"
|
||||
|
||||
## Batch copy using a temporary file to avoid incomplete file being used
|
||||
temp_img=$(mktemp --suffix=."$wallpaper_ext")
|
||||
trap 'rm -f "$temp_img"' EXIT
|
||||
magick "$image" -resize "${screen_width}x${screen_height}^" -gravity center -extent "${screen_width}x${screen_height}" "$temp_img"
|
||||
cp "$temp_img" "$wallpaper_image"
|
||||
|
||||
## Generate hash for caching,
|
||||
## based on content of the source image and resolution of the resized image
|
||||
hash="$(md5sum "$image" | awk '{print $1}')-${screen_width}x${screen_height}"
|
||||
|
||||
# Clean up old wallpapers in the same directory of current wallpaper.
|
||||
# Only keep the newly added one so the wallpaper-daemon can pick it up directly
|
||||
|
||||
find "$current_dir" -type f -name "wallpaper-*" ! -name "$(basename "$wallpaper_image")" -delete
|
||||
|
||||
# Generate blurred wallpaper
|
||||
|
||||
## Similarly, store blurred version of current wallpaper in separate directory
|
||||
blur_dir="$HOME/.local/share/wallpaper/blurred"
|
||||
## Directory to cache blurred wallpapers, so that we don't need to regenerate
|
||||
## them every time when switching wallpapers. This makes it possible to have
|
||||
## an auto-played slideshow with blurred wallpapers without noticeable delay.
|
||||
blur_cache_dir="$HOME/.local/share/wallpaper/blurred-cache"
|
||||
mkdir -p "$blur_dir" "$blur_cache_dir"
|
||||
blurred_image="$blur_dir/blurred-${random_name}.${wallpaper_ext}"
|
||||
blurred_cache_image="$blur_cache_dir/${hash}.${wallpaper_ext}"
|
||||
|
||||
## Time consuming task (magick -blur) in background
|
||||
(
|
||||
# notify-send -a "change-wallpaper" "Generating Blurred Wallpaper" "This may take a few seconds..."
|
||||
|
||||
function apply_blured {
|
||||
find "$blur_dir" -type f -name "blurred-*" ! -name "$(basename "$blurred_image")" -delete
|
||||
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 Applied" "$blurred_image" -i "$blurred_image"
|
||||
}
|
||||
|
||||
### Check if cached blurred image exists
|
||||
if [ -f "$blurred_cache_image" ]; then
|
||||
# sleep 1 # Some ugly workaround
|
||||
if ! cp -f "$blurred_cache_image" "$blurred_image"; then
|
||||
echo "Could not copy cached blurred image"
|
||||
# exit 1 # Non-critical error
|
||||
else
|
||||
apply_blured
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
sigma=$(magick identify -format "%w %h" "$wallpaper_image" | 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
|
||||
}')
|
||||
|
||||
### Batch processing using a temporary file to avoid incomplete file being used
|
||||
temp_blurred=$(mktemp --suffix=."$wallpaper_ext") || exit 1
|
||||
trap 'rm -f "${temp_blurred}"' EXIT
|
||||
magick "$wallpaper_image" -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
|
||||
}
|
||||
|
||||
cp -f "$blurred_image" "$blurred_cache_image" || {
|
||||
echo "Could not cache blurred image"
|
||||
# exit 1 # Non-critical error
|
||||
}
|
||||
|
||||
apply_blured
|
||||
) &
|
||||
|
||||
# Apply wallpaper
|
||||
|
||||
skip_colortheme=0
|
||||
if [ "${4-}" = "--skip-colortheme" ]; then
|
||||
skip_colortheme=1
|
||||
fi
|
||||
|
||||
if [ "$XDG_CURRENT_DESKTOP" = "Hyprland" ]; then
|
||||
swww img -n background "$wallpaper_image" --transition-type fade --transition-duration 2 >/dev/null 2>/dev/null
|
||||
|
||||
# notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$wallpaper_image"
|
||||
|
||||
if [ "$skip_colortheme" = 0 ]; then
|
||||
change-colortheme -i "$wallpaper_image" || exit 1
|
||||
fi
|
||||
elif [ "$XDG_CURRENT_DESKTOP" = "niri" ]; then
|
||||
### Handled in wallpaper-daemon
|
||||
# swww img -n background "$wallpaper_image" --transition-type fade --transition-duration 2 > /dev/null 2> /dev/null
|
||||
|
||||
# notify-send -a "change-wallpaper" "Wallpaper Changed" "$image" -i "$wallpaper_image"
|
||||
|
||||
if [ "$skip_colortheme" = 0 ]; then
|
||||
change-colortheme -i "$wallpaper_image" || exit 1
|
||||
fi
|
||||
else
|
||||
echo "Unsupported desktop environment: $XDG_CURRENT_DESKTOP"
|
||||
exit 1
|
||||
fi
|
||||
Executable
+166
@@ -0,0 +1,166 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# Fetch and cache current weather data using OpenWeather API for use in eww widgets.
|
||||
#
|
||||
# Requirements:
|
||||
# - Environment variables:
|
||||
# - OPENWEATHER_API_KEY
|
||||
# - OPENWEATHER_LAT
|
||||
# - OPENWEATHER_LON
|
||||
|
||||
## 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
|
||||
Executable
+36
@@ -0,0 +1,36 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Description:
|
||||
# Toggle the visibility of lyrics widgets in eww.
|
||||
# - Ensure only one of the two widgets (lyrics or lyrics-single) is open at a time.
|
||||
# - Cycle through the following states:
|
||||
# 1. Both widgets closed -> Open 'lyrics'
|
||||
# 2. 'lyrics' open -> Close 'lyrics' and open 'lyrics-single' (if waybar is running)
|
||||
# 3. 'lyrics-single' open -> Close 'lyrics-single'
|
||||
# 4. Both widgets open -> Close both
|
||||
|
||||
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
|
||||
Executable
+64
@@ -0,0 +1,64 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# Script to record screen.
|
||||
# Inspired by https://github.com/end-4/dots-hyprland
|
||||
#
|
||||
# Requirements:
|
||||
# - wf-recorder
|
||||
# - slurp
|
||||
|
||||
[ -z "$codec" ] && codec="av1_nvenc"
|
||||
[ -z "$pixel_format" ] && pixel_format="p010le"
|
||||
[ -z "$frame_rate" ] && frame_rate="60"
|
||||
[ -z "$codec_params" ] && codec_params=\
|
||||
"preset=p4 rc=constqp qp=18 \
|
||||
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
|
||||
+390
@@ -0,0 +1,390 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# Description:
|
||||
# A wallpaper daemon script that integrates with swww and Niri/Hyprland to
|
||||
# automatically switch between normal and blurred wallpapers based on window focus.
|
||||
#
|
||||
# Requirements:
|
||||
# - swww (or awww case you are from the future)
|
||||
# - niri/hyprland (obviously)
|
||||
# - watchdog (python3 package)
|
||||
|
||||
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
|
||||
_lastWallpaper: Path | None = None
|
||||
_isFirst = True
|
||||
_applyLock: threading.Lock
|
||||
|
||||
def __init__(self, normalDir, blurredDir, interval=0.2):
|
||||
self._interval = interval
|
||||
self._normalDir = normalDir
|
||||
self._blurredDir = blurredDir
|
||||
self._applyLock = threading.Lock()
|
||||
|
||||
# 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:
|
||||
setBlurred = self._isBlurred.is_set()
|
||||
if setBlurred:
|
||||
wallpaper = getFirstFile(self._blurredDir)
|
||||
else:
|
||||
wallpaper = getFirstFile(self._normalDir)
|
||||
|
||||
if wallpaper is not None and wallpaper.exists():
|
||||
success = self._apply(wallpaper)
|
||||
if setBlurred != self._isBlurred.is_set():
|
||||
# State changed during apply, loop again immediately
|
||||
continue
|
||||
if success:
|
||||
# Applied successfully
|
||||
break
|
||||
|
||||
sleep(self._interval)
|
||||
|
||||
def _apply(self, wallpaper: Path) -> bool:
|
||||
with self._applyLock:
|
||||
if wallpaper == self._lastWallpaper:
|
||||
return True
|
||||
|
||||
if not swwwLoadImg("background", wallpaper):
|
||||
return False
|
||||
|
||||
self._lastWallpaper = 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("[Main] connecting to Niri socket")
|
||||
niri_socket = getNiriSocket()
|
||||
if not niri_socket:
|
||||
_log("[Main] NIRI_SOCKET environment variable is not set.")
|
||||
exit(1)
|
||||
while True:
|
||||
try:
|
||||
if not connectNiri(niri_socket, handleEvent):
|
||||
_log("[Main] Connection lost or failed.")
|
||||
except Exception as e:
|
||||
_log(f"[Main] Exception in connection loop: {e}")
|
||||
|
||||
_log("[Main] Retrying in 3 seconds...")
|
||||
sleep(3)
|
||||
|
||||
niri_socket = getNiriSocket() or niri_socket
|
||||
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)
|
||||
Executable
+51
@@ -0,0 +1,51 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# Script to toggle Waybar (start/stop/restart).
|
||||
# Also manages the lyrics widget state.
|
||||
#
|
||||
# Requirements:
|
||||
# - waybar
|
||||
# - eww (for lyrics widget)
|
||||
|
||||
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
|
||||
Executable
+18
@@ -0,0 +1,18 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# Description:
|
||||
# Create and switch to a new workspace in Hyprland.
|
||||
# e.g. if current workspaces are 1,2,4, this will create and switch to workspace 5.
|
||||
#
|
||||
# Requirements:
|
||||
# - hyprctl
|
||||
|
||||
# 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))
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/bin/sh
|
||||
|
||||
# Description:
|
||||
# Show current volume level with notification.
|
||||
# Made for mako, and is one of mako's official examples.
|
||||
#
|
||||
# Requirements:
|
||||
# - wpctl
|
||||
# - notify-send
|
||||
|
||||
# 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}%"
|
||||
Reference in New Issue
Block a user