Compare commits
5 Commits
85a06ae0f8
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| fc2c5e8d18 | |||
| 59e2d9de97 | |||
| 8d1be4712d | |||
| 320dc003c4 | |||
| 3195f61782 |
@@ -13,9 +13,38 @@
|
|||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
border: 2px solid alpha(@color4, 0.5);
|
border: 2px solid alpha(@color4, 0.5);
|
||||||
padding: 8px 15px;
|
padding: 8px 15px;
|
||||||
|
min-width: 300px;
|
||||||
font-family: "JetBrainsMono Nerd Font", sans-serif;
|
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 {
|
.media-btn {
|
||||||
color: @color5;
|
color: @color5;
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
@@ -27,8 +56,26 @@
|
|||||||
color: @color4;
|
color: @color4;
|
||||||
}
|
}
|
||||||
|
|
||||||
.media-text {
|
.media-progress {
|
||||||
color: @foreground;
|
min-height: 4px;
|
||||||
font-size: 14px;
|
padding: 0;
|
||||||
margin-left: 10px;
|
}
|
||||||
}
|
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -6,9 +6,26 @@ import { createPoll } from "ags/time"
|
|||||||
export default function Media(gdkmonitor: Gdk.Monitor) {
|
export default function Media(gdkmonitor: Gdk.Monitor) {
|
||||||
const { TOP, LEFT } = Astal.WindowAnchor
|
const { TOP, LEFT } = Astal.WindowAnchor
|
||||||
|
|
||||||
// Poll playerctl for metadata and status
|
const pollCmd = "playerctl metadata -f '{{title}}|||{{artist}}|||{{mpris:artUrl}}|||{{status}}|||{{mpris:length}}|||{{position}}' 2>/dev/null || echo 'No Media||||||Stopped|||0|||0'";
|
||||||
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 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 (
|
return (
|
||||||
<window
|
<window
|
||||||
@@ -20,30 +37,68 @@ export default function Media(gdkmonitor: Gdk.Monitor) {
|
|||||||
margin={20}
|
margin={20}
|
||||||
application={app}
|
application={app}
|
||||||
>
|
>
|
||||||
<box class="media-container" spacing={10}>
|
<box class="media-container" css="min-width: 350px;" spacing={15}>
|
||||||
<button
|
<box
|
||||||
class="media-btn"
|
class="media-cover"
|
||||||
onClicked={() => execAsync("playerctl previous").catch(print)}
|
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);')}
|
||||||
halign={Gtk.Align.CENTER}
|
/>
|
||||||
>
|
|
||||||
<label label="⏮" />
|
<box vertical class="media-info-box" valign={Gtk.Align.CENTER} hexpand>
|
||||||
</button>
|
<label
|
||||||
<button
|
class="media-title"
|
||||||
class="media-btn"
|
label={mediaState.as(s => s.title)}
|
||||||
onClicked={() => execAsync("playerctl play-pause").catch(print)}
|
halign={Gtk.Align.FILL}
|
||||||
halign={Gtk.Align.CENTER}
|
xalign={0}
|
||||||
>
|
truncate
|
||||||
<label label={statusIcon} />
|
maxWidthChars={35}
|
||||||
</button>
|
css="font-size: 15px;"
|
||||||
<button
|
/>
|
||||||
class="media-btn"
|
<label
|
||||||
onClicked={() => execAsync("playerctl next").catch(print)}
|
class="media-artist"
|
||||||
halign={Gtk.Align.CENTER}
|
label={mediaState.as(s => s.artist)}
|
||||||
>
|
halign={Gtk.Align.FILL}
|
||||||
<label label="⏭" />
|
xalign={0}
|
||||||
</button>
|
truncate
|
||||||
<label class="media-text" label={mediaInfo} />
|
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)}
|
||||||
|
>
|
||||||
|
<label label="" css="font-size: 18px;" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="media-btn"
|
||||||
|
onClicked={() => execAsync("playerctl play-pause").catch(print)}
|
||||||
|
>
|
||||||
|
<label label={mediaState.as(s => s.status === "Playing" ? "" : "")} css="font-size: 22px;" />
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class="media-btn"
|
||||||
|
onClicked={() => execAsync("playerctl next").catch(print)}
|
||||||
|
>
|
||||||
|
<label label="" css="font-size: 18px;" />
|
||||||
|
</button>
|
||||||
|
</box>
|
||||||
|
</box>
|
||||||
</box>
|
</box>
|
||||||
</window>
|
</window>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 79 KiB |
@@ -1,107 +1,133 @@
|
|||||||
#!/usr/bin/env python3
|
#!/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 sys
|
||||||
|
import math
|
||||||
import colorsys
|
import colorsys
|
||||||
|
from dataclasses import dataclass
|
||||||
|
|
||||||
def hex_to_rgb(hex_str):
|
MIN_SATURATION = 0.65
|
||||||
hex_str = hex_str.lstrip('#')
|
MIN_LIGHTNESS = 0.40
|
||||||
return tuple(int(hex_str[i:i+2], 16) / 255.0 for i in (0, 2, 4))
|
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):
|
HUE_SHIFTS: list[float] = [
|
||||||
return '#{:02x}{:02x}{:02x}'.format(int(r * 255), int(g * 255), int(b * 255))
|
0.0,
|
||||||
|
0.5,
|
||||||
|
0.083,
|
||||||
|
-0.083,
|
||||||
|
0.416,
|
||||||
|
-0.416,
|
||||||
|
]
|
||||||
|
|
||||||
def shift_hue_and_saturate(hex_str, shift_amount):
|
@dataclass(frozen=True)
|
||||||
r, g, b = hex_to_rgb(hex_str)
|
class HLS:
|
||||||
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
h: float
|
||||||
h = (h + shift_amount) % 1.0
|
l: float
|
||||||
s = max(s, 0.65) # Force vibrancy
|
s: float
|
||||||
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)
|
|
||||||
|
|
||||||
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):
|
def hex_to_rgb(hex_str: str) -> tuple[float, float, float]:
|
||||||
if not hues or not sats: return False
|
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)
|
|
||||||
|
|
||||||
return hue_diff < hue_threshold or avg_s < sat_threshold
|
|
||||||
|
|
||||||
def main():
|
|
||||||
if len(sys.argv) < 2:
|
def rgb_to_hex(r: float, g: float, b: float) -> str:
|
||||||
print("Usage: python3 blob_color_fixer.py <path_to_wal_colors>")
|
return "#{:02x}{:02x}{:02x}".format(int(r * 255), int(g * 255), int(b * 255))
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
filepath = sys.argv[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)
|
||||||
|
|
||||||
|
|
||||||
|
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:
|
try:
|
||||||
with open(filepath, 'r') as f:
|
with open(filepath) as f:
|
||||||
colors = [line.strip() for line in f if line.strip()]
|
colors = [line.strip() for line in f if line.strip()]
|
||||||
except Exception as e:
|
except OSError as exc:
|
||||||
print(f"Error reading colors: {e}")
|
sys.exit(f"Error reading '{filepath}': {exc}")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if len(colors) < 16:
|
if len(colors) < 16:
|
||||||
print("Not enough colors found in file.")
|
sys.exit(f"Expected ≥ 16 colors in '{filepath}', found {len(colors)}.")
|
||||||
sys.exit(1)
|
|
||||||
|
return colors
|
||||||
# Check hue variance and saturation of accent colors (indices 1-6)
|
|
||||||
accents = colors[1:7]
|
|
||||||
hues, sats = get_hls_stats(accents)
|
def save_colors(filepath: str, colors: list[str]) -> None:
|
||||||
|
try:
|
||||||
if is_monotone_or_bland(hues, sats, hue_threshold=0.15, sat_threshold=0.35):
|
with open(filepath, "w") as f:
|
||||||
print("Detected monotone or bland palette. Generating vibrant complementary colors...")
|
f.write("\n".join(colors) + "\n")
|
||||||
|
except OSError as exc:
|
||||||
# Pick the most saturated accent as the base
|
sys.exit(f"Error writing '{filepath}': {exc}")
|
||||||
base_accent = accents[-1]
|
|
||||||
max_sat = 0
|
|
||||||
for i, sat in enumerate(sats):
|
def main() -> None:
|
||||||
if sat > max_sat:
|
if len(sys.argv) < 2:
|
||||||
max_sat = sat
|
sys.exit("Usage: blob_color_fixer.py <path_to_wal_colors>")
|
||||||
base_accent = accents[i]
|
|
||||||
|
filepath = sys.argv[1]
|
||||||
# Generate new colors with forced vibrancy
|
colors = load_colors(filepath)
|
||||||
comp = shift_hue_and_saturate(base_accent, 0.5)
|
accents = colors[ACCENT_SLICE]
|
||||||
ana1 = shift_hue_and_saturate(base_accent, 0.083)
|
|
||||||
ana2 = shift_hue_and_saturate(base_accent, -0.083)
|
if not palette_is_bland(accents):
|
||||||
split1 = shift_hue_and_saturate(base_accent, 0.416)
|
print("Palette is already vibrant and diverse — nothing to do.")
|
||||||
split2 = shift_hue_and_saturate(base_accent, -0.416)
|
return
|
||||||
|
|
||||||
# The base accent itself also gets saturated if it was too bland
|
print("Monotone / bland palette detected. Generating vibrant harmony…")
|
||||||
r, g, b = hex_to_rgb(base_accent)
|
|
||||||
h, l, s = colorsys.rgb_to_hls(r, g, b)
|
base = most_saturated(accents)
|
||||||
if s < 0.65:
|
new_accents = [vibrant_shift(base, shift) for shift in HUE_SHIFTS]
|
||||||
base_vibrant = shift_hue_and_saturate(base_accent, 0.0)
|
|
||||||
else:
|
for i, color in enumerate(new_accents):
|
||||||
base_vibrant = base_accent
|
colors[i + 1] = color
|
||||||
|
colors[i + 1 + BRIGHT_OFFSET] = color
|
||||||
new_accents = [base_vibrant, comp, ana1, ana2, split1, split2]
|
|
||||||
|
save_colors(filepath, colors)
|
||||||
# Replace normal and bright accents
|
print("Done — colors enhanced and written back to file.")
|
||||||
for i in range(6):
|
|
||||||
colors[i+1] = new_accents[i]
|
|
||||||
colors[i+9] = new_accents[i]
|
|
||||||
|
|
||||||
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__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
@@ -61,6 +61,18 @@ color14 = "$(sed -n '15p' ~/.cache/wal/colors)"
|
|||||||
color15 = "$(sed -n '16p' ~/.cache/wal/colors)"
|
color15 = "$(sed -n '16p' ~/.cache/wal/colors)"
|
||||||
EOF
|
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
|
# Apply the Blob-Dynamic theme
|
||||||
# omarchy-theme-set manages the background and reloads waybar and AGS
|
# omarchy-theme-set manages the background and reloads waybar and AGS
|
||||||
omarchy-theme-set "blob-dynamic"
|
omarchy-theme-set "blob-dynamic"
|
||||||
|
|||||||
|
After Width: | Height: | Size: 1.5 MiB |
|
After Width: | Height: | Size: 475 KiB |
|
After Width: | Height: | Size: 663 KiB |
|
After Width: | Height: | Size: 4.8 MiB |
|
After Width: | Height: | Size: 4.3 MiB |
|
After Width: | Height: | Size: 95 KiB |
|
After Width: | Height: | Size: 61 KiB |