publish: the public projection begins here
This is a projection, not a development branch. The tree above was constructed from the internal source named below under a manifest that decides which paths may leave, then scanned as a whole tree rather than as a series of patches, and only then published. Public history starts here because the history before it was not admissible, and neither was the tree. What used to stand in this repository included a rescue copy of another machine, a directory of phone handoffs, deployment wired to one house, and a submodule pointing at a forge no stranger can reach. None of that was ever the product. It stays in the private forge, which is allowed to hold the whole working organism, and this is what was deliberately sent out instead. Three mechanisms produced this tree, in decreasing order of trust. A top-level path the manifest does not name never arrives at all, which is the one that catches directories nobody has thought of yet. Named internal files inside admitted roots are dropped. A short, reviewed table replaces deployment defaults that a public build must not carry -- an endpoint aimed at one LAN, a VPN profile belonging to one phone, packaging built from one checkout path. Everything after this commit is an ordinary publication with the same three trailers, so a force push stops being routine and starts meaning that something deliberate happened. The trailers bind the projection to its source without pretending the public SHA is the private one: same lineage, different tree, and the record says so. Source-Sha: 8f27b1e76a8fef560a336aba18e6990713ff1047 Policy-Sha: 6b261d2f3e6e1fb19874846ba4bb1dfe15565d25b8618c1c1afba0419c101d27 Tree-Digest: 18ec3563c5e5ef9a414993a9f6734b251ff9ed3cd56eebdd6cac01e45c6e3067
This commit is contained in:
commit
8f42fc953d
1476 changed files with 238455 additions and 0 deletions
65
surfaces/quickshell/ii-base/scripts/ai/gemini-categorize-wallpaper.sh
Executable file
65
surfaces/quickshell/ii-base/scripts/ai/gemini-categorize-wallpaper.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "Usage: $0 <image_path> [model] [prompt]"
|
||||
echo "Tip: set GEMINI_WALLPAPER_MODEL and/or GEMINI_WALLPAPER_PROMPT to provide defaults."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
SOURCE_IMG_PATH="$1"
|
||||
MODEL="${2:-${GEMINI_WALLPAPER_MODEL:-gemini-2.5-flash}}" # We use the flash variant so it's fast
|
||||
WALLPAPER_NAME="$(basename "$SOURCE_IMG_PATH")"
|
||||
PROMPT="${3:-${GEMINI_WALLPAPER_PROMPT:-Categorize the wallpaper. Its file name is $WALLPAPER_NAME}}"
|
||||
RESIZED_IMG_PATH="/tmp/quickshell/ai/wallpaper.jpg"
|
||||
|
||||
# Resize image for speed
|
||||
mkdir -p "$(dirname "$RESIZED_IMG_PATH")"
|
||||
magick "$SOURCE_IMG_PATH" -resize 200x -quality 50 "$RESIZED_IMG_PATH"
|
||||
|
||||
# Get API key
|
||||
API_KEY=$(secret-tool lookup 'application' 'illogical-impulse' | jq -r '.apiKeys.gemini')
|
||||
|
||||
# Encode image to base64
|
||||
if [[ "$(base64 --version 2>&1)" = *"FreeBSD"* ]]; then
|
||||
B64FLAGS="--input"
|
||||
else
|
||||
B64FLAGS="-w0"
|
||||
fi
|
||||
B64DATA="$(base64 $B64FLAGS $RESIZED_IMG_PATH)"
|
||||
# echo $B64DATA
|
||||
|
||||
# Prepare request data
|
||||
payload='{
|
||||
"contents": [{
|
||||
"parts":[
|
||||
{
|
||||
"inline_data": {
|
||||
"mime_type":"image/jpeg",
|
||||
"data": "'"$B64DATA"'"
|
||||
}
|
||||
},
|
||||
{"text": "'"$PROMPT"'"}
|
||||
]
|
||||
}],
|
||||
"generationConfig": {
|
||||
"responseMimeType": "text/x.enum",
|
||||
"responseSchema": {
|
||||
"type": "string",
|
||||
"enum": [ "abstract", "anime", "city", "minimalist", "landscape", "plants", "person", "space" ]
|
||||
},
|
||||
"temperature": 0
|
||||
}
|
||||
}'
|
||||
# echo "$payload" | jq
|
||||
|
||||
# Make the request
|
||||
response=$(curl "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent" \
|
||||
-H "x-goog-api-key: $API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
-d "$payload" 2> /dev/null)
|
||||
# echo "$response" | jq
|
||||
|
||||
# Write the result
|
||||
echo "$response" | jq -r '.candidates[0].content.parts[0].text'
|
||||
65
surfaces/quickshell/ii-base/scripts/ai/gemini-translate.sh
Executable file
65
surfaces/quickshell/ii-base/scripts/ai/gemini-translate.sh
Executable file
|
|
@ -0,0 +1,65 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
if [[ -z "$1" ]]; then
|
||||
echo "Usage: $0 <target_locale> [model]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Variables
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
SHELL_CONFIG_DIR="$XDG_CONFIG_HOME/illogical-impulse"
|
||||
SHELL_CONFIG_FILE="${SHELL_CONFIG_DIR}/config.json"
|
||||
TRANSLATIONS_DIR="${SCRIPT_DIR}/../../translations"
|
||||
TRANSLATIONS_TARGET_DIR="${SHELL_CONFIG_DIR}/translations"
|
||||
SOURCE_LOCALE="en_US"
|
||||
NOTIFICATION_APP_NAME="Shell"
|
||||
TARGET_LOCALE="$1"
|
||||
MODEL="${2:-${GEMINI_MODEL:-gemini-2.5-flash}}"
|
||||
|
||||
# Update the source keys for translation
|
||||
"${TRANSLATIONS_DIR}/tools/manage-translations.sh" update -l "$SOURCE_LOCALE" --yes
|
||||
mkdir -p "$TRANSLATIONS_TARGET_DIR"
|
||||
|
||||
# Construct the prompt string
|
||||
instruction='You are to translate the user interface of a **desktop shell**. Given a JSON object of key-value pairs, return a JSON with the same structure, with keys unchanged and values translated to '"$TARGET_LOCALE"'. Be as **concise** as possible to save screen space, and make sure terminology is relevant (e.g. "discharging" refers to the battery status).'
|
||||
content=$(cat "${TRANSLATIONS_DIR}/en_US.json")
|
||||
prompt_json=$(jq -n --arg prompt_text "$instruction" --arg content "$content" '$prompt_text + "\n```\n" + $content + "\n```\n"')
|
||||
|
||||
# Prepare request data using jq
|
||||
payload=$(jq -n \
|
||||
--arg prompt "$prompt_json" \
|
||||
--arg temperature "0" \
|
||||
--arg model "$MODEL" \
|
||||
'{
|
||||
contents: [{
|
||||
parts: [
|
||||
{text: $prompt}
|
||||
]
|
||||
}],
|
||||
generationConfig: {
|
||||
temperature: ($temperature | tonumber),
|
||||
"responseMimeType": "application/json",
|
||||
}
|
||||
}'
|
||||
)
|
||||
# echo "$payload" | jq
|
||||
|
||||
# Get API key
|
||||
API_KEY=$(secret-tool lookup 'application' 'illogical-impulse' | jq -r '.apiKeys.gemini')
|
||||
|
||||
# Notify start
|
||||
notify-send "Translation started" "Will take 2 minutes, and you'll be notified when it's done, so feel free to do something else in the meantime." -a "$NOTIFICATION_APP_NAME"
|
||||
|
||||
# Make the request
|
||||
response=$(curl "https://generativelanguage.googleapis.com/v1beta/models/${MODEL}:generateContent" \
|
||||
-H "x-goog-api-key: $API_KEY" \
|
||||
-H 'Content-Type: application/json' \
|
||||
-X POST \
|
||||
-d "$payload" 2> /dev/null)
|
||||
# echo "$response" | jq
|
||||
|
||||
# Write the result
|
||||
echo "$response" | jq -r '.candidates[0].content.parts[0].text' > "${TRANSLATIONS_TARGET_DIR}/${TARGET_LOCALE}.json"
|
||||
jq --arg locale "$TARGET_LOCALE" '.language.ui = $locale' "$SHELL_CONFIG_FILE" > "${SHELL_CONFIG_FILE}.tmp" && mv "${SHELL_CONFIG_FILE}.tmp" "$SHELL_CONFIG_FILE"
|
||||
notify-send "Translation complete" "Enjoy! In case you wanna refine it, the file is in ${TRANSLATIONS_TARGET_DIR}/${TARGET_LOCALE}.json" -a "$NOTIFICATION_APP_NAME"
|
||||
16
surfaces/quickshell/ii-base/scripts/ai/show-installed-ollama-models.sh
Executable file
16
surfaces/quickshell/ii-base/scripts/ai/show-installed-ollama-models.sh
Executable file
|
|
@ -0,0 +1,16 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Get the list, skip the header, and extract the first column (model names)
|
||||
model_names=$(ollama list | tail -n +2 | awk '{print $1}')
|
||||
|
||||
# Build a JSON array
|
||||
json_array="["
|
||||
for name in $model_names; do
|
||||
json_array+="\"$name\","
|
||||
done
|
||||
|
||||
# Remove trailing comma and close the array
|
||||
json_array="${json_array%,}]"
|
||||
|
||||
# Output the JSON array
|
||||
echo "$json_array"
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
[general]
|
||||
mode = waves
|
||||
framerate = 60
|
||||
autosens = 1
|
||||
bars = 50
|
||||
|
||||
[output]
|
||||
method = raw
|
||||
raw_target = /dev/stdout
|
||||
data_format = ascii
|
||||
channels = mono
|
||||
mono_option = average
|
||||
|
||||
[smoothing]
|
||||
noise_reduction = 20
|
||||
|
||||
|
||||
110
surfaces/quickshell/ii-base/scripts/colors/applycolor.sh
Executable file
110
surfaces/quickshell/ii-base/scripts/colors/applycolor.sh
Executable file
|
|
@ -0,0 +1,110 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
QUICKSHELL_CONFIG_NAME="ii"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||||
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME"
|
||||
CACHE_DIR="$XDG_CACHE_HOME/quickshell"
|
||||
STATE_DIR="$XDG_STATE_HOME/quickshell"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
term_alpha=100 #Set this to < 100 make all your terminals transparent
|
||||
# sleep 0 # idk i wanted some delay or colors dont get applied properly
|
||||
if [ ! -d "$STATE_DIR"/user/generated ]; then
|
||||
mkdir -p "$STATE_DIR"/user/generated
|
||||
fi
|
||||
cd "$CONFIG_DIR" || exit
|
||||
|
||||
colornames=''
|
||||
colorstrings=''
|
||||
colorlist=()
|
||||
colorvalues=()
|
||||
|
||||
colornames=$(cat $STATE_DIR/user/generated/material_colors.scss | cut -d: -f1)
|
||||
colorstrings=$(cat $STATE_DIR/user/generated/material_colors.scss | cut -d: -f2 | cut -d ' ' -f2 | cut -d ";" -f1)
|
||||
IFS=$'\n'
|
||||
colorlist=($colornames) # Array of color names
|
||||
colorvalues=($colorstrings) # Array of color values
|
||||
|
||||
apply_kitty() {
|
||||
# Check if terminal escape sequence template exists
|
||||
if [ ! -f "$SCRIPT_DIR/terminal/kitty-theme.conf" ]; then
|
||||
echo "Template file not found for Kitty theme. Skipping that."
|
||||
return
|
||||
fi
|
||||
# Copy template
|
||||
mkdir -p "$STATE_DIR"/user/generated/terminal
|
||||
cp "$SCRIPT_DIR/terminal/kitty-theme.conf" "$STATE_DIR"/user/generated/terminal/kitty-theme.conf
|
||||
# Apply colors
|
||||
for i in "${!colorlist[@]}"; do
|
||||
sed -i "s/${colorlist[$i]} #/${colorvalues[$i]#\#}/g" "$STATE_DIR"/user/generated/terminal/kitty-theme.conf
|
||||
done
|
||||
|
||||
# Reload
|
||||
if ! pgrep -f kitty >/dev/null; then
|
||||
return
|
||||
fi
|
||||
kill -SIGUSR1 $(pidof kitty)
|
||||
}
|
||||
|
||||
apply_ghostty() {
|
||||
# Ghostty has no template/sed flow like kitty; a small python script reads
|
||||
# the generated material_colors.scss and writes a Ghostty theme file.
|
||||
if [ ! -f "$SCRIPT_DIR/generate_ghostty_theme.py" ]; then
|
||||
echo "Generator not found for Ghostty theme. Skipping that."
|
||||
return
|
||||
fi
|
||||
mkdir -p "$STATE_DIR"/user/generated/terminal
|
||||
python3 "$SCRIPT_DIR/generate_ghostty_theme.py" \
|
||||
--scss "$STATE_DIR/user/generated/material_colors.scss" \
|
||||
--out "$STATE_DIR/user/generated/terminal/ghostty-theme.conf"
|
||||
|
||||
# Reload running ghostty instances. Ghostty has no reload signal like kitty's
|
||||
# SIGUSR1; users bind reload_config (default ctrl+shift+,) to pick up changes.
|
||||
}
|
||||
|
||||
apply_anyterm() {
|
||||
# Check if terminal escape sequence template exists
|
||||
if [ ! -f "$SCRIPT_DIR/terminal/sequences.txt" ]; then
|
||||
echo "Template file not found for Terminal. Skipping that."
|
||||
return
|
||||
fi
|
||||
# Copy template
|
||||
mkdir -p "$STATE_DIR"/user/generated/terminal
|
||||
cp "$SCRIPT_DIR/terminal/sequences.txt" "$STATE_DIR"/user/generated/terminal/sequences.txt
|
||||
# Apply colors
|
||||
for i in "${!colorlist[@]}"; do
|
||||
sed -i "s/${colorlist[$i]} #/${colorvalues[$i]#\#}/g" "$STATE_DIR"/user/generated/terminal/sequences.txt
|
||||
done
|
||||
|
||||
sed -i "s/\$alpha/$term_alpha/g" "$STATE_DIR/user/generated/terminal/sequences.txt"
|
||||
|
||||
for file in /dev/pts/*; do
|
||||
if [[ $file =~ ^/dev/pts/[0-9]+$ ]]; then
|
||||
{
|
||||
cat "$STATE_DIR"/user/generated/terminal/sequences.txt >"$file"
|
||||
} & disown || true
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
apply_term() {
|
||||
apply_anyterm &
|
||||
apply_kitty &
|
||||
apply_ghostty &
|
||||
}
|
||||
|
||||
# Check if terminal theming is enabled in config
|
||||
CONFIG_FILE="$XDG_CONFIG_HOME/illogical-impulse/config.json"
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
enable_terminal=$(jq -r '.appearance.wallpaperTheming.enableTerminal' "$CONFIG_FILE")
|
||||
if [ "$enable_terminal" = "true" ]; then
|
||||
apply_term &
|
||||
fi
|
||||
else
|
||||
echo "Config file not found at $CONFIG_FILE. Applying terminal theming by default."
|
||||
apply_term &
|
||||
fi
|
||||
|
||||
# apply_qt & # Qt theming is already handled by kde-material-colors
|
||||
33
surfaces/quickshell/ii-base/scripts/colors/code/material-code-set-color.sh
Executable file
33
surfaces/quickshell/ii-base/scripts/colors/code/material-code-set-color.sh
Executable file
|
|
@ -0,0 +1,33 @@
|
|||
#!/usr/bin/env bash
|
||||
COLOR_FILE_PATH="${XDG_STATE_HOME:-$HOME/.local/state}/quickshell/user/generated/color.txt"
|
||||
|
||||
# Define an array of possible VSCode settings file paths for various forks
|
||||
settings_paths=(
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Code/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/VSCodium/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Code - OSS/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Code - Insiders/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Cursor/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Antigravity/User/settings.json"
|
||||
"${XDG_CONFIG_HOME:-$HOME/.config}/Windsurf/User/settings.json"
|
||||
|
||||
# Add more paths as needed for other forks
|
||||
)
|
||||
|
||||
new_color=$(cat "$COLOR_FILE_PATH")
|
||||
|
||||
# Loop through each settings file path
|
||||
for CODE_SETTINGS_PATH in "${settings_paths[@]}"; do
|
||||
if [[ -f "$CODE_SETTINGS_PATH" ]]; then
|
||||
# Try to update the key if it exists
|
||||
if grep -q '"material-code.primaryColor"' "$CODE_SETTINGS_PATH"; then
|
||||
sed -i -E \
|
||||
"s/(\"material-code.primaryColor\"\s*:\s*\")[^\"]*(\")/\1${new_color}\2/" \
|
||||
"$CODE_SETTINGS_PATH"
|
||||
else # If the key is not already there, add it
|
||||
sed -i '$ s/}/,\n "material-code.primaryColor": "'${new_color}'"\n}/' "$CODE_SETTINGS_PATH"
|
||||
sed -i '$ s/,\n,/,/' "$CODE_SETTINGS_PATH"
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
182
surfaces/quickshell/ii-base/scripts/colors/generate_colors_material.py
Executable file
182
surfaces/quickshell/ii-base/scripts/colors/generate_colors_material.py
Executable file
|
|
@ -0,0 +1,182 @@
|
|||
#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$HOME/.local/state/quickshell/.venv/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
|
||||
import argparse
|
||||
import math
|
||||
import json
|
||||
from PIL import Image
|
||||
from materialyoucolor.quantize import QuantizeCelebi
|
||||
from materialyoucolor.score.score import Score
|
||||
from materialyoucolor.hct import Hct
|
||||
from materialyoucolor.dynamiccolor.material_dynamic_colors import MaterialDynamicColors
|
||||
from materialyoucolor.utils.color_utils import (rgba_from_argb, argb_from_rgb, argb_from_rgba)
|
||||
from materialyoucolor.utils.math_utils import (sanitize_degrees_double, difference_degrees, rotation_direction)
|
||||
|
||||
parser = argparse.ArgumentParser(description='Color generation script')
|
||||
parser.add_argument('--path', type=str, default=None, help='generate colorscheme from image')
|
||||
parser.add_argument('--size', type=int , default=128 , help='bitmap image size')
|
||||
parser.add_argument('--color', type=str, default=None, help='generate colorscheme from color')
|
||||
parser.add_argument('--mode', type=str, choices=['dark', 'light'], default='dark', help='dark or light mode')
|
||||
parser.add_argument('--scheme', type=str, default='vibrant', help='material scheme to use')
|
||||
parser.add_argument('--smart', action='store_true', default=False, help='decide scheme type based on image color')
|
||||
parser.add_argument('--transparency', type=str, choices=['opaque', 'transparent'], default='opaque', help='enable transparency')
|
||||
parser.add_argument('--termscheme', type=str, default=None, help='JSON file containg the terminal scheme for generating term colors')
|
||||
parser.add_argument('--harmony', type=float , default=0.8, help='(0-1) Color hue shift towards accent')
|
||||
parser.add_argument('--harmonize_threshold', type=float , default=100, help='(0-180) Max threshold angle to limit color hue shift')
|
||||
parser.add_argument('--term_fg_boost', type=float , default=0.35, help='Make terminal foreground more different from the background')
|
||||
parser.add_argument('--term_bg_tone', type=float , default=0.4, help='(0-1) Terminal background darkness/tone (lower = darker)')
|
||||
parser.add_argument('--blend_bg_fg', action='store_true', default=False, help='Shift terminal background or foreground towards accent')
|
||||
parser.add_argument('--cache', type=str, default=None, help='file path to store the generated color')
|
||||
parser.add_argument('--debug', action='store_true', default=False, help='debug mode')
|
||||
args = parser.parse_args()
|
||||
|
||||
rgba_to_hex = lambda rgba: "#{:02X}{:02X}{:02X}".format(rgba[0], rgba[1], rgba[2])
|
||||
argb_to_hex = lambda argb: "#{:02X}{:02X}{:02X}".format(*map(round, rgba_from_argb(argb)))
|
||||
hex_to_argb = lambda hex_code: argb_from_rgb(int(hex_code[1:3], 16), int(hex_code[3:5], 16), int(hex_code[5:], 16))
|
||||
display_color = lambda rgba : "\x1B[38;2;{};{};{}m{}\x1B[0m".format(rgba[0], rgba[1], rgba[2], "\x1b[7m \x1b[7m")
|
||||
|
||||
def calculate_optimal_size (width: int, height: int, bitmap_size: int) -> (int, int):
|
||||
image_area = width * height;
|
||||
bitmap_area = bitmap_size ** 2
|
||||
scale = math.sqrt(bitmap_area/image_area) if image_area > bitmap_area else 1
|
||||
new_width = round(width * scale)
|
||||
new_height = round(height * scale)
|
||||
if new_width == 0:
|
||||
new_width = 1
|
||||
if new_height == 0:
|
||||
new_height = 1
|
||||
return new_width, new_height
|
||||
|
||||
def harmonize (design_color: int, source_color: int, threshold: float = 35, harmony: float = 0.5) -> int:
|
||||
from_hct = Hct.from_int(design_color)
|
||||
to_hct = Hct.from_int(source_color)
|
||||
difference_degrees_ = difference_degrees(from_hct.hue, to_hct.hue)
|
||||
rotation_degrees = min(difference_degrees_ * harmony, threshold)
|
||||
output_hue = sanitize_degrees_double(
|
||||
from_hct.hue + rotation_degrees * rotation_direction(from_hct.hue, to_hct.hue)
|
||||
)
|
||||
return Hct.from_hct(output_hue, from_hct.chroma, from_hct.tone).to_int()
|
||||
|
||||
def boost_chroma_tone (argb: int, chroma: float = 1, tone: float = 1) -> int:
|
||||
hct = Hct.from_int(argb)
|
||||
return Hct.from_hct(hct.hue, hct.chroma * chroma, hct.tone * tone).to_int()
|
||||
|
||||
darkmode = (args.mode == 'dark')
|
||||
transparent = (args.transparency == 'transparent')
|
||||
|
||||
if args.path is not None:
|
||||
image = Image.open(args.path)
|
||||
|
||||
if image.format == "GIF":
|
||||
image.seek(1)
|
||||
|
||||
if image.mode in ["L", "P"]:
|
||||
image = image.convert('RGB')
|
||||
wsize, hsize = image.size
|
||||
wsize_new, hsize_new = calculate_optimal_size(wsize, hsize, args.size)
|
||||
if wsize_new < wsize or hsize_new < hsize:
|
||||
image = image.resize((wsize_new, hsize_new), Image.Resampling.BICUBIC)
|
||||
colors = QuantizeCelebi(list(image.getdata()), 128)
|
||||
argb = Score.score(colors)[0]
|
||||
|
||||
if args.cache is not None:
|
||||
with open(args.cache, 'w') as file:
|
||||
file.write(argb_to_hex(argb))
|
||||
hct = Hct.from_int(argb)
|
||||
if(args.smart):
|
||||
if(hct.chroma < 20):
|
||||
args.scheme = 'neutral'
|
||||
elif args.color is not None:
|
||||
argb = hex_to_argb(args.color)
|
||||
hct = Hct.from_int(argb)
|
||||
|
||||
if args.scheme == 'scheme-fruit-salad':
|
||||
from materialyoucolor.scheme.scheme_fruit_salad import SchemeFruitSalad as Scheme
|
||||
elif args.scheme == 'scheme-expressive':
|
||||
from materialyoucolor.scheme.scheme_expressive import SchemeExpressive as Scheme
|
||||
elif args.scheme == 'scheme-monochrome':
|
||||
from materialyoucolor.scheme.scheme_monochrome import SchemeMonochrome as Scheme
|
||||
elif args.scheme == 'scheme-rainbow':
|
||||
from materialyoucolor.scheme.scheme_rainbow import SchemeRainbow as Scheme
|
||||
elif args.scheme == 'scheme-tonal-spot':
|
||||
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
|
||||
elif args.scheme == 'scheme-neutral':
|
||||
from materialyoucolor.scheme.scheme_neutral import SchemeNeutral as Scheme
|
||||
elif args.scheme == 'scheme-fidelity':
|
||||
from materialyoucolor.scheme.scheme_fidelity import SchemeFidelity as Scheme
|
||||
elif args.scheme == 'scheme-content':
|
||||
from materialyoucolor.scheme.scheme_content import SchemeContent as Scheme
|
||||
elif args.scheme == 'scheme-vibrant':
|
||||
from materialyoucolor.scheme.scheme_vibrant import SchemeVibrant as Scheme
|
||||
else:
|
||||
from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot as Scheme
|
||||
# Generate
|
||||
scheme = Scheme(hct, darkmode, 0.0)
|
||||
|
||||
material_colors = {}
|
||||
term_colors = {}
|
||||
|
||||
for color in vars(MaterialDynamicColors).keys():
|
||||
color_name = getattr(MaterialDynamicColors, color)
|
||||
if hasattr(color_name, "get_hct"):
|
||||
rgba = color_name.get_hct(scheme).to_rgba()
|
||||
material_colors[color] = rgba_to_hex(rgba)
|
||||
|
||||
# Extended material
|
||||
if darkmode == True:
|
||||
material_colors['success'] = '#B5CCBA'
|
||||
material_colors['onSuccess'] = '#213528'
|
||||
material_colors['successContainer'] = '#374B3E'
|
||||
material_colors['onSuccessContainer'] = '#D1E9D6'
|
||||
else:
|
||||
material_colors['success'] = '#4F6354'
|
||||
material_colors['onSuccess'] = '#FFFFFF'
|
||||
material_colors['successContainer'] = '#D1E8D5'
|
||||
material_colors['onSuccessContainer'] = '#0C1F13'
|
||||
|
||||
# Terminal Colors
|
||||
if args.termscheme is not None:
|
||||
with open(args.termscheme, 'r') as f:
|
||||
json_termscheme = f.read()
|
||||
term_source_colors = json.loads(json_termscheme)['dark' if darkmode else 'light']
|
||||
|
||||
primary_color_argb = hex_to_argb(material_colors['primary_paletteKeyColor'])
|
||||
for color, val in term_source_colors.items():
|
||||
if(args.scheme == 'monochrome') :
|
||||
term_colors[color] = val
|
||||
continue
|
||||
if args.blend_bg_fg and color == "term0":
|
||||
harmonized = boost_chroma_tone(hex_to_argb(material_colors['surfaceContainerLow']), 1.2, args.term_bg_tone)
|
||||
elif args.blend_bg_fg and color == "term15":
|
||||
harmonized = boost_chroma_tone(hex_to_argb(material_colors['onSurface']), 3, 1)
|
||||
else:
|
||||
harmonized = harmonize(hex_to_argb(val), primary_color_argb, args.harmonize_threshold, args.harmony)
|
||||
harmonized = boost_chroma_tone(harmonized, 1, 1 + (args.term_fg_boost * (1 if darkmode else -1)))
|
||||
term_colors[color] = argb_to_hex(harmonized)
|
||||
|
||||
if args.debug == False:
|
||||
print(f"$darkmode: {darkmode};")
|
||||
print(f"$transparent: {transparent};")
|
||||
for color, code in material_colors.items():
|
||||
print(f"${color}: {code};")
|
||||
for color, code in term_colors.items():
|
||||
print(f"${color}: {code};")
|
||||
else:
|
||||
if args.path is not None:
|
||||
print('\n--------------Image properties-----------------')
|
||||
print(f"Image size: {wsize} x {hsize}")
|
||||
print(f"Resized image: {wsize_new} x {hsize_new}")
|
||||
print('\n---------------Selected color------------------')
|
||||
print(f"Dark mode: {darkmode}")
|
||||
print(f"Scheme: {args.scheme}")
|
||||
print(f"Accent color: {display_color(rgba_from_argb(argb))} {argb_to_hex(argb)}")
|
||||
print(f"HCT: {hct.hue:.2f} {hct.chroma:.2f} {hct.tone:.2f}")
|
||||
print('\n---------------Material colors-----------------')
|
||||
for color, code in material_colors.items():
|
||||
rgba = rgba_from_argb(hex_to_argb(code))
|
||||
print(f"{color.ljust(32)} : {display_color(rgba)} {code}")
|
||||
print('\n----------Harmonize terminal colors------------')
|
||||
for color, code in term_colors.items():
|
||||
rgba = rgba_from_argb(hex_to_argb(code))
|
||||
code_source = term_source_colors[color]
|
||||
rgba_source = rgba_from_argb(hex_to_argb(code_source))
|
||||
print(f"{color.ljust(6)} : {display_color(rgba_source)} {code_source} --> {display_color(rgba)} {code}")
|
||||
print('-----------------------------------------------')
|
||||
90
surfaces/quickshell/ii-base/scripts/colors/generate_ghostty_theme.py
Executable file
90
surfaces/quickshell/ii-base/scripts/colors/generate_ghostty_theme.py
Executable file
|
|
@ -0,0 +1,90 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Generate a Ghostty terminal theme from the Material You colors.
|
||||
|
||||
Counterpart to applycolor.sh's apply_kitty(): same color mapping as
|
||||
terminal/kitty-theme.conf, emitted in Ghostty's `palette = N=#hex` format.
|
||||
|
||||
Source is the generated `material_colors.scss`, which holds lines like:
|
||||
|
||||
$term0: #1A1110;
|
||||
$onSecondaryContainer: #FFDDB0;
|
||||
|
||||
Mapping (kept identical to terminal/kitty-theme.conf):
|
||||
|
||||
palette 0..15 -> term0..term15
|
||||
background -> term0
|
||||
foreground -> term7
|
||||
cursor-color -> term7
|
||||
cursor-text -> term0 (Kitty leaves unset; use bg)
|
||||
selection-background -> onSecondaryContainer
|
||||
selection-foreground -> secondaryContainer
|
||||
window-titlebar-* -> term0 / term7 (needs window-theme = ghostty)
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
|
||||
XDG_STATE_HOME = os.environ.get("XDG_STATE_HOME", os.path.expanduser("~/.local/state"))
|
||||
DEFAULT_SCSS = os.path.join(XDG_STATE_HOME, "quickshell/user/generated/material_colors.scss")
|
||||
DEFAULT_OUT = os.path.join(XDG_STATE_HOME, "quickshell/user/generated/terminal/ghostty-theme.conf")
|
||||
|
||||
SCSS_LINE = re.compile(r"^\s*\$([A-Za-z0-9_]+)\s*:\s*(#[0-9A-Fa-f]{3,8})\s*;")
|
||||
|
||||
|
||||
def parse_scss(path: str) -> dict:
|
||||
"""Read `$name: #hex;` lines into a {name: '#hex'} dict."""
|
||||
colors = {}
|
||||
with open(path, "r") as f:
|
||||
for line in f:
|
||||
m = SCSS_LINE.match(line)
|
||||
if m:
|
||||
colors[m.group(1)] = m.group(2)
|
||||
return colors
|
||||
|
||||
|
||||
def build_theme(c: dict) -> str:
|
||||
def col(name: str) -> str:
|
||||
if name not in c:
|
||||
sys.exit(f"error: color '{name}' missing from scss source")
|
||||
return c[name]
|
||||
|
||||
lines = []
|
||||
for i in range(16):
|
||||
lines.append(f"palette = {i}=#{col(f'term{i}')[1:]}")
|
||||
lines.append(f"background = {col('term0')}")
|
||||
lines.append(f"foreground = {col('term7')}")
|
||||
lines.append(f"cursor-color = {col('term7')}")
|
||||
lines.append(f"cursor-text = {col('term0')}")
|
||||
lines.append(f"selection-background = {col('onSecondaryContainer')}")
|
||||
lines.append(f"selection-foreground = {col('secondaryContainer')}")
|
||||
# GTK headerbar/titlebar. Only honored when the main ghostty config sets
|
||||
# `window-theme = ghostty` (Linux/Adwaita only). Match terminal bg/fg.
|
||||
lines.append(f"window-titlebar-background = {col('term0')}")
|
||||
lines.append(f"window-titlebar-foreground = {col('term7')}")
|
||||
return "\n".join(lines) + "\n"
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Generate Ghostty theme from Material colors")
|
||||
parser.add_argument("--scss", default=DEFAULT_SCSS, help="path to material_colors.scss")
|
||||
parser.add_argument("--out", default=DEFAULT_OUT, help="output theme file path")
|
||||
parser.add_argument("--print", dest="to_stdout", action="store_true", help="print to stdout instead of writing file")
|
||||
args = parser.parse_args()
|
||||
|
||||
if not os.path.isfile(args.scss):
|
||||
sys.exit(f"error: scss source not found: {args.scss}")
|
||||
|
||||
theme = build_theme(parse_scss(args.scss))
|
||||
|
||||
if args.to_stdout:
|
||||
sys.stdout.write(theme)
|
||||
else:
|
||||
os.makedirs(os.path.dirname(args.out), exist_ok=True)
|
||||
with open(args.out, "w") as f:
|
||||
f.write(theme)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
43
surfaces/quickshell/ii-base/scripts/colors/random/random_konachan_wall.sh
Executable file
43
surfaces/quickshell/ii-base/scripts/colors/random/random_konachan_wall.sh
Executable file
|
|
@ -0,0 +1,43 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_pictures_dir() {
|
||||
if command -v xdg-user-dir &> /dev/null; then
|
||||
xdg-user-dir PICTURES
|
||||
return
|
||||
fi
|
||||
|
||||
local config_file="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
|
||||
if [ -f "$config_file" ]; then
|
||||
local pictures_path
|
||||
pictures_path=$(source "$config_file" >/dev/null 2>&1; echo "$XDG_PICTURES_DIR")
|
||||
echo "${pictures_path/#\$HOME/$HOME}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$HOME/Pictures"
|
||||
}
|
||||
|
||||
QUICKSHELL_CONFIG_NAME="ii"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||||
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
PICTURES_DIR=$(get_pictures_dir)
|
||||
CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME"
|
||||
CACHE_DIR="$XDG_CACHE_HOME/quickshell"
|
||||
STATE_DIR="$XDG_STATE_HOME/quickshell"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
mkdir -p "$PICTURES_DIR/Wallpapers"
|
||||
page=$((1 + RANDOM % 1000));
|
||||
illogicalImpulseConfigPath="$HOME/.config/illogical-impulse/config.json"
|
||||
userAgent=$(jq -r '.networking.userAgent // empty' "$illogicalImpulseConfigPath" 2>/dev/null)
|
||||
response=$(curl -A "$userAgent" "https://konachan.net/post.json?tags=rating%3Asafe&limit=1&page=$page")
|
||||
link=$(echo "$response" | jq '.[0].file_url' -r);
|
||||
ext=$(echo "$link" | awk -F. '{print $NF}')
|
||||
downloadPath="$PICTURES_DIR/Wallpapers/random_wallpaper.$ext"
|
||||
currentWallpaperPath=$(jq -r '.background.wallpaperPath' "$illogicalImpulseConfigPath")
|
||||
if [ "$downloadPath" == "$currentWallpaperPath" ]; then
|
||||
downloadPath="$PICTURES_DIR/Wallpapers/random_wallpaper-1.$ext"
|
||||
fi
|
||||
curl -A "$userAgent" "$link" -o "$downloadPath"
|
||||
"$SCRIPT_DIR/../switchwall.sh" --image "$downloadPath"
|
||||
44
surfaces/quickshell/ii-base/scripts/colors/random/random_osu_wall.sh
Executable file
44
surfaces/quickshell/ii-base/scripts/colors/random/random_osu_wall.sh
Executable file
|
|
@ -0,0 +1,44 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
get_pictures_dir() {
|
||||
if command -v xdg-user-dir &> /dev/null; then
|
||||
xdg-user-dir PICTURES
|
||||
return
|
||||
fi
|
||||
|
||||
local config_file="${XDG_CONFIG_HOME:-$HOME/.config}/user-dirs.dirs"
|
||||
if [ -f "$config_file" ]; then
|
||||
local pictures_path
|
||||
pictures_path=$(source "$config_file" >/dev/null 2>&1; echo "$XDG_PICTURES_DIR")
|
||||
echo "${pictures_path/#\$HOME/$HOME}"
|
||||
return
|
||||
fi
|
||||
|
||||
echo "$HOME/Pictures"
|
||||
}
|
||||
|
||||
QUICKSHELL_CONFIG_NAME="ii"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||||
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
PICTURES_DIR=$(get_pictures_dir)
|
||||
CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME"
|
||||
CACHE_DIR="$XDG_CACHE_HOME/quickshell"
|
||||
STATE_DIR="$XDG_STATE_HOME/quickshell"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
mkdir -p "$PICTURES_DIR/Wallpapers"
|
||||
|
||||
response=$(curl "https://osu.ppy.sh/api/v2/seasonal-backgrounds")
|
||||
images=$(echo "$response" | jq '.backgrounds | length' -r);
|
||||
randomIndex=$((RANDOM % images));
|
||||
link=$(echo "$response" | jq ".backgrounds[$randomIndex].url" -r)
|
||||
ext=$(echo "$link" | awk -F. '{print $NF}')
|
||||
downloadPath="$PICTURES_DIR/Wallpapers/random_wallpaper.$ext"
|
||||
illogicalImpulseConfigPath="$HOME/.config/illogical-impulse/config.json"
|
||||
currentWallpaperPath=$(jq -r '.background.wallpaperPath' $illogicalImpulseConfigPath)
|
||||
if [ "$downloadPath" == "$currentWallpaperPath" ]; then
|
||||
downloadPath="$PICTURES_DIR/Wallpapers/random_wallpaper-1.$ext"
|
||||
fi
|
||||
curl "$link" -o "$downloadPath"
|
||||
"$SCRIPT_DIR/../switchwall.sh" --image "$downloadPath"
|
||||
70
surfaces/quickshell/ii-base/scripts/colors/scheme_for_image.py
Executable file
70
surfaces/quickshell/ii-base/scripts/colors/scheme_for_image.py
Executable file
|
|
@ -0,0 +1,70 @@
|
|||
#!/usr/bin/env python3
|
||||
import sys
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
# Allowed scheme types
|
||||
SCHEMES = [
|
||||
"scheme-content",
|
||||
"scheme-expressive",
|
||||
"scheme-fidelity",
|
||||
"scheme-fruit-salad",
|
||||
"scheme-monochrome",
|
||||
"scheme-neutral",
|
||||
"scheme-rainbow",
|
||||
"scheme-tonal-spot"
|
||||
]
|
||||
|
||||
def image_colorfulness(image):
|
||||
# Based on Hasler and Süsstrunk's colorfulness metric
|
||||
(B, G, R) = cv2.split(image.astype("float"))
|
||||
rg = np.absolute(R - G)
|
||||
yb = np.absolute(0.5 * (R + G) - B)
|
||||
std_rg = np.std(rg)
|
||||
std_yb = np.std(yb)
|
||||
mean_rg = np.mean(rg)
|
||||
mean_yb = np.mean(yb)
|
||||
colorfulness = np.sqrt(std_rg ** 2 + std_yb ** 2) + (0.3 * np.sqrt(mean_rg ** 2 + mean_yb ** 2))
|
||||
return colorfulness
|
||||
|
||||
# scheme-content respects the image's colors very well, but it might
|
||||
# look too saturated, so we only use it for not very colorful images to be safe
|
||||
def pick_scheme(colorfulness):
|
||||
if colorfulness < 40:
|
||||
return "scheme-neutral"
|
||||
else:
|
||||
return "scheme-tonal-spot"
|
||||
|
||||
def load_and_resize_image(img_path, max_dim=128):
|
||||
img = cv2.imread(img_path)
|
||||
if img is None:
|
||||
return None
|
||||
h, w = img.shape[:2]
|
||||
if max(h, w) > max_dim:
|
||||
scale = max_dim / max(h, w)
|
||||
img = cv2.resize(img, (int(w * scale), int(h * scale)), interpolation=cv2.INTER_AREA)
|
||||
return img
|
||||
|
||||
def main():
|
||||
colorfulness_mode = False
|
||||
args = sys.argv[1:]
|
||||
if '--colorfulness' in args:
|
||||
colorfulness_mode = True
|
||||
args.remove('--colorfulness')
|
||||
if len(args) < 1:
|
||||
print("scheme-tonal-spot")
|
||||
sys.exit(1)
|
||||
img_path = args[0]
|
||||
img = load_and_resize_image(img_path)
|
||||
if img is None:
|
||||
print("scheme-tonal-spot")
|
||||
sys.exit(1)
|
||||
colorfulness = image_colorfulness(img)
|
||||
if colorfulness_mode:
|
||||
print(f"{colorfulness}")
|
||||
else:
|
||||
scheme = pick_scheme(colorfulness)
|
||||
print(scheme)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
476
surfaces/quickshell/ii-base/scripts/colors/switchwall.sh
Executable file
476
surfaces/quickshell/ii-base/scripts/colors/switchwall.sh
Executable file
|
|
@ -0,0 +1,476 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
QUICKSHELL_CONFIG_NAME="ii"
|
||||
XDG_CONFIG_HOME="${XDG_CONFIG_HOME:-$HOME/.config}"
|
||||
XDG_CACHE_HOME="${XDG_CACHE_HOME:-$HOME/.cache}"
|
||||
XDG_STATE_HOME="${XDG_STATE_HOME:-$HOME/.local/state}"
|
||||
CONFIG_DIR="$XDG_CONFIG_HOME/quickshell/$QUICKSHELL_CONFIG_NAME"
|
||||
CACHE_DIR="$XDG_CACHE_HOME/quickshell"
|
||||
STATE_DIR="$XDG_STATE_HOME/quickshell"
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
SHELL_CONFIG_FILE="$XDG_CONFIG_HOME/illogical-impulse/config.json"
|
||||
MATUGEN_DIR="$XDG_CONFIG_HOME/matugen"
|
||||
terminalscheme="$SCRIPT_DIR/terminal/scheme-base.json"
|
||||
|
||||
handle_kde_material_you_colors() {
|
||||
# Check if Qt app theming is enabled in config
|
||||
if [ -f "$SHELL_CONFIG_FILE" ]; then
|
||||
enable_qt_apps=$(jq -r '.appearance.wallpaperTheming.enableQtApps' "$SHELL_CONFIG_FILE")
|
||||
if [ "$enable_qt_apps" == "false" ]; then
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Map $type_flag to allowed scheme variants for kde-material-you-colors-wrapper.sh
|
||||
local kde_scheme_variant=""
|
||||
case "$type_flag" in
|
||||
scheme-content|scheme-expressive|scheme-fidelity|scheme-fruit-salad|scheme-monochrome|scheme-neutral|scheme-rainbow|scheme-tonal-spot)
|
||||
kde_scheme_variant="$type_flag"
|
||||
;;
|
||||
*)
|
||||
kde_scheme_variant="scheme-tonal-spot" # default
|
||||
;;
|
||||
esac
|
||||
"$XDG_CONFIG_HOME"/matugen/templates/kde/kde-material-you-colors-wrapper.sh --scheme-variant "$kde_scheme_variant"
|
||||
}
|
||||
|
||||
pre_process() {
|
||||
local mode_flag="$1"
|
||||
# Set GNOME color-scheme if mode_flag is dark or light
|
||||
if [[ "$mode_flag" == "dark" ]]; then
|
||||
gsettings set org.gnome.desktop.interface color-scheme 'prefer-dark'
|
||||
gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3-dark'
|
||||
elif [[ "$mode_flag" == "light" ]]; then
|
||||
gsettings set org.gnome.desktop.interface color-scheme 'prefer-light'
|
||||
gsettings set org.gnome.desktop.interface gtk-theme 'adw-gtk3'
|
||||
fi
|
||||
|
||||
if [ ! -d "$CACHE_DIR"/user/generated ]; then
|
||||
mkdir -p "$CACHE_DIR"/user/generated
|
||||
fi
|
||||
}
|
||||
|
||||
post_process() {
|
||||
local screen_width="$1"
|
||||
local screen_height="$2"
|
||||
local wallpaper_path="$3"
|
||||
|
||||
handle_kde_material_you_colors &
|
||||
"$SCRIPT_DIR/code/material-code-set-color.sh" &
|
||||
}
|
||||
|
||||
check_and_prompt_upscale() {
|
||||
local img="$1"
|
||||
min_width_desired="$(hyprctl monitors -j | jq '([.[].width] | max)' | xargs)" # max monitor width
|
||||
min_height_desired="$(hyprctl monitors -j | jq '([.[].height] | max)' | xargs)" # max monitor height
|
||||
|
||||
if command -v identify &>/dev/null && [ -f "$img" ]; then
|
||||
local img_width img_height
|
||||
if is_video "$img"; then # Not check resolution for videos, just let em pass
|
||||
img_width=$min_width_desired
|
||||
img_height=$min_height_desired
|
||||
else
|
||||
img_width=$(identify -format "%w" "$img" 2>/dev/null)
|
||||
img_height=$(identify -format "%h" "$img" 2>/dev/null)
|
||||
fi
|
||||
if [[ "$img_width" -lt "$min_width_desired" || "$img_height" -lt "$min_height_desired" ]]; then
|
||||
action=$(notify-send "Upscale?" \
|
||||
"Image resolution (${img_width}x${img_height}) is lower than screen resolution (${min_width_desired}x${min_height_desired})" \
|
||||
-A "open_upscayl=Open Upscayl"\
|
||||
-a "Wallpaper switcher")
|
||||
if [[ "$action" == "open_upscayl" ]]; then
|
||||
if command -v upscayl &>/dev/null; then
|
||||
nohup upscayl > /dev/null 2>&1 &
|
||||
else
|
||||
action2=$(notify-send \
|
||||
-a "Wallpaper switcher" \
|
||||
-c "im.error" \
|
||||
-A "install_upscayl=Install Upscayl (Arch)" \
|
||||
"Install Upscayl?" \
|
||||
"yay -S upscayl-bin")
|
||||
if [[ "$action2" == "install_upscayl" ]]; then
|
||||
kitty -1 yay -S upscayl-bin
|
||||
if command -v upscayl &>/dev/null; then
|
||||
nohup upscayl > /dev/null 2>&1 &
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
}
|
||||
|
||||
CUSTOM_DIR="$XDG_CONFIG_HOME/hypr/custom"
|
||||
RESTORE_SCRIPT_DIR="$CUSTOM_DIR/scripts"
|
||||
RESTORE_SCRIPT="$RESTORE_SCRIPT_DIR/__restore_video_wallpaper.sh"
|
||||
THUMBNAIL_DIR="$RESTORE_SCRIPT_DIR/mpvpaper_thumbnails"
|
||||
VIDEO_OPTS="no-audio loop hwdec=auto scale=bilinear interpolation=no video-sync=display-resample panscan=1.0 video-scale-x=1.0 video-scale-y=1.0 video-align-x=0.5 video-align-y=0.5 load-scripts=no"
|
||||
|
||||
is_video() {
|
||||
local extension="${1##*.}"
|
||||
[[ "$extension" == "mp4" || "$extension" == "webm" || "$extension" == "mkv" || "$extension" == "avi" || "$extension" == "mov" ]] && return 0 || return 1
|
||||
}
|
||||
|
||||
kill_existing_mpvpaper() {
|
||||
pkill -f -9 mpvpaper || true
|
||||
}
|
||||
|
||||
create_restore_script() {
|
||||
local video_path=$1
|
||||
cat > "$RESTORE_SCRIPT.tmp" << EOF
|
||||
#!/bin/bash
|
||||
# Generated by switchwall.sh - Don't modify it by yourself.
|
||||
# Time: $(date)
|
||||
|
||||
pkill -f -9 mpvpaper
|
||||
|
||||
for monitor in \$(hyprctl monitors -j | jq -r '.[] | .name'); do
|
||||
nohup mpvpaper -o "$VIDEO_OPTS" "\$monitor" "$video_path" >/dev/null 2>&1 &
|
||||
sleep 0.1
|
||||
done
|
||||
EOF
|
||||
mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
|
||||
chmod +x "$RESTORE_SCRIPT"
|
||||
}
|
||||
|
||||
remove_restore() {
|
||||
cat > "$RESTORE_SCRIPT.tmp" << EOF
|
||||
#!/bin/bash
|
||||
# The content of this script will be generated by switchwall.sh - Don't modify it by yourself.
|
||||
EOF
|
||||
mv "$RESTORE_SCRIPT.tmp" "$RESTORE_SCRIPT"
|
||||
}
|
||||
|
||||
set_wallpaper_path() {
|
||||
local path="$1"
|
||||
if [ -f "$SHELL_CONFIG_FILE" ]; then
|
||||
jq --arg path "$path" '.background.wallpaperPath = $path' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
set_thumbnail_path() {
|
||||
local path="$1"
|
||||
if [ -f "$SHELL_CONFIG_FILE" ]; then
|
||||
jq --arg path "$path" '.background.thumbnailPath = $path' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE"
|
||||
fi
|
||||
}
|
||||
|
||||
categorize_wallpaper() {
|
||||
img_cat=$("$SCRIPT_DIR/../ai/gemini-categorize-wallpaper.sh" "$1")
|
||||
# notify-send "Wallpaper category" "$img_cat"
|
||||
echo "$img_cat" > "$STATE_DIR/user/generated/wallpaper/category.txt"
|
||||
}
|
||||
|
||||
switch() {
|
||||
imgpath="$1"
|
||||
mode_flag="$2"
|
||||
type_flag="$3"
|
||||
color_flag="$4"
|
||||
color="$5"
|
||||
|
||||
# Start Gemini auto-categorization if enabled
|
||||
aiStylingEnabled=$(jq -r '.background.widgets.clock.cookie.aiStyling' "$SHELL_CONFIG_FILE")
|
||||
if [[ "$aiStylingEnabled" == "true" ]]; then
|
||||
categorize_wallpaper "$imgpath" &
|
||||
fi
|
||||
|
||||
read scale screenx screeny screensizey < <(hyprctl monitors -j | jq '.[] | select(.focused) | .scale, .x, .y, .height' | xargs)
|
||||
cursorposx=$(hyprctl cursorpos -j | jq '.x' 2>/dev/null) || cursorposx=960
|
||||
cursorposx=$(bc <<< "scale=0; ($cursorposx - $screenx) * $scale / 1")
|
||||
cursorposy=$(hyprctl cursorpos -j | jq '.y' 2>/dev/null) || cursorposy=540
|
||||
cursorposy=$(bc <<< "scale=0; ($cursorposy - $screeny) * $scale / 1")
|
||||
cursorposy_inverted=$((screensizey - cursorposy))
|
||||
|
||||
matugen_args=(--source-color-index 0)
|
||||
|
||||
if [[ "$color_flag" == "1" ]]; then
|
||||
matugen_args+=(color hex "$color")
|
||||
generate_colors_material_args=(--color "$color")
|
||||
else
|
||||
if [[ -z "$imgpath" ]]; then
|
||||
echo 'Aborted'
|
||||
exit 0
|
||||
fi
|
||||
|
||||
check_and_prompt_upscale "$imgpath" &
|
||||
kill_existing_mpvpaper
|
||||
|
||||
if is_video "$imgpath"; then
|
||||
mkdir -p "$THUMBNAIL_DIR"
|
||||
|
||||
missing_deps=()
|
||||
if ! command -v mpvpaper &> /dev/null; then
|
||||
missing_deps+=("mpvpaper")
|
||||
fi
|
||||
if ! command -v ffmpeg &> /dev/null; then
|
||||
missing_deps+=("ffmpeg")
|
||||
fi
|
||||
if [ ${#missing_deps[@]} -gt 0 ]; then
|
||||
echo "Missing deps: ${missing_deps[*]}"
|
||||
echo "Arch: sudo pacman -S ${missing_deps[*]}"
|
||||
action=$(notify-send \
|
||||
-a "Wallpaper switcher" \
|
||||
-c "im.error" \
|
||||
-A "install_arch=Install (Arch)" \
|
||||
"Can't switch to video wallpaper" \
|
||||
"Missing dependencies: ${missing_deps[*]}")
|
||||
if [[ "$action" == "install_arch" ]]; then
|
||||
kitty -1 sudo pacman -S "${missing_deps[*]}"
|
||||
if command -v mpvpaper &>/dev/null && command -v ffmpeg &>/dev/null; then
|
||||
notify-send 'Wallpaper switcher' 'Alright, try again!' -a "Wallpaper switcher"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Set wallpaper path
|
||||
set_wallpaper_path "$imgpath"
|
||||
|
||||
# Set video wallpaper
|
||||
local video_path="$imgpath"
|
||||
monitors=$(hyprctl monitors -j | jq -r '.[] | .name')
|
||||
for monitor in $monitors; do
|
||||
nohup mpvpaper -o "$VIDEO_OPTS" "$monitor" "$video_path" >/dev/null 2>&1 &
|
||||
sleep 0.1
|
||||
done
|
||||
|
||||
# Extract first frame for color generation
|
||||
thumbnail="$THUMBNAIL_DIR/$(basename "$imgpath").jpg"
|
||||
ffmpeg -y -i "$imgpath" -vframes 1 "$thumbnail" 2>/dev/null
|
||||
|
||||
# Set thumbnail path
|
||||
set_thumbnail_path "$thumbnail"
|
||||
|
||||
if [ -f "$thumbnail" ]; then
|
||||
matugen_args+=(image "$thumbnail")
|
||||
generate_colors_material_args=(--path "$thumbnail")
|
||||
create_restore_script "$video_path"
|
||||
else
|
||||
echo "Cannot create image to colorgen"
|
||||
remove_restore
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
matugen_args+=(image "$imgpath")
|
||||
generate_colors_material_args=(--path "$imgpath")
|
||||
# Update wallpaper path in config
|
||||
set_wallpaper_path "$imgpath"
|
||||
remove_restore
|
||||
fi
|
||||
fi
|
||||
|
||||
# Determine mode if not set
|
||||
if [[ -z "$mode_flag" ]]; then
|
||||
current_mode=$(gsettings get org.gnome.desktop.interface color-scheme 2>/dev/null | tr -d "'")
|
||||
if [[ "$current_mode" == "prefer-dark" ]]; then
|
||||
mode_flag="dark"
|
||||
else
|
||||
mode_flag="light"
|
||||
fi
|
||||
fi
|
||||
|
||||
# enforce dark mode for terminal
|
||||
if [[ -n "$mode_flag" ]]; then
|
||||
matugen_args+=(--mode "$mode_flag")
|
||||
if [[ $(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.forceDarkMode' "$SHELL_CONFIG_FILE") == "true" ]]; then
|
||||
generate_colors_material_args+=(--mode "dark")
|
||||
else
|
||||
generate_colors_material_args+=(--mode "$mode_flag")
|
||||
fi
|
||||
fi
|
||||
[[ -n "$type_flag" ]] && matugen_args+=(--type "$type_flag") && generate_colors_material_args+=(--scheme "$type_flag")
|
||||
generate_colors_material_args+=(--termscheme "$terminalscheme" --blend_bg_fg)
|
||||
generate_colors_material_args+=(--cache "$STATE_DIR/user/generated/color.txt")
|
||||
|
||||
pre_process "$mode_flag"
|
||||
|
||||
# Check if app and shell theming is enabled in config
|
||||
if [ -f "$SHELL_CONFIG_FILE" ]; then
|
||||
enable_apps_shell=$(jq -r '.appearance.wallpaperTheming.enableAppsAndShell' "$SHELL_CONFIG_FILE")
|
||||
if [ "$enable_apps_shell" == "false" ]; then
|
||||
echo "App and shell theming disabled, skipping matugen and color generation"
|
||||
return
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set harmony and related properties
|
||||
if [ -f "$SHELL_CONFIG_FILE" ]; then
|
||||
harmony=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.harmony' "$SHELL_CONFIG_FILE")
|
||||
harmonize_threshold=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.harmonizeThreshold' "$SHELL_CONFIG_FILE")
|
||||
term_fg_boost=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.termFgBoost' "$SHELL_CONFIG_FILE")
|
||||
term_bg_tone=$(jq -r '.appearance.wallpaperTheming.terminalGenerationProps.termBgTone' "$SHELL_CONFIG_FILE")
|
||||
[[ "$harmony" != "null" && -n "$harmony" ]] && generate_colors_material_args+=(--harmony "$harmony")
|
||||
[[ "$harmonize_threshold" != "null" && -n "$harmonize_threshold" ]] && generate_colors_material_args+=(--harmonize_threshold "$harmonize_threshold")
|
||||
[[ "$term_fg_boost" != "null" && -n "$term_fg_boost" ]] && generate_colors_material_args+=(--term_fg_boost "$term_fg_boost")
|
||||
[[ "$term_bg_tone" != "null" && -n "$term_bg_tone" ]] && generate_colors_material_args+=(--term_bg_tone "$term_bg_tone")
|
||||
fi
|
||||
|
||||
matugen "${matugen_args[@]}"
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
python3 "$SCRIPT_DIR/generate_colors_material.py" "${generate_colors_material_args[@]}" \
|
||||
> "$STATE_DIR"/user/generated/material_colors.scss
|
||||
deactivate
|
||||
"$SCRIPT_DIR"/applycolor.sh
|
||||
|
||||
# Pass screen width, height, and wallpaper path to post_process
|
||||
max_width_desired="$(hyprctl monitors -j | jq '([.[].width] | min)' | xargs)"
|
||||
max_height_desired="$(hyprctl monitors -j | jq '([.[].height] | min)' | xargs)"
|
||||
post_process "$max_width_desired" "$max_height_desired" "$imgpath"
|
||||
}
|
||||
|
||||
main() {
|
||||
imgpath=""
|
||||
mode_flag=""
|
||||
type_flag=""
|
||||
color_flag=""
|
||||
color=""
|
||||
noswitch_flag=""
|
||||
|
||||
get_type_from_config() {
|
||||
jq -r '.appearance.palette.type' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "auto"
|
||||
}
|
||||
get_accent_color_from_config() {
|
||||
jq -r '.appearance.palette.accentColor' "$SHELL_CONFIG_FILE" 2>/dev/null || echo ""
|
||||
}
|
||||
set_accent_color() {
|
||||
local color="$1"
|
||||
jq --arg color "$color" '.appearance.palette.accentColor = $color' "$SHELL_CONFIG_FILE" > "$SHELL_CONFIG_FILE.tmp" && mv "$SHELL_CONFIG_FILE.tmp" "$SHELL_CONFIG_FILE"
|
||||
}
|
||||
|
||||
detect_scheme_type_from_image() {
|
||||
local img="$1"
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
"$SCRIPT_DIR"/scheme_for_image.py "$img" 2>/dev/null | tr -d '\n'
|
||||
deactivate
|
||||
}
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--mode)
|
||||
mode_flag="$2"
|
||||
shift 2
|
||||
;;
|
||||
--type)
|
||||
type_flag="$2"
|
||||
shift 2
|
||||
;;
|
||||
--color)
|
||||
if [[ "$2" =~ ^#?[A-Fa-f0-9]{6}$ ]]; then
|
||||
set_accent_color "$2"
|
||||
shift 2
|
||||
elif [[ "$2" == "clear" ]]; then
|
||||
set_accent_color ""
|
||||
shift 2
|
||||
else
|
||||
set_accent_color $(hyprpicker --no-fancy)
|
||||
shift
|
||||
fi
|
||||
;;
|
||||
--image)
|
||||
imgpath="$2"
|
||||
shift 2
|
||||
;;
|
||||
--noswitch)
|
||||
noswitch_flag="1"
|
||||
imgpath=$(jq -r '.background.wallpaperPath' "$SHELL_CONFIG_FILE" 2>/dev/null || echo "")
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [[ -z "$imgpath" ]]; then
|
||||
imgpath="$1"
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
# If accentColor is set in config, use it
|
||||
config_color="$(get_accent_color_from_config)"
|
||||
if [[ "$config_color" =~ ^#?[A-Fa-f0-9]{6}$ ]]; then
|
||||
color_flag="1"
|
||||
color="$config_color"
|
||||
fi
|
||||
|
||||
# If type_flag is not set, get it from config
|
||||
if [[ -z "$type_flag" ]]; then
|
||||
type_flag="$(get_type_from_config)"
|
||||
fi
|
||||
|
||||
# Validate type_flag (allow 'auto' as well)
|
||||
allowed_types=(scheme-content scheme-expressive scheme-fidelity scheme-fruit-salad scheme-monochrome scheme-neutral scheme-rainbow scheme-tonal-spot auto)
|
||||
valid_type=0
|
||||
for t in "${allowed_types[@]}"; do
|
||||
if [[ "$type_flag" == "$t" ]]; then
|
||||
valid_type=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $valid_type -eq 0 ]]; then
|
||||
echo "[switchwall.sh] Warning: Invalid type '$type_flag', defaulting to 'auto'" >&2
|
||||
type_flag="auto"
|
||||
fi
|
||||
|
||||
# Only prompt for wallpaper if not using --color and not using --noswitch and no imgpath set
|
||||
if [[ -z "$imgpath" && -z "$color_flag" && -z "$noswitch_flag" ]]; then
|
||||
cd "$(xdg-user-dir PICTURES)/Wallpapers/showcase" 2>/dev/null || cd "$(xdg-user-dir PICTURES)/Wallpapers" 2>/dev/null || cd "$(xdg-user-dir PICTURES)" || return 1
|
||||
imgpath="$(kdialog --getopenfilename . --title 'Choose wallpaper')"
|
||||
fi
|
||||
|
||||
if [[ -n "$imgpath" && -z "$noswitch_flag" ]]; then
|
||||
set_accent_color ""
|
||||
color_flag=""
|
||||
color=""
|
||||
fi
|
||||
|
||||
# If type_flag is 'auto', detect scheme type from image (after imgpath is set)
|
||||
if [[ "$type_flag" == "auto" ]]; then
|
||||
if [[ -n "$imgpath" && -f "$imgpath" ]]; then
|
||||
detected_type="$(detect_scheme_type_from_image "$imgpath")"
|
||||
# Only use detected_type if it's valid
|
||||
valid_detected=0
|
||||
for t in "${allowed_types[@]}"; do
|
||||
if [[ "$detected_type" == "$t" && "$detected_type" != "auto" ]]; then
|
||||
valid_detected=1
|
||||
break
|
||||
fi
|
||||
done
|
||||
if [[ $valid_detected -eq 1 ]]; then
|
||||
type_flag="$detected_type"
|
||||
else
|
||||
echo "[switchwall] Warning: Could not auto-detect a valid scheme, defaulting to 'scheme-tonal-spot'" >&2
|
||||
type_flag="scheme-tonal-spot"
|
||||
fi
|
||||
else
|
||||
echo "[switchwall] Warning: No image to auto-detect scheme from, defaulting to 'scheme-tonal-spot'" >&2
|
||||
type_flag="scheme-tonal-spot"
|
||||
fi
|
||||
fi
|
||||
|
||||
# If mode_flag is dark or light, try to find a variant with that mode suffix
|
||||
if [[ "$mode_flag" == "dark" || "$mode_flag" == "light" ]]; then
|
||||
# Get directory, filename without extension, and extension
|
||||
local imgdir="$(dirname "$imgpath")"
|
||||
local imgbase="$(basename "$imgpath")"
|
||||
local imgname="${imgbase%.*}"
|
||||
local imgext="${imgbase##*.}"
|
||||
|
||||
# Strip existing -dark or -light suffix
|
||||
local stripped_name="${imgname%-dark}"
|
||||
stripped_name="${stripped_name%-light}"
|
||||
|
||||
# Construct the new path with the requested mode suffix
|
||||
local new_imgpath="${imgdir}/${stripped_name}-${mode_flag}.${imgext}"
|
||||
local new_stripped_imgpath="${imgdir}/${stripped_name}.${imgext}"
|
||||
|
||||
# If the variant exists, use it
|
||||
if [[ -f "$new_imgpath" ]]; then
|
||||
imgpath="$new_imgpath"
|
||||
elif [[ -f "$new_stripped_imgpath" ]]; then
|
||||
imgpath="$new_stripped_imgpath"
|
||||
fi
|
||||
fi
|
||||
|
||||
switch "$imgpath" "$mode_flag" "$type_flag" "$color_flag" "$color"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
|
|
@ -0,0 +1,51 @@
|
|||
background #$term0 #
|
||||
|
||||
color0 #$term0 #
|
||||
color1 #$term1 #
|
||||
color2 #$term2 #
|
||||
color3 #$term3 #
|
||||
color4 #$term4 #
|
||||
color5 #$term5 #
|
||||
color6 #$term6 #
|
||||
color7 #$term7 #
|
||||
color8 #$term8 #
|
||||
color9 #$term9 #
|
||||
color10 #$term10 #
|
||||
color11 #$term11 #
|
||||
color12 #$term12 #
|
||||
color13 #$term13 #
|
||||
color14 #$term14 #
|
||||
color15 #$term15 #
|
||||
|
||||
cursor #$term7 #
|
||||
|
||||
foreground #$term7 #
|
||||
|
||||
selection_background #$onSecondaryContainer #
|
||||
selection_foreground #$secondaryContainer #
|
||||
|
||||
# Override obscure colors for starship prompt (these are greys at the end)
|
||||
color255 #$primary #
|
||||
color254 #$primaryContainer #
|
||||
color253 #$secondary #
|
||||
color252 #$secondaryContainer #
|
||||
color251 #$tertiary #
|
||||
color250 #$tertiaryContainer #
|
||||
color249 #$error #
|
||||
color248 #$errorContainer #
|
||||
|
||||
color232 #$onPrimary #
|
||||
color233 #$onPrimaryContainer #
|
||||
color234 #$onSecondary #
|
||||
color235 #$onSecondaryContainer #
|
||||
color236 #$onTertiary #
|
||||
color237 #$onTertiaryContainer #
|
||||
color238 #$onError #
|
||||
color239 #$onErrorContainer #
|
||||
color240 #$onPrimary #
|
||||
# Somehow 232 doesn't work so i gotta use another number
|
||||
|
||||
# Some stuff should specifically use the colors in the middle so they look acceptable in both unthemed light/dark
|
||||
color243 #$primary #
|
||||
color244 #$error #
|
||||
color245 #$outlineVariant #
|
||||
|
|
@ -0,0 +1,38 @@
|
|||
{
|
||||
"dark": {
|
||||
"term0" : "#282828",
|
||||
"term1" : "#CC241D",
|
||||
"term2" : "#98971A",
|
||||
"term3" : "#D79921",
|
||||
"term4" : "#458588",
|
||||
"term5" : "#B16286",
|
||||
"term6" : "#689D6A",
|
||||
"term7" : "#A89984",
|
||||
"term8" : "#928374",
|
||||
"term9" : "#FB4934",
|
||||
"term10" : "#B8BB26",
|
||||
"term11" : "#FABD2F",
|
||||
"term12" : "#83A598",
|
||||
"term13" : "#D3869B",
|
||||
"term14" : "#8EC07C",
|
||||
"term15" : "#EBDBB2"
|
||||
},
|
||||
"light": {
|
||||
"term0" : "#FDF9F3",
|
||||
"term1" : "#FF6188",
|
||||
"term2" : "#A9DC76",
|
||||
"term3" : "#FC9867",
|
||||
"term4" : "#FFD866",
|
||||
"term5" : "#F47FD4",
|
||||
"term6" : "#78DCE8",
|
||||
"term7" : "#333034",
|
||||
"term8" : "#121212",
|
||||
"term9" : "#FF6188",
|
||||
"term10" : "#A9DC76",
|
||||
"term11" : "#FC9867",
|
||||
"term12" : "#FFD866",
|
||||
"term13" : "#F47FD4",
|
||||
"term14" : "#78DCE8",
|
||||
"term15" : "#333034"
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1 @@
|
|||
]4;0;#$term0 #\]1;0;#$term0 #\]4;1;#$term1 #\]4;2;#$term2 #\]4;3;#$term3 #\]4;4;#$term4 #\]4;5;#$term5 #\]4;6;#$term6 #\]4;7;#$term7 #\]4;8;#$term8 #\]4;9;#$term9 #\]4;10;#$term10 #\]4;11;#$term11 #\]4;12;#$term12 #\]4;13;#$term13 #\]4;14;#$term14 #\]4;15;#$term15 #\]4;232;#$term7 #\]4;255;#$primary #\]4;254;#$primaryContainer #\]4;253;#$secondary #\]4;252;#$secondaryContainer #\]4;251;#$tertiary #\]4;250;#$tertiaryContainer #\]4;249;#$error #\]4;248;#$errorContainer #\]4;232;#$onPrimary #\]4;233;#$onPrimaryContainer #\]4;234;#$onSecondary #\]4;235;#$onSecondaryContainer #\]4;236;#$onTertiary #\]4;237;#$onTertiaryContainer #\]4;238;#$onError #\]4;239;#$onErrorContainer #\]4;240;#$onPrimary #\]4;243;#$primary #\]4;244;#$error #\]4;245;#$outlineVariant #\]10;#$term7 #\]11;[100]#$term0 #\]12;#$term7 #\]13;#$term7 #\]17;#$term7 #\]19;#$term0 #\]708;[100]#$term0 #
|
||||
138
surfaces/quickshell/ii-base/scripts/hyprland/hyprconfigurator.py
Executable file
138
surfaces/quickshell/ii-base/scripts/hyprland/hyprconfigurator.py
Executable file
|
|
@ -0,0 +1,138 @@
|
|||
#!/usr/bin/env -S\_/bin/sh\_-c\_"source\_\$HOME/.local/state/quickshell/.venv/bin/activate&&exec\_python\_-E\_"\$0"\_"\$@""
|
||||
import argparse
|
||||
import re
|
||||
import os
|
||||
import tempfile
|
||||
|
||||
def format_value(value):
|
||||
"""Format value: quote strings, leave numbers and booleans as-is"""
|
||||
if value in ('true', 'false'):
|
||||
return value
|
||||
try:
|
||||
float(value)
|
||||
return value
|
||||
except ValueError:
|
||||
return f'"{value}"'
|
||||
|
||||
def build_nested_structure(key_parts, value):
|
||||
"""Recursively build nested structure from key parts"""
|
||||
if len(key_parts) == 1:
|
||||
return f'{key_parts[0]}={format_value(value)}'
|
||||
else:
|
||||
return f'{key_parts[0]}={{{build_nested_structure(key_parts[1:], value)}}}'
|
||||
|
||||
def generate_config_line(key, value):
|
||||
"""Generate hl.config line for given key and value"""
|
||||
key_parts = key.split(':')
|
||||
nested_structure = build_nested_structure(key_parts, value)
|
||||
return f'hl.config({{{nested_structure}}})\n'
|
||||
|
||||
def edit_hyprland_config(file_path, set_args, reset_args):
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r') as file:
|
||||
lines = file.readlines()
|
||||
else:
|
||||
lines = []
|
||||
|
||||
set_dict = {k: v for k, v in set_args} if set_args else {}
|
||||
reset_set = set(reset_args) if reset_args else set()
|
||||
|
||||
new_lines = []
|
||||
found_keys = set()
|
||||
|
||||
patterns = {}
|
||||
for k in list(set_dict.keys()) + list(reset_set):
|
||||
key_parts = k.split(':')
|
||||
main_key = key_parts[0]
|
||||
if len(key_parts) > 1:
|
||||
# Build pattern to match nested structure
|
||||
pattern_parts = [rf'\s*{re.escape(part)}\s*=' for part in key_parts]
|
||||
nested_pattern = '\{'.join(pattern_parts)
|
||||
patterns[k] = re.compile(rf'^\s*hl\.config\(\{{\s*{nested_pattern}')
|
||||
else:
|
||||
patterns[k] = re.compile(rf'^\s*hl\.config\(\{{\s*{re.escape(main_key)}\s*=')
|
||||
|
||||
for line in lines:
|
||||
matched = False
|
||||
|
||||
# Check if line matches a key to be reset
|
||||
for key in reset_set:
|
||||
if patterns[key].match(line):
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
continue
|
||||
|
||||
# Check if line matches a key to be set
|
||||
for key, value in set_dict.items():
|
||||
if patterns[key].match(line):
|
||||
new_line = generate_config_line(key, value)
|
||||
new_lines.append(new_line)
|
||||
found_keys.add(key)
|
||||
matched = True
|
||||
break
|
||||
|
||||
if matched:
|
||||
continue
|
||||
|
||||
new_lines.append(line)
|
||||
|
||||
if set_dict:
|
||||
for key, value in set_dict.items():
|
||||
if key not in found_keys:
|
||||
if new_lines and not new_lines[-1].endswith('\n'):
|
||||
new_lines[-1] += '\n'
|
||||
new_lines.append(generate_config_line(key, value))
|
||||
|
||||
dir_name = os.path.dirname(os.path.abspath(file_path))
|
||||
os.makedirs(dir_name, exist_ok=True)
|
||||
temp_path = None
|
||||
try:
|
||||
with tempfile.NamedTemporaryFile(mode='w', dir=dir_name, delete=False) as temp_file:
|
||||
temp_file.writelines(new_lines)
|
||||
temp_path = temp_file.name
|
||||
|
||||
if os.path.exists(file_path):
|
||||
os.chmod(temp_path, os.stat(file_path).st_mode)
|
||||
else:
|
||||
os.chmod(temp_path, 0o644)
|
||||
|
||||
os.replace(temp_path, file_path)
|
||||
except Exception as e:
|
||||
if temp_path and os.path.exists(temp_path):
|
||||
os.remove(temp_path)
|
||||
print(f"Error saving file: {e}")
|
||||
return
|
||||
|
||||
for key in reset_set:
|
||||
print(f"Removed '{key}' from '{file_path}'")
|
||||
for key, value in set_dict.items():
|
||||
print(f"Updated '{file_path}' with {generate_config_line(key, value).strip()}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
parser = argparse.ArgumentParser(description="Edit a Hyprland config file. Subkeys use colon (:) for nesting.")
|
||||
parser.add_argument("--file", default="~/.config/hypr/hyprland.conf", help="Path to the Hyprland config file (default: ~/.config/hypr/hyprland.conf).")
|
||||
|
||||
parser.add_argument("--set", nargs=2, action="append", metavar=("KEY", "VALUE"), help="Set a configuration key to a value.")
|
||||
parser.add_argument("--reset", action="append", metavar="KEY", help="Remove a configuration key.")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
file_path = os.path.expanduser(args.file)
|
||||
|
||||
raw_set_args = args.set or []
|
||||
reset_args = args.reset or []
|
||||
|
||||
set_args = []
|
||||
for key, value in raw_set_args:
|
||||
if value == "[[EMPTY]]":
|
||||
reset_args.append(key)
|
||||
else:
|
||||
set_args.append((key, value))
|
||||
|
||||
if not set_args and not reset_args:
|
||||
print("Error: Must specify at least one key to set or reset.")
|
||||
else:
|
||||
edit_hyprland_config(file_path, set_args, reset_args)
|
||||
|
||||
6
surfaces/quickshell/ii-base/scripts/images/find-regions-venv.sh
Executable file
6
surfaces/quickshell/ii-base/scripts/images/find-regions-venv.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
"$SCRIPT_DIR/find_regions.py" "$@"
|
||||
deactivate
|
||||
120
surfaces/quickshell/ii-base/scripts/images/find_regions.py
Executable file
120
surfaces/quickshell/ii-base/scripts/images/find_regions.py
Executable file
|
|
@ -0,0 +1,120 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
import argparse
|
||||
import cv2
|
||||
import json
|
||||
import numpy as np
|
||||
import sys
|
||||
|
||||
DEFAULT_IMAGE_PATH = '/tmp/quickshell/media/screenshot/image'
|
||||
|
||||
def iou(boxA, boxB):
|
||||
# Compute intersection over union for two boxes
|
||||
xA = max(boxA['x'], boxB['x'])
|
||||
yA = max(boxA['y'], boxB['y'])
|
||||
xB = min(boxA['x'] + boxA['width'], boxB['x'] + boxB['width'])
|
||||
yB = min(boxA['y'] + boxA['height'], boxB['y'] + boxB['height'])
|
||||
interW = max(0, xB - xA)
|
||||
interH = max(0, yB - yA)
|
||||
interArea = interW * interH
|
||||
boxAArea = boxA['width'] * boxA['height']
|
||||
boxBArea = boxB['width'] * boxB['height']
|
||||
iou = interArea / float(boxAArea + boxBArea - interArea) if (boxAArea + boxBArea - interArea) > 0 else 0
|
||||
return iou
|
||||
|
||||
def non_max_suppression(regions, iou_threshold=0.7):
|
||||
# Sort by area (largest first)
|
||||
regions = sorted(regions, key=lambda r: r['width'] * r['height'], reverse=True)
|
||||
keep = []
|
||||
while regions:
|
||||
current = regions.pop(0)
|
||||
keep.append(current)
|
||||
regions = [r for r in regions if iou(current, r) < iou_threshold]
|
||||
return keep
|
||||
|
||||
def find_regions(image_path, min_width, min_height, max_width=None, max_height=None, quality=False, k=150, min_size=20, sigma=0.8, resize_factor=1.0):
|
||||
image = cv2.imread(image_path)
|
||||
if image is None:
|
||||
print(f'Error: Could not load image {image_path}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
orig_h, orig_w = image.shape[:2]
|
||||
if resize_factor != 1.0:
|
||||
image = cv2.resize(image, (int(orig_w * resize_factor), int(orig_h * resize_factor)), interpolation=cv2.INTER_AREA)
|
||||
ss = cv2.ximgproc.segmentation.createSelectiveSearchSegmentation()
|
||||
ss.setBaseImage(image)
|
||||
if quality:
|
||||
ss.switchToSelectiveSearchQuality(k, min_size, sigma)
|
||||
else:
|
||||
ss.switchToSelectiveSearchFast(k, min_size, sigma)
|
||||
rects = ss.process()
|
||||
regions = []
|
||||
for (x, y, w, h) in rects:
|
||||
# Scale regions back to original image size if resized
|
||||
if resize_factor != 1.0:
|
||||
x = int(x / resize_factor)
|
||||
y = int(y / resize_factor)
|
||||
w = int(w / resize_factor)
|
||||
h = int(h / resize_factor)
|
||||
# Filter out region that is exactly the same size as the original image
|
||||
if w == orig_w and h == orig_h and x == 0 and y == 0:
|
||||
continue
|
||||
if w > min_width and h > min_height:
|
||||
if (max_width is None or w < max_width) and (max_height is None or h < max_height):
|
||||
regions.append({'x': int(x), 'y': int(y), 'width': int(w), 'height': int(h)})
|
||||
# Remove duplicates/overlaps
|
||||
regions = non_max_suppression(regions, iou_threshold=0.7)
|
||||
return regions, cv2.imread(image_path) # Return original image for drawing
|
||||
|
||||
def draw_regions(image, regions, output_path):
|
||||
for region in regions:
|
||||
if 'x' in region:
|
||||
x, y, w, h = region['x'], region['y'], region['width'], region['height']
|
||||
elif 'at' in region and 'size' in region:
|
||||
x, y = region['at']
|
||||
w, h = region['size']
|
||||
else:
|
||||
continue
|
||||
cv2.rectangle(image, (x, y), (x + w, y + h), (0, 0, 255), 2)
|
||||
cv2.imwrite(output_path, image)
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Find regions of interest in an image using selective search.')
|
||||
parser.add_argument('-i', '--image', default=DEFAULT_IMAGE_PATH, help='Path to input image')
|
||||
parser.add_argument('-do', '--debug-output', help='Path to save debug image with rectangles')
|
||||
parser.add_argument('--min-width', type=int, default=200, help='Minimum width of detected region')
|
||||
parser.add_argument('--min-height', type=int, default=100, help='Minimum height of detected region')
|
||||
parser.add_argument('--max-width', type=int, help='Maximum width of detected region')
|
||||
parser.add_argument('--max-height', type=int, help='Maximum height of detected region')
|
||||
parser.add_argument('--single', action='store_true', help='Only output the most likely (largest) region')
|
||||
parser.add_argument('--quality', action='store_true', help='Use quality mode for selective search (slower, less sensitive)')
|
||||
parser.add_argument('--k', type=int, default=3000, help='Segmentation parameter k (default: 150)')
|
||||
parser.add_argument('--min-size', type=int, default=50, help='Segmentation parameter min_size (default: 20)')
|
||||
parser.add_argument('--sigma', type=float, default=0.6, help='Segmentation parameter sigma (default: 0.8)')
|
||||
parser.add_argument('--resize-factor', type=float, default=0.1, help='Resize factor for input image before processing (default: 1.0, e.g. 0.5 for half size)')
|
||||
parser.add_argument('--hyprctl', action='store_true', help='Mimics hyprctl\'s window output, like {"at": [x, y], "size": [w, h]}')
|
||||
args = parser.parse_args()
|
||||
|
||||
regions, image = find_regions(
|
||||
args.image,
|
||||
min_width=args.min_width,
|
||||
min_height=args.min_height,
|
||||
max_width=args.max_width,
|
||||
max_height=args.max_height,
|
||||
quality=args.quality,
|
||||
k=args.k,
|
||||
min_size=args.min_size,
|
||||
sigma=args.sigma,
|
||||
resize_factor=args.resize_factor
|
||||
)
|
||||
if args.single and regions:
|
||||
largest = max(regions, key=lambda r: r['width'] * r['height'])
|
||||
regions = [largest]
|
||||
if args.hyprctl:
|
||||
regions = [{"at": [r['x'], r['y']], "size": [r['width'], r['height']]} for r in regions]
|
||||
print(json.dumps(regions))
|
||||
if args.debug_output:
|
||||
draw_regions(image, regions, args.debug_output)
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
|
||||
6
surfaces/quickshell/ii-base/scripts/images/least-busy-region-venv.sh
Executable file
6
surfaces/quickshell/ii-base/scripts/images/least-busy-region-venv.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
"$SCRIPT_DIR/least_busy_region.py" "$@"
|
||||
deactivate
|
||||
399
surfaces/quickshell/ii-base/scripts/images/least_busy_region.py
Executable file
399
surfaces/quickshell/ii-base/scripts/images/least_busy_region.py
Executable file
|
|
@ -0,0 +1,399 @@
|
|||
#!/usr/bin/env python3
|
||||
# Disclaimer: This script was ai-generated and went through minimal revision.
|
||||
|
||||
import os
|
||||
os.environ["OPENCV_LOG_LEVEL"] = "SILENT"
|
||||
import cv2
|
||||
import numpy as np
|
||||
import argparse
|
||||
import json
|
||||
|
||||
def center_crop(img, target_w, target_h):
|
||||
h, w = img.shape[:2]
|
||||
if w == target_w and h == target_h:
|
||||
return img
|
||||
x1 = max(0, (w - target_w) // 2)
|
||||
y1 = max(0, (h - target_h) // 2)
|
||||
x2 = x1 + target_w
|
||||
y2 = y1 + target_h
|
||||
return img[y1:y2, x1:x2]
|
||||
|
||||
def find_least_busy_region(image_path, region_width=300, region_height=200, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", horizontal_padding=50, vertical_padding=50, busiest=False):
|
||||
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Image not found: {image_path}")
|
||||
orig_h, orig_w = img.shape
|
||||
scale = 1.0
|
||||
if screen_width is not None and screen_height is not None:
|
||||
scale_w = screen_width / orig_w
|
||||
scale_h = screen_height / orig_h
|
||||
if screen_mode == "fill":
|
||||
scale = max(scale_w, scale_h)
|
||||
else:
|
||||
scale = min(scale_w, scale_h)
|
||||
new_w = int(orig_w * scale)
|
||||
new_h = int(orig_h * scale)
|
||||
if verbose:
|
||||
print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
|
||||
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
img = center_crop(img, screen_width, screen_height)
|
||||
if verbose:
|
||||
print(f"Cropped image to {screen_width}x{screen_height}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f"Using original image size: {orig_w}x{orig_h}")
|
||||
arr = img.astype(np.float64)
|
||||
h, w = arr.shape
|
||||
# Validate & adjust stride
|
||||
stride = max(1, int(stride) if stride else 1)
|
||||
# Adjust region size if it does not fit given padding
|
||||
if horizontal_padding * 2 >= w or vertical_padding * 2 >= h:
|
||||
# Reduce padding to fit at least a 1x1 region
|
||||
horizontal_padding = max(0, min(horizontal_padding, (w - 1) // 2))
|
||||
vertical_padding = max(0, min(vertical_padding, (h - 1) // 2))
|
||||
max_region_w = w - 2 * horizontal_padding
|
||||
max_region_h = h - 2 * vertical_padding
|
||||
if max_region_w <= 0 or max_region_h <= 0:
|
||||
raise ValueError("Image too small for the specified padding.")
|
||||
if region_width > max_region_w:
|
||||
if verbose:
|
||||
print(f"Requested region_width {region_width} too large; clamping to {max_region_w}")
|
||||
region_width = max_region_w
|
||||
if region_height > max_region_h:
|
||||
if verbose:
|
||||
print(f"Requested region_height {region_height} too large; clamping to {max_region_h}")
|
||||
region_height = max_region_h
|
||||
# Use OpenCV's integral for fast computation
|
||||
integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
|
||||
integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
|
||||
def region_sum(ii, x1, y1, x2, y2):
|
||||
# Assume bounds have been checked before calling
|
||||
total = ii[y2, x2]
|
||||
if x1 > 0:
|
||||
total -= ii[y2, x1-1]
|
||||
if y1 > 0:
|
||||
total -= ii[y1-1, x2]
|
||||
if x1 > 0 and y1 > 0:
|
||||
total += ii[y1-1, x1-1]
|
||||
return total
|
||||
min_var = None
|
||||
max_var = None
|
||||
min_coords = (horizontal_padding, vertical_padding)
|
||||
max_coords = (horizontal_padding, vertical_padding)
|
||||
area = region_width * region_height
|
||||
x_start = horizontal_padding
|
||||
y_start = vertical_padding
|
||||
x_end = w - region_width - horizontal_padding + 1
|
||||
y_end = h - region_height - vertical_padding + 1
|
||||
if x_end < x_start:
|
||||
x_end = x_start
|
||||
if y_end < y_start:
|
||||
y_end = y_start
|
||||
for y in range(y_start, y_end + 1, stride):
|
||||
for x in range(x_start, x_end + 1, stride):
|
||||
x1, y1 = x, y
|
||||
x2, y2 = x + region_width - 1, y + region_height - 1
|
||||
if x2 >= w or y2 >= h:
|
||||
continue # Skip out-of-bounds window
|
||||
s = region_sum(integral, x1, y1, x2, y2)
|
||||
s2 = region_sum(integral_sq, x1, y1, x2, y2)
|
||||
mean = s / area
|
||||
var = (s2 / area) - (mean ** 2)
|
||||
if (min_var is None) or (var < min_var):
|
||||
min_var = var
|
||||
min_coords = (x, y)
|
||||
if (max_var is None) or (var > max_var):
|
||||
max_var = var
|
||||
max_coords = (x, y)
|
||||
if busiest:
|
||||
return max_coords, max_var
|
||||
else:
|
||||
return min_coords, min_var
|
||||
|
||||
def find_largest_region(image_path, screen_width=None, screen_height=None, verbose=False, stride=2, screen_mode="fill", threshold=100.0, aspect_ratio=1.0, horizontal_padding=50, vertical_padding=50):
|
||||
img = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Image not found: {image_path}")
|
||||
orig_h, orig_w = img.shape
|
||||
# ...existing scaling logic...
|
||||
scale = 1.0
|
||||
if screen_width is not None and screen_height is not None:
|
||||
scale_w = screen_width / orig_w
|
||||
scale_h = screen_height / orig_h
|
||||
if screen_mode == "fill":
|
||||
scale = max(scale_w, scale_h)
|
||||
else:
|
||||
scale = min(scale_w, scale_h)
|
||||
new_w = int(orig_w * scale)
|
||||
new_h = int(orig_h * scale)
|
||||
if verbose:
|
||||
print(f"Scaling image from {orig_w}x{orig_h} to {new_w}x{new_h} (scale: {scale:.3f}, mode: {screen_mode})")
|
||||
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
img = center_crop(img, screen_width, screen_height)
|
||||
if verbose:
|
||||
print(f"Cropped image to {screen_width}x{screen_height}")
|
||||
else:
|
||||
if verbose:
|
||||
print(f"Using original image size: {orig_w}x{orig_h}")
|
||||
arr = img.astype(np.float64)
|
||||
h, w = arr.shape
|
||||
stride = max(1, int(stride) if stride else 1)
|
||||
threshold = max(0.0, float(threshold))
|
||||
# Adjust padding if image too small
|
||||
if horizontal_padding * 2 >= w or vertical_padding * 2 >= h:
|
||||
horizontal_padding = max(0, min(horizontal_padding, (w - 1) // 2))
|
||||
vertical_padding = max(0, min(vertical_padding, (h - 1) // 2))
|
||||
# Use OpenCV's integral for fast computation
|
||||
integral = cv2.integral(arr, sdepth=cv2.CV_64F)[1:,1:]
|
||||
integral_sq = cv2.integral(arr**2, sdepth=cv2.CV_64F)[1:,1:]
|
||||
def region_sum(ii, x1, y1, x2, y2):
|
||||
total = ii[y2, x2]
|
||||
if x1 > 0:
|
||||
total -= ii[y2, x1-1]
|
||||
if y1 > 0:
|
||||
total -= ii[y1-1, x2]
|
||||
if x1 > 0 and y1 > 0:
|
||||
total += ii[y1-1, x1-1]
|
||||
return total
|
||||
min_size = 10
|
||||
# Determine maximum feasible size respecting padding
|
||||
effective_w = w - 2 * horizontal_padding
|
||||
effective_h = h - 2 * vertical_padding
|
||||
if effective_w <= 0 or effective_h <= 0:
|
||||
return None, (0, 0), None
|
||||
# Largest square-ish dimension given aspect ratio and effective space
|
||||
if aspect_ratio >= 1.0:
|
||||
max_size = min(effective_h, int(effective_w / aspect_ratio))
|
||||
else:
|
||||
max_size = min(int(effective_h * aspect_ratio), effective_w)
|
||||
if max_size < min_size:
|
||||
min_size = 1
|
||||
max_size = max(1, max_size)
|
||||
best = None
|
||||
while min_size <= max_size:
|
||||
mid = (min_size + max_size) // 2
|
||||
if aspect_ratio >= 1.0:
|
||||
region_h = mid
|
||||
region_w = int(round(mid * aspect_ratio))
|
||||
else:
|
||||
region_w = mid
|
||||
region_h = int(round(mid / aspect_ratio if aspect_ratio != 0 else mid))
|
||||
if region_w <= 0 or region_h <= 0:
|
||||
break
|
||||
if region_w > effective_w or region_h > effective_h:
|
||||
max_size = mid - 1
|
||||
continue
|
||||
found = False
|
||||
x_start = horizontal_padding
|
||||
y_start = vertical_padding
|
||||
x_end = w - region_w - horizontal_padding
|
||||
y_end = h - region_h - vertical_padding
|
||||
for y in range(y_start, y_end + 1, stride):
|
||||
for x in range(x_start, x_end + 1, stride):
|
||||
x1, y1 = x, y
|
||||
x2, y2 = x + region_w - 1, y + region_h - 1
|
||||
if x2 >= w or y2 >= h:
|
||||
continue
|
||||
s = region_sum(integral, x1, y1, x2, y2)
|
||||
s2 = region_sum(integral_sq, x1, y1, x2, y2)
|
||||
area = region_w * region_h
|
||||
mean = s / area
|
||||
var = (s2 / area) - (mean ** 2)
|
||||
if var <= threshold:
|
||||
found = True
|
||||
best = (x, y, region_w, region_h, var)
|
||||
break
|
||||
if found:
|
||||
break
|
||||
if found:
|
||||
min_size = mid + 1
|
||||
else:
|
||||
max_size = mid - 1
|
||||
if best:
|
||||
x, y, region_w, region_h, var = best
|
||||
center_x = x + region_w // 2
|
||||
center_y = y + region_h // 2
|
||||
return (center_x, center_y), (region_w, region_h), var
|
||||
else:
|
||||
return None, (0, 0), None
|
||||
|
||||
def draw_region(image_path, coords, region_width=300, region_height=200, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Image not found: {image_path}")
|
||||
orig_h, orig_w = img.shape[:2]
|
||||
if screen_width is not None and screen_height is not None:
|
||||
scale_w = screen_width / orig_w
|
||||
scale_h = screen_height / orig_h
|
||||
if screen_mode == "fill":
|
||||
scale = max(scale_w, scale_h)
|
||||
else:
|
||||
scale = min(scale_w, scale_h)
|
||||
new_w = int(orig_w * scale)
|
||||
new_h = int(orig_h * scale)
|
||||
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
img = center_crop(img, screen_width, screen_height)
|
||||
x, y = coords
|
||||
cv2.rectangle(img, (x, y), (x+region_width-1, y+region_height-1), (0,0,255), 3)
|
||||
cv2.imwrite(output_path, img)
|
||||
# print removed for quieter operation
|
||||
|
||||
def draw_largest_region(image_path, center, size, output_path='output.png', screen_width=None, screen_height=None, screen_mode="fill"):
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Image not found: {image_path}")
|
||||
orig_h, orig_w = img.shape[:2]
|
||||
if screen_width is not None and screen_height is not None:
|
||||
scale_w = screen_width / orig_w
|
||||
scale_h = screen_height / orig_h
|
||||
if screen_mode == "fill":
|
||||
scale = max(scale_w, scale_h)
|
||||
else:
|
||||
scale = min(scale_w, scale_h)
|
||||
new_w = int(orig_w * scale)
|
||||
new_h = int(orig_h * scale)
|
||||
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
img = center_crop(img, screen_width, screen_height)
|
||||
cx, cy = center
|
||||
region_w, region_h = size
|
||||
x1 = cx - region_w // 2
|
||||
y1 = cy - region_h // 2
|
||||
x2 = cx + region_w // 2 - 1
|
||||
y2 = cy + region_h // 2 - 1
|
||||
cv2.rectangle(img, (x1, y1), (x2, y2), (255,0,0), 3)
|
||||
cv2.imwrite(output_path, img)
|
||||
# print removed for quieter operation
|
||||
|
||||
def get_dominant_color(image_path, x, y, w, h, screen_width=None, screen_height=None, screen_mode="fill"):
|
||||
img = cv2.imread(image_path)
|
||||
if img is None:
|
||||
raise FileNotFoundError(f"Image not found: {image_path}")
|
||||
orig_h, orig_w = img.shape[:2]
|
||||
if screen_width is not None and screen_height is not None:
|
||||
scale_w = screen_width / orig_w
|
||||
scale_h = screen_height / orig_h
|
||||
if screen_mode == "fill":
|
||||
scale = max(scale_w, scale_h)
|
||||
else:
|
||||
scale = min(scale_w, scale_h)
|
||||
new_w = int(orig_w * scale)
|
||||
new_h = int(orig_h * scale)
|
||||
img = cv2.resize(img, (new_w, new_h), interpolation=cv2.INTER_LANCZOS4)
|
||||
img = center_crop(img, screen_width, screen_height)
|
||||
# Ensure region is within bounds
|
||||
x = max(0, x)
|
||||
y = max(0, y)
|
||||
w = max(1, min(w, img.shape[1] - x))
|
||||
h = max(1, min(h, img.shape[0] - y))
|
||||
region = img[y:y+h, x:x+w]
|
||||
if region.size == 0 or region.shape[0] == 0 or region.shape[1] == 0:
|
||||
return [0, 0, 0]
|
||||
region = region.reshape((-1, 3))
|
||||
# Filter out black pixels (optional, improves accuracy for some images)
|
||||
non_black = region[np.any(region > 10, axis=1)]
|
||||
if non_black.shape[0] == 0:
|
||||
non_black = region
|
||||
region = np.float32(non_black)
|
||||
if region.shape[0] < 3:
|
||||
return [int(x) for x in np.mean(region, axis=0)]
|
||||
# K-means to find dominant color
|
||||
criteria = (cv2.TERM_CRITERIA_EPS + cv2.TERM_CRITERIA_MAX_ITER, 10, 1.0)
|
||||
K = min(3, region.shape[0])
|
||||
_, labels, centers = cv2.kmeans(region, K, None, criteria, 10, cv2.KMEANS_RANDOM_CENTERS)
|
||||
counts = np.bincount(labels.flatten())
|
||||
dominant = centers[np.argmax(counts)]
|
||||
# Reverse from BGR to RGB
|
||||
return [int(x) for x in reversed(dominant)]
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Find least busy region in an image and output a JSON. Made for determining a suitable position for a wallpaper widget.")
|
||||
parser.add_argument("image_path", help="Path to the input image")
|
||||
parser.add_argument("--width", type=int, default=300, help="Region width")
|
||||
parser.add_argument("--height", type=int, default=200, help="Region height")
|
||||
parser.add_argument("-v", "--visual-output", action="store_true", help="Output image with rectangle")
|
||||
parser.add_argument("--screen-width", type=int, default=1920, help="Screen width for wallpaper scaling")
|
||||
parser.add_argument("--screen-height", type=int, default=1080, help="Screen height for wallpaper scaling")
|
||||
parser.add_argument("--stride", type=int, default=10, help="Step size for sliding window (higher is faster, less precise)")
|
||||
parser.add_argument("--screen-mode", choices=["fill", "fit"], default="fill", help="Wallpaper scaling mode: 'fill' (default) or 'fit'")
|
||||
parser.add_argument("--verbose", action="store_true", help="Print verbose output")
|
||||
parser.add_argument("-l", "--largest-region", action="store_true", help="Find the largest region under the variance threshold and output its center")
|
||||
parser.add_argument("-t", "--variance-threshold", type=float, default=1000.0, help="Variance threshold for largest region mode")
|
||||
parser.add_argument("--aspect-ratio", type=float, default=1.78, help="Aspect ratio (width/height) for largest region mode")
|
||||
parser.add_argument("--horizontal-padding", "-hp", type=int, default=50, help="Minimum horizontal distance from region to image edge")
|
||||
parser.add_argument("--vertical-padding", "-vp", type=int, default=50, help="Minimum vertical distance from region to image edge")
|
||||
parser.add_argument("--busiest", action="store_true", help="Find the busiest region instead of the least busy")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.largest_region:
|
||||
center, size, var = find_largest_region(
|
||||
args.image_path,
|
||||
screen_width=args.screen_width,
|
||||
screen_height=args.screen_height,
|
||||
verbose=args.verbose,
|
||||
stride=args.stride,
|
||||
screen_mode=args.screen_mode,
|
||||
threshold=args.variance_threshold,
|
||||
aspect_ratio=args.aspect_ratio,
|
||||
horizontal_padding=args.horizontal_padding,
|
||||
vertical_padding=args.vertical_padding
|
||||
)
|
||||
if center:
|
||||
if args.visual_output:
|
||||
draw_largest_region(args.image_path, center, size, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
|
||||
# Extract dominant color
|
||||
cx, cy = center
|
||||
region_w, region_h = size
|
||||
x1 = cx - region_w // 2
|
||||
y1 = cy - region_h // 2
|
||||
dominant_color = get_dominant_color(
|
||||
args.image_path, x1, y1, region_w, region_h,
|
||||
screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
|
||||
)
|
||||
dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
|
||||
print(json.dumps({
|
||||
"center_x": center[0],
|
||||
"center_y": center[1],
|
||||
"width": size[0],
|
||||
"height": size[1],
|
||||
"variance": var,
|
||||
"dominant_color": dominant_color_hex
|
||||
}))
|
||||
else:
|
||||
print(json.dumps({"error": "No region found under the threshold."}))
|
||||
return
|
||||
|
||||
coords, variance = find_least_busy_region(
|
||||
args.image_path,
|
||||
region_width=args.width,
|
||||
region_height=args.height,
|
||||
screen_width=args.screen_width,
|
||||
screen_height=args.screen_height,
|
||||
verbose=args.verbose,
|
||||
stride=args.stride,
|
||||
screen_mode=args.screen_mode,
|
||||
horizontal_padding=args.horizontal_padding,
|
||||
vertical_padding=args.vertical_padding,
|
||||
busiest=args.busiest
|
||||
)
|
||||
if args.visual_output:
|
||||
draw_region(args.image_path, coords, region_width=args.width, region_height=args.height, screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode)
|
||||
# Output JSON with center point
|
||||
center_x = coords[0] + args.width // 2
|
||||
center_y = coords[1] + args.height // 2
|
||||
dominant_color = get_dominant_color(
|
||||
args.image_path, coords[0], coords[1], args.width, args.height,
|
||||
screen_width=args.screen_width, screen_height=args.screen_height, screen_mode=args.screen_mode
|
||||
)
|
||||
dominant_color_hex = '#{:02x}{:02x}{:02x}'.format(*dominant_color)
|
||||
print(json.dumps({
|
||||
"center_x": center_x,
|
||||
"center_y": center_y,
|
||||
"width": args.width,
|
||||
"height": args.height,
|
||||
"variance": variance,
|
||||
"dominant_color": dominant_color_hex
|
||||
}))
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
6
surfaces/quickshell/ii-base/scripts/images/text-color-venv.sh
Executable file
6
surfaces/quickshell/ii-base/scripts/images/text-color-venv.sh
Executable file
|
|
@ -0,0 +1,6 @@
|
|||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
"$SCRIPT_DIR/text_color.py" "$@"
|
||||
deactivate
|
||||
60
surfaces/quickshell/ii-base/scripts/images/text_color.py
Executable file
60
surfaces/quickshell/ii-base/scripts/images/text_color.py
Executable file
|
|
@ -0,0 +1,60 @@
|
|||
#!/usr/bin/env python3
|
||||
# Disclaimer: This script was ai-generated and went through minimal revision.
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
import json
|
||||
import sys
|
||||
|
||||
def to_hex(color):
|
||||
return "#{:02x}{:02x}{:02x}".format(int(color[0]), int(color[1]), int(color[2]))
|
||||
|
||||
def get_color_from_stdin():
|
||||
# Read raw bytes from stdin
|
||||
input_data = sys.stdin.buffer.read()
|
||||
if not input_data:
|
||||
return {"error": "No data received via stdin"}
|
||||
|
||||
# Convert bytes to numpy array and decode to image
|
||||
nparr = np.frombuffer(input_data, np.uint8)
|
||||
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
|
||||
|
||||
if img is None:
|
||||
return {"error": "Could not decode image data"}
|
||||
|
||||
img_rgb = cv2.cvtColor(img, cv2.COLOR_BGR2RGB)
|
||||
h, w, _ = img_rgb.shape
|
||||
|
||||
# 1. Sample corner pixels (The background anchors)
|
||||
corners = np.array([
|
||||
img_rgb[0, 0],
|
||||
img_rgb[0, w-1],
|
||||
img_rgb[h-1, 0],
|
||||
img_rgb[h-1, w-1]
|
||||
])
|
||||
|
||||
# 2. Determine single dominant background
|
||||
# Using median handles noise/gradients better than a simple average
|
||||
bg_color = np.median(corners, axis=0).astype(int)
|
||||
|
||||
# 3. Find the Text Color
|
||||
pixels = img_rgb.reshape(-1, 3).astype(int)
|
||||
distances = np.linalg.norm(pixels - bg_color, axis=1)
|
||||
|
||||
# Take the 95th percentile of pixels furthest from background
|
||||
threshold = np.percentile(distances, 95)
|
||||
text_pixels = pixels[distances >= threshold]
|
||||
|
||||
if len(text_pixels) == 0:
|
||||
text_color = [255, 255, 255] # Fallback
|
||||
else:
|
||||
text_color = np.median(text_pixels, axis=0).astype(int)
|
||||
|
||||
return {
|
||||
"background": to_hex(bg_color),
|
||||
"text": to_hex(text_color)
|
||||
}
|
||||
|
||||
if __name__ == "__main__":
|
||||
result = get_color_from_stdin()
|
||||
print(json.dumps(result))
|
||||
80
surfaces/quickshell/ii-base/scripts/keyboard_monitor.py
Executable file
80
surfaces/quickshell/ii-base/scripts/keyboard_monitor.py
Executable file
|
|
@ -0,0 +1,80 @@
|
|||
#!/usr/bin/env python3
|
||||
"""Monitor physical keyboard events via evdev and output JSON lines to stdout."""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import select
|
||||
import struct
|
||||
import sys
|
||||
|
||||
# Linux input event format: struct input_event { timeval time; __u16 type; __u16 code; __s32 value; }
|
||||
# On 64-bit: timeval is 16 bytes (tv_sec: 8, tv_usec: 8), total = 24 bytes
|
||||
EVENT_FORMAT = "llHHI"
|
||||
EVENT_SIZE = struct.calcsize(EVENT_FORMAT)
|
||||
EV_KEY = 1
|
||||
|
||||
|
||||
def find_keyboard_devices():
|
||||
"""Find keyboard event devices via /dev/input/by-path."""
|
||||
seen = set()
|
||||
devices = []
|
||||
for path in sorted(glob.glob("/dev/input/by-path/*-event-kbd")):
|
||||
real = os.path.realpath(path)
|
||||
if real not in seen:
|
||||
seen.add(real)
|
||||
devices.append(real)
|
||||
return devices
|
||||
|
||||
|
||||
def emit(obj):
|
||||
sys.stdout.write(json.dumps(obj) + "\n")
|
||||
sys.stdout.flush()
|
||||
|
||||
|
||||
def main():
|
||||
devices = find_keyboard_devices()
|
||||
if not devices:
|
||||
emit({"error": "No keyboard devices found"})
|
||||
return 1
|
||||
|
||||
fds = {}
|
||||
for dev_path in devices:
|
||||
try:
|
||||
fd = os.open(dev_path, os.O_RDONLY | os.O_NONBLOCK)
|
||||
fds[fd] = dev_path
|
||||
except PermissionError:
|
||||
emit({"error": f"Permission denied: {dev_path}"})
|
||||
|
||||
if not fds:
|
||||
emit({"error": "Could not open any keyboard devices"})
|
||||
return 1
|
||||
|
||||
emit({"status": "ready", "devices": list(fds.values())})
|
||||
|
||||
try:
|
||||
while True:
|
||||
readable, _, _ = select.select(list(fds.keys()), [], [])
|
||||
for fd in readable:
|
||||
try:
|
||||
data = os.read(fd, EVENT_SIZE * 64)
|
||||
for offset in range(0, len(data), EVENT_SIZE):
|
||||
chunk = data[offset : offset + EVENT_SIZE]
|
||||
if len(chunk) < EVENT_SIZE:
|
||||
break
|
||||
_, _, ev_type, code, value = struct.unpack(EVENT_FORMAT, chunk)
|
||||
# value: 0=release, 1=press, 2=repeat (we ignore repeat)
|
||||
if ev_type == EV_KEY and value in (0, 1):
|
||||
emit({"keycode": code, "pressed": value == 1})
|
||||
except BlockingIOError:
|
||||
pass
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
for fd in fds:
|
||||
os.close(fd)
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
11
surfaces/quickshell/ii-base/scripts/keyring/is_unlocked.sh
Executable file
11
surfaces/quickshell/ii-base/scripts/keyring/is_unlocked.sh
Executable file
|
|
@ -0,0 +1,11 @@
|
|||
#!/usr/bin/env bash
|
||||
locked_state=$(busctl --user get-property org.freedesktop.secrets \
|
||||
/org/freedesktop/secrets/collection/login \
|
||||
org.freedesktop.Secret.Collection Locked)
|
||||
if [[ "${locked_state}" == "b false" ]]; then
|
||||
echo 'Keyring is unlocked' >&2
|
||||
exit 0
|
||||
else
|
||||
echo 'Keyring is locked' >&2
|
||||
exit 1
|
||||
fi
|
||||
15
surfaces/quickshell/ii-base/scripts/keyring/try_lookup.sh
Executable file
15
surfaces/quickshell/ii-base/scripts/keyring/try_lookup.sh
Executable file
|
|
@ -0,0 +1,15 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
data=$(secret-tool lookup 'application' 'illogical-impulse')
|
||||
if [[ -z "$data" ]]; then
|
||||
if "${SCRIPT_DIR}/is_unlocked.sh"; then
|
||||
echo 'not found'
|
||||
exit 1
|
||||
else
|
||||
echo 'locked'
|
||||
exit 2
|
||||
fi
|
||||
fi
|
||||
echo "$data"
|
||||
23
surfaces/quickshell/ii-base/scripts/keyring/unlock.sh
Executable file
23
surfaces/quickshell/ii-base/scripts/keyring/unlock.sh
Executable file
|
|
@ -0,0 +1,23 @@
|
|||
#!/usr/bin/env bash
|
||||
# Based on https://unix.stackexchange.com/a/602935
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
# Skip if already unlocked
|
||||
if "${SCRIPT_DIR}/is_unlocked.sh"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Prompt for password if not provided
|
||||
if [[ -z "${UNLOCK_PASSWORD}" ]]; then
|
||||
echo -n 'Login password: ' >&2
|
||||
read -s UNLOCK_PASSWORD || return
|
||||
fi
|
||||
|
||||
# Unlock
|
||||
killall -q -u "$(whoami)" gnome-keyring-daemon
|
||||
eval $(echo -n "${UNLOCK_PASSWORD}" \
|
||||
| gnome-keyring-daemon --daemonize --login \
|
||||
| sed -e 's/^/export /')
|
||||
unset UNLOCK_PASSWORD
|
||||
echo '' >&2
|
||||
55
surfaces/quickshell/ii-base/scripts/musicRecognition/recognize-music.sh
Executable file
55
surfaces/quickshell/ii-base/scripts/musicRecognition/recognize-music.sh
Executable file
|
|
@ -0,0 +1,55 @@
|
|||
#!/bin/bash
|
||||
|
||||
INTERVAL=2
|
||||
TOTAL_DURATION=30
|
||||
SOURCE_TYPE="monitor" # monitor | input
|
||||
FIFO=$(mktemp -u /tmp/songrec_out_XXXXXX)
|
||||
|
||||
while getopts "i:t:s:" opt; do
|
||||
case $opt in
|
||||
i) INTERVAL=$OPTARG ;;
|
||||
t) TOTAL_DURATION=$OPTARG ;;
|
||||
s) SOURCE_TYPE=$OPTARG ;;
|
||||
*) exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if ! command -v songrec >/dev/null 2>&1; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$SOURCE_TYPE" = "monitor" ]; then
|
||||
AUDIO_DEVICE=$(pactl get-default-sink).monitor
|
||||
elif [ "$SOURCE_TYPE" = "input" ]; then
|
||||
AUDIO_DEVICE=$(pactl info | grep "Default Source:" | awk '{print $3}' || true)
|
||||
else
|
||||
echo "Invalid source type"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ -z "$AUDIO_DEVICE" ] || ! pactl list short sources | grep -q "$AUDIO_DEVICE"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkfifo "$FIFO"
|
||||
|
||||
cleanup() {
|
||||
kill "$SONGREC_PID" 2>/dev/null || true
|
||||
wait "$SONGREC_PID" 2>/dev/null
|
||||
rm -f "$FIFO"
|
||||
}
|
||||
trap cleanup EXIT
|
||||
|
||||
songrec listen --audio-device "$AUDIO_DEVICE" --request-interval "$INTERVAL" --json --disable-mpris > "$FIFO" &
|
||||
SONGREC_PID=$!
|
||||
|
||||
( sleep "$TOTAL_DURATION" && kill "$SONGREC_PID" 2>/dev/null ) &
|
||||
|
||||
while IFS= read -r line; do
|
||||
if echo "$line" | grep -q '"track":'; then
|
||||
echo "$line"
|
||||
exit 0
|
||||
fi
|
||||
done < "$FIFO"
|
||||
|
||||
exit 0
|
||||
132
surfaces/quickshell/ii-base/scripts/thumbnails/generate-thumbnails-magick.sh
Executable file
132
surfaces/quickshell/ii-base/scripts/thumbnails/generate-thumbnails-magick.sh
Executable file
|
|
@ -0,0 +1,132 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
# Generate thumbnails for files using ImageMagick, following Freedesktop spec
|
||||
# Usage:
|
||||
# ./generate-thumbnails-magick.sh --file <path>
|
||||
# ./generate-thumbnails-magick.sh --directory <path>
|
||||
|
||||
set -e
|
||||
|
||||
# Thumbnail sizes mapping
|
||||
get_thumbnail_size() {
|
||||
case "$1" in
|
||||
normal) echo 128 ;;
|
||||
large) echo 256 ;;
|
||||
x-large) echo 512 ;;
|
||||
xx-large) echo 1024 ;;
|
||||
*) echo 128 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
usage() {
|
||||
echo "Usage: $0 --file <path> | --directory <path>"
|
||||
exit 1
|
||||
}
|
||||
|
||||
md5() {
|
||||
# Calculate md5 hash of the file's absolute path
|
||||
echo -n "$1" | md5sum | awk '{print $1}'
|
||||
}
|
||||
|
||||
urlencode() {
|
||||
# Percent-encode a string for use in a URI, but do not encode slashes
|
||||
local str="$1"
|
||||
local encoded=""
|
||||
local c
|
||||
for ((i=0; i<${#str}; i++)); do
|
||||
c="${str:$i:1}"
|
||||
case "$c" in
|
||||
[a-zA-Z0-9.~_-]|/|'('|')'|'*') encoded+="$c" ;;
|
||||
*) printf -v hex '%%%02X' "'${c}'"; encoded+="$hex" ;;
|
||||
esac
|
||||
done
|
||||
echo "$encoded"
|
||||
}
|
||||
|
||||
generate_thumbnail() {
|
||||
local src="$1"
|
||||
local abs_path
|
||||
abs_path="$(realpath "$src")"
|
||||
local encoded_path
|
||||
encoded_path="$(urlencode "$abs_path")"
|
||||
local uri
|
||||
uri="file://$encoded_path"
|
||||
local hash
|
||||
hash="$(md5 "$uri")"
|
||||
local out="$CACHE_DIR/$hash.png"
|
||||
mkdir -p "$CACHE_DIR"
|
||||
if [ -f "$out" ]; then
|
||||
return
|
||||
fi
|
||||
case "${abs_path,,}" in
|
||||
*.mp4|*.webm|*.mkv|*.avi|*.mov)
|
||||
ffmpeg -y -i "$abs_path" -vframes 1 -vf "scale=${THUMBNAIL_SIZE}:${THUMBNAIL_SIZE}:force_original_aspect_ratio=decrease" "$out" 2>/dev/null
|
||||
;;
|
||||
*.gif)
|
||||
magick "$abs_path" -resize "${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE}" "$out"
|
||||
;;
|
||||
*)
|
||||
magick "$abs_path" -resize "${THUMBNAIL_SIZE}x${THUMBNAIL_SIZE}" "$out"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# Parse arguments
|
||||
SIZE_NAME="normal"
|
||||
MODE=""
|
||||
TARGET=""
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--file|-f)
|
||||
MODE="file"
|
||||
TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--directory|-d)
|
||||
MODE="dir"
|
||||
TARGET="$2"
|
||||
shift 2
|
||||
;;
|
||||
--size|-s)
|
||||
SIZE_NAME="$2"
|
||||
shift 2
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
# Only one mode allowed
|
||||
[[ -n "$MODE" ]] && break
|
||||
done
|
||||
|
||||
THUMBNAIL_SIZE="$(get_thumbnail_size "$SIZE_NAME")"
|
||||
CACHE_DIR="$HOME/.cache/thumbnails/$SIZE_NAME"
|
||||
|
||||
if [ -z "$MODE" ] || [ -z "$TARGET" ]; then
|
||||
usage
|
||||
fi
|
||||
|
||||
case "$MODE" in
|
||||
file)
|
||||
if [ ! -f "$TARGET" ]; then
|
||||
echo "File not found: $TARGET"
|
||||
exit 2
|
||||
fi
|
||||
generate_thumbnail "$TARGET"
|
||||
;;
|
||||
dir)
|
||||
if [ ! -d "$TARGET" ]; then
|
||||
echo "Directory not found: $TARGET"
|
||||
exit 2
|
||||
fi
|
||||
for f in "$TARGET"/*; do
|
||||
[ -f "$f" ] || continue
|
||||
generate_thumbnail "$f" &
|
||||
done
|
||||
wait
|
||||
;;
|
||||
*)
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
|
||||
9
surfaces/quickshell/ii-base/scripts/thumbnails/thumbgen-venv.sh
Executable file
9
surfaces/quickshell/ii-base/scripts/thumbnails/thumbgen-venv.sh
Executable file
|
|
@ -0,0 +1,9 @@
|
|||
#!/usr/bin/env bash
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
source "$HOME/.local/state/quickshell/.venv/bin/activate"
|
||||
GIO_USE_VFS=local "$SCRIPT_DIR/thumbgen.py" "$@"
|
||||
THUMBGEN_EXIT_CODE=$?
|
||||
deactivate
|
||||
|
||||
exit $THUMBGEN_EXIT_CODE
|
||||
119
surfaces/quickshell/ii-base/scripts/thumbnails/thumbgen.py
Executable file
119
surfaces/quickshell/ii-base/scripts/thumbnails/thumbgen.py
Executable file
|
|
@ -0,0 +1,119 @@
|
|||
#!/usr/bin/env python3
|
||||
|
||||
# From https://github.com/difference-engine/thumbnail-generator-ubuntu (MIT License)
|
||||
# Since the script is small and the maintainers seem inactive to accept my PR (#11) I decided to just copy it over.
|
||||
# When it gets merged and the python package gets updated we can just use it
|
||||
|
||||
import os
|
||||
import sys
|
||||
from multiprocessing import Pool
|
||||
from pathlib import Path
|
||||
from typing import List, Union
|
||||
|
||||
import click
|
||||
import gi
|
||||
from loguru import logger
|
||||
from tqdm import tqdm
|
||||
|
||||
gi.require_version("GnomeDesktop", "4.0")
|
||||
from gi.repository import Gio, GnomeDesktop # isort:skip
|
||||
|
||||
thumbnail_size_map = {
|
||||
"normal": GnomeDesktop.DesktopThumbnailSize.NORMAL,
|
||||
"large": GnomeDesktop.DesktopThumbnailSize.LARGE,
|
||||
"x-large": GnomeDesktop.DesktopThumbnailSize.XLARGE,
|
||||
"xx-large": GnomeDesktop.DesktopThumbnailSize.XXLARGE,
|
||||
}
|
||||
|
||||
factory = None
|
||||
logger.remove()
|
||||
logger.add(sys.stdout, level="INFO")
|
||||
logger.add("/tmp/thumbgen.log", level="DEBUG", rotation="100 MB")
|
||||
|
||||
def make_thumbnail(fpath: str) -> bool:
|
||||
mtime = os.path.getmtime(fpath)
|
||||
# Use Gio to determine the URI and mime type
|
||||
f = Gio.file_new_for_path(str(fpath))
|
||||
uri = f.get_uri()
|
||||
info = f.query_info("standard::content-type", Gio.FileQueryInfoFlags.NONE, None)
|
||||
mime_type = info.get_content_type()
|
||||
|
||||
if factory.lookup(uri, mtime) is not None:
|
||||
logger.debug("FRESH {}".format(uri))
|
||||
return False
|
||||
|
||||
if not factory.can_thumbnail(uri, mime_type, mtime):
|
||||
logger.debug("UNSUPPORTED {}".format(uri))
|
||||
return False
|
||||
|
||||
thumbnail = factory.generate_thumbnail(uri, mime_type)
|
||||
if thumbnail is None:
|
||||
logger.debug("ERROR {}".format(uri))
|
||||
return False
|
||||
|
||||
logger.debug("OK {}".format(uri))
|
||||
factory.save_thumbnail(thumbnail, uri, mtime)
|
||||
return True
|
||||
|
||||
|
||||
@logger.catch()
|
||||
def thumbnail_folder(*, dir_path: Path, workers: int, only_images: bool, recursive: bool, machine_progress: bool = False) -> None:
|
||||
all_files = get_all_files(dir_path=dir_path, recursive=recursive)
|
||||
if only_images:
|
||||
all_files = get_all_images(all_files=all_files)
|
||||
all_files = [str(fpath) for fpath in all_files]
|
||||
if machine_progress:
|
||||
completed = 0
|
||||
total = len(all_files)
|
||||
with Pool(processes=workers) as p:
|
||||
for result in p.imap(make_thumbnail, all_files):
|
||||
completed += 1
|
||||
print(f"PROGRESS {completed}/{total} FILE {all_files[completed-1]}")
|
||||
sys.stdout.flush()
|
||||
else:
|
||||
with Pool(processes=workers) as p:
|
||||
list(tqdm(p.imap(make_thumbnail, all_files), total=len(all_files)))
|
||||
|
||||
|
||||
def get_all_images(*, all_files: List[Path]) -> List[Path]:
|
||||
img_suffixes = [".jpg", ".jpeg", ".png", ".gif"]
|
||||
all_images = [fpath for fpath in all_files if fpath.suffix in img_suffixes]
|
||||
print("Found {} images".format(len(all_images)))
|
||||
return all_images
|
||||
|
||||
|
||||
def get_all_files(*, dir_path: Path, recursive: bool) -> List[Path]:
|
||||
if not (dir_path.exists() and dir_path.is_dir()):
|
||||
raise ValueError("{} doesn't exist or isn't a valid directory!".format(dir_path.resolve()))
|
||||
if recursive:
|
||||
all_files = dir_path.rglob("*")
|
||||
else:
|
||||
all_files = dir_path.glob("*")
|
||||
all_files = [fpath for fpath in all_files if fpath.is_file()]
|
||||
print("Found {} files in the directory: {}".format(len(all_files), dir_path.resolve()))
|
||||
return all_files
|
||||
|
||||
@click.command()
|
||||
@click.option(
|
||||
"-d", "--img_dirs", required=True, help='directories to generate thumbnails seperated by space, eg: "dir1/dir2 dir3"'
|
||||
)
|
||||
@click.option(
|
||||
"-s", "--size", default="normal", type=click.Choice(["normal", "large", "x-large", "xx-large"]), help="Thumbnail size: normal, large, x-large, xx-large"
|
||||
)
|
||||
@click.option("-w", "--workers", default=1, help="no of cpus to use for processing")
|
||||
@click.option(
|
||||
"-i", "--only_images", is_flag=True, default=False, help="Whether to only look for images to be thumbnailed"
|
||||
)
|
||||
@click.option("-r", "--recursive", is_flag=True, default=False, help="Whether to recursively look for files")
|
||||
@click.option("--machine_progress", is_flag=True, default=False, help="Print machine-readable progress lines instead of a progress bar")
|
||||
def main(img_dirs: str, size: str, workers: str, only_images: bool, recursive: bool, machine_progress: bool) -> None:
|
||||
img_dirs = [Path(img_dir) for img_dir in img_dirs.split()]
|
||||
global factory
|
||||
factory = GnomeDesktop.DesktopThumbnailFactory.new(thumbnail_size_map[size])
|
||||
for img_dir in img_dirs:
|
||||
thumbnail_folder(dir_path=img_dir, workers=workers, only_images=only_images, recursive=recursive, machine_progress=machine_progress)
|
||||
print("Thumbnail Generation Completed!")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
78
surfaces/quickshell/ii-base/scripts/videos/record.sh
Executable file
78
surfaces/quickshell/ii-base/scripts/videos/record.sh
Executable file
|
|
@ -0,0 +1,78 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
CONFIG_FILE="$HOME/.config/illogical-impulse/config.json"
|
||||
JSON_PATH=".screenRecord.savePath"
|
||||
|
||||
CUSTOM_PATH=$(jq -r "$JSON_PATH" "$CONFIG_FILE" 2>/dev/null)
|
||||
|
||||
RECORDING_DIR=""
|
||||
|
||||
if [[ -n "$CUSTOM_PATH" ]]; then
|
||||
RECORDING_DIR="$CUSTOM_PATH"
|
||||
else
|
||||
RECORDING_DIR="$HOME/Videos" # Use default path
|
||||
fi
|
||||
|
||||
getdate() {
|
||||
date '+%Y-%m-%d_%H.%M.%S'
|
||||
}
|
||||
getaudiooutput() {
|
||||
pactl list sources | grep 'Name' | grep 'monitor' | cut -d ' ' -f2
|
||||
}
|
||||
getactivemonitor() {
|
||||
hyprctl monitors -j | jq -r '.[] | select(.focused == true) | .name'
|
||||
}
|
||||
|
||||
mkdir -p "$RECORDING_DIR"
|
||||
cd "$RECORDING_DIR" || exit
|
||||
|
||||
# parse --region <value> without modifying $@ so other flags like --fullscreen still work
|
||||
ARGS=("$@")
|
||||
MANUAL_REGION=""
|
||||
SOUND_FLAG=0
|
||||
FULLSCREEN_FLAG=0
|
||||
for ((i=0;i<${#ARGS[@]};i++)); do
|
||||
if [[ "${ARGS[i]}" == "--region" ]]; then
|
||||
if (( i+1 < ${#ARGS[@]} )); then
|
||||
MANUAL_REGION="${ARGS[i+1]}"
|
||||
else
|
||||
notify-send "Recording cancelled" "No region specified for --region" -a 'Recorder' & disown
|
||||
exit 1
|
||||
fi
|
||||
elif [[ "${ARGS[i]}" == "--sound" ]]; then
|
||||
SOUND_FLAG=1
|
||||
elif [[ "${ARGS[i]}" == "--fullscreen" ]]; then
|
||||
FULLSCREEN_FLAG=1
|
||||
fi
|
||||
done
|
||||
|
||||
if pgrep wf-recorder > /dev/null; then
|
||||
notify-send "Recording Stopped" "Stopped" -a 'Recorder' &
|
||||
pkill wf-recorder &
|
||||
else
|
||||
if [[ $FULLSCREEN_FLAG -eq 1 ]]; then
|
||||
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
|
||||
if [[ $SOUND_FLAG -eq 1 ]]; then
|
||||
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --audio="$(getaudiooutput)"
|
||||
else
|
||||
wf-recorder -o "$(getactivemonitor)" --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t
|
||||
fi
|
||||
else
|
||||
# If a manual region was provided via --region, use it; otherwise run slurp as before.
|
||||
if [[ -n "$MANUAL_REGION" ]]; then
|
||||
region="$MANUAL_REGION"
|
||||
else
|
||||
if ! region="$(slurp 2>&1)"; then
|
||||
notify-send "Recording cancelled" "Selection was cancelled" -a 'Recorder' & disown
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
notify-send "Starting recording" 'recording_'"$(getdate)"'.mp4' -a 'Recorder' & disown
|
||||
if [[ $SOUND_FLAG -eq 1 ]]; then
|
||||
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region" --audio="$(getaudiooutput)"
|
||||
else
|
||||
wf-recorder --pixel-format yuv420p -f './recording_'"$(getdate)"'.mp4' -t --geometry "$region"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
Loading…
Reference in a new issue