Compare commits

...

8 Commits

24 changed files with 348 additions and 150 deletions
+51 -4
View File
@@ -13,9 +13,38 @@
border-radius: 8px;
border: 2px solid alpha(@color4, 0.5);
padding: 8px 15px;
min-width: 300px;
font-family: "JetBrainsMono Nerd Font", sans-serif;
}
.media-cover {
min-width: 64px;
min-height: 64px;
border-radius: 4px;
background-size: cover;
background-position: center;
}
.media-info-box {
margin-left: 10px;
}
.media-title {
color: @foreground;
font-size: 14px;
font-weight: bold;
}
.media-artist {
color: @foreground;
font-size: 13px;
opacity: 0.8;
}
.media-controls {
/* Box spacing is handled by the component */
}
.media-btn {
color: @color5;
font-size: 16px;
@@ -27,8 +56,26 @@
color: @color4;
}
.media-text {
color: @foreground;
font-size: 14px;
margin-left: 10px;
.media-progress {
min-height: 4px;
padding: 0;
}
.media-progress trough {
min-height: 4px;
background-color: alpha(@foreground, 0.2);
border-radius: 4px;
}
.media-progress highlight {
background-color: @accent;
border-radius: 4px;
}
.media-progress slider {
background-color: transparent;
min-width: 0px;
min-height: 0px;
border: none;
box-shadow: none;
}
+66 -11
View File
@@ -6,9 +6,26 @@ import { createPoll } from "ags/time"
export default function Media(gdkmonitor: Gdk.Monitor) {
const { TOP, LEFT } = Astal.WindowAnchor
// Poll playerctl for metadata and status
const mediaInfo = createPoll("No Media Playing", 1000, 'sh -c "playerctl metadata -f \'{{title}} - {{artist}}\' 2>/dev/null || echo \'No Media Playing\'"')
const statusIcon = createPoll("▶", 1000, 'sh -c "s=\\$(playerctl status 2>/dev/null); if [ \\"\\$s\\" = \\"Playing\\" ]; then echo \\"⏸\\"; else echo \\"▶\\"; fi"')
const pollCmd = "playerctl metadata -f '{{title}}|||{{artist}}|||{{mpris:artUrl}}|||{{status}}|||{{mpris:length}}|||{{position}}' 2>/dev/null || echo 'No Media||||||Stopped|||0|||0'";
const mediaState = createPoll({
title: "No Media",
artist: "",
artUrl: "",
status: "Stopped",
length: 0,
position: 0
}, 1000, pollCmd, (stdout) => {
const parts = stdout.split("|||");
const title = parts[0]?.trim() || "No Media";
const artist = parts[1]?.trim() || "";
const rawUrl = parts[2]?.trim() || "";
const artUrl = rawUrl.replace(/^file:\/\//, '');
const status = parts[3]?.trim() || "Stopped";
const length = Number(parts[4]) || 0;
const position = Number(parts[5]) || 0;
return { title, artist, artUrl, status, length, position };
});
return (
<window
@@ -20,29 +37,67 @@ export default function Media(gdkmonitor: Gdk.Monitor) {
margin={20}
application={app}
>
<box class="media-container" spacing={10}>
<box class="media-container" css="min-width: 350px;" spacing={15}>
<box
class="media-cover"
css={mediaState.as(s => s.artUrl ? `background-image: url('${s.artUrl}'); min-width: 80px; min-height: 80px;` : 'min-width: 80px; min-height: 80px; background-color: alpha(@color0, 0.5);')}
/>
<box vertical class="media-info-box" valign={Gtk.Align.CENTER} hexpand>
<label
class="media-title"
label={mediaState.as(s => s.title)}
halign={Gtk.Align.FILL}
xalign={0}
truncate
maxWidthChars={35}
css="font-size: 15px;"
/>
<label
class="media-artist"
label={mediaState.as(s => s.artist)}
halign={Gtk.Align.FILL}
xalign={0}
truncate
maxWidthChars={40}
css="font-size: 13px;"
/>
<slider
class="media-progress"
drawValue={false}
hexpand
min={0}
max={1}
value={mediaState.as(s => {
const len = s.length / 1000000;
return len > 0 ? Math.min(s.position / len, 1) : 0;
})}
marginTop={10}
marginBottom={10}
/>
<box class="media-controls" spacing={15} halign={Gtk.Align.CENTER}>
<button
class="media-btn"
onClicked={() => execAsync("playerctl previous").catch(print)}
halign={Gtk.Align.CENTER}
>
<label label="" />
<label label="" css="font-size: 18px;" />
</button>
<button
class="media-btn"
onClicked={() => execAsync("playerctl play-pause").catch(print)}
halign={Gtk.Align.CENTER}
>
<label label={statusIcon} />
<label label={mediaState.as(s => s.status === "Playing" ? "" : "")} css="font-size: 22px;" />
</button>
<button
class="media-btn"
onClicked={() => execAsync("playerctl next").catch(print)}
halign={Gtk.Align.CENTER}
>
<label label="" />
<label label="" css="font-size: 18px;" />
</button>
<label class="media-text" label={mediaInfo} />
</box>
</box>
</box>
</window>
)
Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 79 KiB

@@ -0,0 +1,57 @@
Name = "blobBackgroundSelector"
NamePretty = "Blob's Background Selector"
Cache = false
HideFromProviderlist = true
SearchName = true
local function ShellEscape(s)
return "'" .. s:gsub("'", "'\\''") .. "'"
end
function FormatName(filename)
local name = filename:gsub("^%d+", ""):gsub("^%-", "")
name = name:gsub("%.[^%.]+$", "")
name = name:gsub("-", " ")
name = name:gsub("%S+", function(word)
return word:sub(1, 1):upper() .. word:sub(2):lower()
end)
return name
end
function GetEntries()
local entries = {}
local home = os.getenv("HOME")
local dirs = {
home .. "/wallpapers",
}
local seen = {}
for _, wallpaper_dir in ipairs(dirs) do
local handle = io.popen(
"find " .. ShellEscape(wallpaper_dir)
.. " -maxdepth 1 -type f \\( -name '*.jpg' -o -name '*.jpeg' -o -name '*.png' -o -name '*.gif' -o -name '*.bmp' -o -name '*.webp' \\) 2>/dev/null | sort"
)
if handle then
for background in handle:lines() do
local filename = background:match("([^/]+)$")
if filename and not seen[filename] then
seen[filename] = true
table.insert(entries, {
Text = FormatName(filename),
Value = filename,
Actions = {
activate = "blob_wallpaper " .. ShellEscape(background),
},
Preview = background,
PreviewType = "file",
})
end
end
handle:close()
end
end
return entries
end
+5
View File
@@ -104,6 +104,9 @@ install_dependencies() {
if ! command -v magick &> /dev/null && ! command -v convert &> /dev/null; then
deps_needed+=("imagemagick")
fi
if ! command -v awww &> /dev/null; then
deps_needed+=("awww")
fi
if [ ${#deps_needed[@]} -gt 0 ]; then
echo "Installing missing dependencies: ${deps_needed[*]}"
@@ -134,6 +137,7 @@ check_file "$SCRIPT_DIR/ags/app.ts" "$HOME_DIR/.config/ags/app.ts" "ags/app.ts"
check_file "$SCRIPT_DIR/ags/style.css" "$HOME_DIR/.config/ags/style.css" "ags/style.css" || check_status=1
check_file "$SCRIPT_DIR/ags/widget/Media.tsx" "$HOME_DIR/.config/ags/widget/Media.tsx" "ags/widget/Media.tsx" || check_status=1
check_file "$SCRIPT_DIR/waybar/style.css" "$HOME_DIR/.config/waybar/style.css" "waybar/style.css" || check_status=1
check_file "$SCRIPT_DIR/elephant/menus/blob_background_selector.lua" "$HOME_DIR/.config/elephant/menus/blob_background_selector.lua" "elephant/menus/blob_background_selector.lua" || check_status=1
check_file "$SCRIPT_DIR/branding/about.txt" "$HOME_DIR/.config/omarchy/branding/about.txt" "branding/about.txt" || check_status=1
check_file "$SCRIPT_DIR/branding/screensaver.txt" "$HOME_DIR/.config/omarchy/branding/screensaver.txt" "branding/screensaver.txt" || check_status=1
@@ -168,6 +172,7 @@ backup_and_copy "$SCRIPT_DIR/waybar" "$HOME_DIR/.config/waybar" "Waybar config"
backup_and_copy "$SCRIPT_DIR/ags" "$HOME_DIR/.config/ags" "AGS config"
backup_and_copy "$SCRIPT_DIR/hypr" "$HOME_DIR/.config/hypr" "Hyprland config"
backup_and_copy "$SCRIPT_DIR/branding" "$HOME_DIR/.config/omarchy/branding" "Branding files"
backup_and_copy "$SCRIPT_DIR/elephant" "$HOME_DIR/.config/elephant" "Elephant configs"
backup_and_copy "$SCRIPT_DIR/omarchy/hooks" "$HOME_DIR/.config/omarchy/hooks" "Omarchy hooks"
backup_and_copy "$SCRIPT_DIR/wallpapers" "$HOME_DIR/wallpapers" "Custom wallpapers"
+104 -78
View File
@@ -1,107 +1,133 @@
#!/usr/bin/env python3
"""
blob_color_fixer.py — Detects monotone/bland wal color palettes and replaces
accent colors with vibrant, harmonically-spread alternatives.
"""
import sys
import math
import colorsys
from dataclasses import dataclass
def hex_to_rgb(hex_str):
hex_str = hex_str.lstrip('#')
return tuple(int(hex_str[i:i+2], 16) / 255.0 for i in (0, 2, 4))
MIN_SATURATION = 0.65
MIN_LIGHTNESS = 0.40
MAX_LIGHTNESS = 0.70
HUE_THRESHOLD = 0.15
SAT_THRESHOLD = 0.35
HUE_VARIANCE_THRESHOLD = 0.15
ACCENT_SLICE = slice(1, 7)
BRIGHT_OFFSET = 8
def rgb_to_hex(r, g, b):
return '#{:02x}{:02x}{:02x}'.format(int(r * 255), int(g * 255), int(b * 255))
HUE_SHIFTS: list[float] = [
0.0,
0.5,
0.083,
-0.083,
0.416,
-0.416,
]
def shift_hue_and_saturate(hex_str, shift_amount):
r, g, b = hex_to_rgb(hex_str)
h, l, s = colorsys.rgb_to_hls(r, g, b)
h = (h + shift_amount) % 1.0
s = max(s, 0.65) # Force vibrancy
l = min(max(l, 0.4), 0.7) # Ensure it's not too dark or overly washed out
r, g, b = colorsys.hls_to_rgb(h, l, s)
return rgb_to_hex(r, g, b)
@dataclass(frozen=True)
class HLS:
h: float
l: float
s: float
def get_hls_stats(hex_list):
hues = []
sats = []
for hex_str in hex_list:
r, g, b = hex_to_rgb(hex_str)
h, l, s = colorsys.rgb_to_hls(r, g, b)
hues.append(h)
sats.append(s)
return hues, sats
def is_monotone_or_bland(hues, sats, hue_threshold=0.15, sat_threshold=0.35):
if not hues or not sats: return False
def hex_to_rgb(hex_str: str) -> tuple[float, float, float]:
hex_str = hex_str.lstrip("#")
return tuple(int(hex_str[i : i + 2], 16) / 255.0 for i in (0, 2, 4)) # type: ignore[return-value]
# Calculate hue variance
min_h = min(hues)
max_h = max(hues)
hue_diff = min(max_h - min_h, 1.0 - (max_h - min_h))
# Calculate average saturation
avg_s = sum(sats) / len(sats)
def rgb_to_hex(r: float, g: float, b: float) -> str:
return "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
return hue_diff < hue_threshold or avg_s < sat_threshold
def main():
if len(sys.argv) < 2:
print("Usage: python3 blob_color_fixer.py <path_to_wal_colors>")
sys.exit(1)
def hex_to_hls(hex_str: str) -> HLS:
h, l, s = colorsys.rgb_to_hls(*hex_to_rgb(hex_str))
return HLS(h, l, s)
filepath = sys.argv[1]
def vibrant_shift(hex_str: str, hue_shift: float) -> str:
"""Return *hex_str* with its hue rotated by *hue_shift* and vibrancy enforced."""
hls = hex_to_hls(hex_str)
h = (hls.h + hue_shift) % 1.0
s = max(hls.s, MIN_SATURATION)
l = min(max(hls.l, MIN_LIGHTNESS), MAX_LIGHTNESS)
return rgb_to_hex(*colorsys.hls_to_rgb(h, l, s))
def palette_is_bland(accents: list[str]) -> bool:
"""Return True when accent colors are too similar, clustered, or too desaturated."""
stats = [hex_to_hls(c) for c in accents]
hues = [hls.h for hls in stats]
sats = [hls.s for hls in stats]
raw_spread = max(hues) - min(hues)
hue_spread = min(raw_spread, 1.0 - raw_spread)
avg_sat = sum(sats) / len(sats)
if hue_spread < HUE_THRESHOLD or avg_sat < SAT_THRESHOLD:
return True
mean_sin = sum(math.sin(2 * math.pi * h) for h in hues) / len(hues)
mean_cos = sum(math.cos(2 * math.pi * h) for h in hues) / len(hues)
hue_variance = 1.0 - math.sqrt(mean_sin ** 2 + mean_cos ** 2)
if hue_variance < HUE_VARIANCE_THRESHOLD:
return True
return False
def most_saturated(accents: list[str]) -> str:
return max(accents, key=lambda c: hex_to_hls(c).s)
def load_colors(filepath: str) -> list[str]:
try:
with open(filepath, 'r') as f:
with open(filepath) as f:
colors = [line.strip() for line in f if line.strip()]
except Exception as e:
print(f"Error reading colors: {e}")
sys.exit(1)
except OSError as exc:
sys.exit(f"Error reading '{filepath}': {exc}")
if len(colors) < 16:
print("Not enough colors found in file.")
sys.exit(1)
sys.exit(f"Expected ≥ 16 colors in '{filepath}', found {len(colors)}.")
# Check hue variance and saturation of accent colors (indices 1-6)
accents = colors[1:7]
hues, sats = get_hls_stats(accents)
return colors
if is_monotone_or_bland(hues, sats, hue_threshold=0.15, sat_threshold=0.35):
print("Detected monotone or bland palette. Generating vibrant complementary colors...")
# Pick the most saturated accent as the base
base_accent = accents[-1]
max_sat = 0
for i, sat in enumerate(sats):
if sat > max_sat:
max_sat = sat
base_accent = accents[i]
def save_colors(filepath: str, colors: list[str]) -> None:
try:
with open(filepath, "w") as f:
f.write("\n".join(colors) + "\n")
except OSError as exc:
sys.exit(f"Error writing '{filepath}': {exc}")
# Generate new colors with forced vibrancy
comp = shift_hue_and_saturate(base_accent, 0.5)
ana1 = shift_hue_and_saturate(base_accent, 0.083)
ana2 = shift_hue_and_saturate(base_accent, -0.083)
split1 = shift_hue_and_saturate(base_accent, 0.416)
split2 = shift_hue_and_saturate(base_accent, -0.416)
# The base accent itself also gets saturated if it was too bland
r, g, b = hex_to_rgb(base_accent)
h, l, s = colorsys.rgb_to_hls(r, g, b)
if s < 0.65:
base_vibrant = shift_hue_and_saturate(base_accent, 0.0)
else:
base_vibrant = base_accent
def main() -> None:
if len(sys.argv) < 2:
sys.exit("Usage: blob_color_fixer.py <path_to_wal_colors>")
new_accents = [base_vibrant, comp, ana1, ana2, split1, split2]
filepath = sys.argv[1]
colors = load_colors(filepath)
accents = colors[ACCENT_SLICE]
# Replace normal and bright accents
for i in range(6):
colors[i+1] = new_accents[i]
colors[i+9] = new_accents[i]
if not palette_is_bland(accents):
print("Palette is already vibrant and diverse — nothing to do.")
return
print("Monotone / bland palette detected. Generating vibrant harmony…")
base = most_saturated(accents)
new_accents = [vibrant_shift(base, shift) for shift in HUE_SHIFTS]
for i, color in enumerate(new_accents):
colors[i + 1] = color
colors[i + 1 + BRIGHT_OFFSET] = color
save_colors(filepath, colors)
print("Done — colors enhanced and written back to file.")
with open(filepath, 'w') as f:
for color in colors:
f.write(f"{color}\n")
print("Colors successfully enhanced.")
else:
print("Palette is already vibrant and diverse enough.")
if __name__ == "__main__":
main()
+34 -26
View File
@@ -8,32 +8,9 @@ mkdir -p "$WALLPAPER_DIR"
mkdir -p "$THEME_DIR/backgrounds"
if [ -z "$1" ]; then
echo "Available wallpapers in $WALLPAPER_DIR:"
# Read files into an array
mapfile -t files < <(ls -1 "$WALLPAPER_DIR" 2>/dev/null)
if [ ${#files[@]} -eq 0 ]; then
echo "No wallpapers found."
exit 1
fi
# Print the menu
for i in "${!files[@]}"; do
printf "%3d. %s\n" "$((i+1))" "${files[$i]}"
done
echo ""
read -p "Select a wallpaper number (1-${#files[@]}): " selection
# Validate selection
if ! [[ "$selection" =~ ^[0-9]+$ ]] || [ "$selection" -lt 1 ] || [ "$selection" -gt "${#files[@]}" ]; then
echo "Invalid selection."
exit 1
fi
SELECTED_FILE="${files[$((selection-1))]}"
IMAGE_PATH=$(realpath "$WALLPAPER_DIR/$SELECTED_FILE")
# Use walker dmenu for GUI selection
omarchy-launch-walker -m menus:blobBackgroundSelector --width 800 --minheight 400 -p "Select Wallpaper…"
exit 0
else
# Check if the argument is a file in the wallpapers directory
if [ -f "$WALLPAPER_DIR/$1" ]; then
@@ -84,8 +61,39 @@ color14 = "$(sed -n '15p' ~/.cache/wal/colors)"
color15 = "$(sed -n '16p' ~/.cache/wal/colors)"
EOF
# Write neovim.lua to satisfy LazyVim's theme symlink requirement
cat <<EOF > "$THEME_DIR/neovim.lua"
return {
{
"LazyVim/LazyVim",
opts = {
colorscheme = "tokyonight",
},
},
}
EOF
# Apply the Blob-Dynamic theme
# omarchy-theme-set manages the background and reloads waybar and AGS
omarchy-theme-set "blob-dynamic"
# If it's a GIF, override swaybg with awww (swww replacement)
if [[ "${IMAGE_PATH,,}" == *.gif ]]; then
echo "GIF detected, switching to awww..."
pkill -x swaybg
# Start awww daemon if not running
if ! pgrep -x awww-daemon >/dev/null; then
awww-daemon >/dev/null 2>&1 &
sleep 1
fi
awww img "$IMAGE_PATH"
else
# Ensure awww is stopped for static wallpapers so swaybg can render them
if pgrep -x awww-daemon >/dev/null; then
pkill -x awww-daemon
fi
fi
echo "Wallpaper and dynamic theme applied successfully: $IMAGE_PATH"
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.5 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 475 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 663 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 347 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 327 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 502 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB