Watch
1
0
Fork
You've already forked souveraine
0

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:
Fimeg 2026-09-04 15:55:48 -04:00
commit 8f42fc953d
1476 changed files with 238455 additions and 0 deletions

View 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'

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

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

View file

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

View 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

View 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

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

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

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

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

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

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

View file

@ -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 #

View file

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

View file

@ -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 #

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

View 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

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

View 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

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

View 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

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

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

View 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

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

View 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

View 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

View 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

View 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

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

View 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