- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
127 lines
4.1 KiB
Python
127 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""Locate the plugin panel in a screenshot by its accent border.
|
|
|
|
Trimming to "any accent-coloured pixel" is not enough: the compositor draws the
|
|
focused window's border in the same accent, so a trim swallows whatever sits
|
|
behind the panel. The panel is instead found as a filled rectangle -- four
|
|
borders enclosing a region -- and the largest such rectangle is returned.
|
|
|
|
The accent itself is read out of the image rather than hardcoded. It is a theme
|
|
colour, so a hardcoded value silently stops matching the day the operator
|
|
changes themes -- which is exactly how this last failed, with every shot
|
|
reported as "could not locate the panel border" and no hint as to why.
|
|
|
|
find_panel.py <image> [--accent RRGGBB] -> "WxH+X+Y" on stdout
|
|
"""
|
|
import sys
|
|
from collections import Counter
|
|
from PIL import Image
|
|
|
|
# Enough slack for antialiasing and the border's own gradient, not enough to
|
|
# merge two distinct theme colours.
|
|
TOLERANCE = 26
|
|
# The panel is wider than any window border is thick.
|
|
MIN_RUN = 260
|
|
|
|
|
|
def close(px, target, tol=TOLERANCE):
|
|
return all(abs(px[i] - target[i]) <= tol for i in range(3))
|
|
|
|
|
|
def candidate_accents(img, limit=6):
|
|
"""Saturated colours in the image, most common first.
|
|
|
|
The panel border is a solid run of one theme colour, so it is always among
|
|
the most common saturated pixels. Returning several candidates means a
|
|
highlighted row or a colourful wallpaper cannot derail the search.
|
|
"""
|
|
w, h = img.size
|
|
px = img.load()
|
|
counts = Counter()
|
|
for y in range(0, h, 2):
|
|
for x in range(0, w, 2):
|
|
r, g, b = px[x, y]
|
|
if max(r, g, b) > 110 and (max(r, g, b) - min(r, g, b)) > 60:
|
|
counts[(r, g, b)] += 1
|
|
|
|
accents = []
|
|
for colour, _ in counts.most_common():
|
|
# Skip anything already covered by a candidate we kept.
|
|
if any(close(colour, kept) for kept in accents):
|
|
continue
|
|
accents.append(colour)
|
|
if len(accents) >= limit:
|
|
break
|
|
return accents
|
|
|
|
|
|
def find_box(img, accent):
|
|
w, h = img.size
|
|
px = img.load()
|
|
|
|
# Rows that contain a long horizontal run of accent pixels are candidate
|
|
# top/bottom borders.
|
|
runs = {} # row -> (start, end) of its longest accent run
|
|
for y in range(h):
|
|
best = (0, 0, 0)
|
|
run_start, run_len = None, 0
|
|
for x in range(w):
|
|
if close(px[x, y], accent):
|
|
if run_start is None:
|
|
run_start = x
|
|
run_len += 1
|
|
else:
|
|
if run_len > best[0]:
|
|
best = (run_len, run_start, x - 1)
|
|
run_start, run_len = None, 0
|
|
if run_len > best[0]:
|
|
best = (run_len, run_start, w - 1)
|
|
if best[0] >= MIN_RUN:
|
|
runs[y] = (best[1], best[2])
|
|
|
|
if not runs:
|
|
return None
|
|
|
|
# Pair each top edge with the furthest bottom edge sharing its extent, and
|
|
# keep the tallest rectangle: that is the panel, not a window border.
|
|
best_box = None
|
|
ys = sorted(runs)
|
|
for i, top in enumerate(ys):
|
|
x0, x1 = runs[top]
|
|
for bottom in reversed(ys[i + 1:]):
|
|
bx0, bx1 = runs[bottom]
|
|
if abs(bx0 - x0) <= 4 and abs(bx1 - x1) <= 4 and bottom - top > 120:
|
|
box = (x1 - x0 + 1, bottom - top + 1, x0, top)
|
|
if best_box is None or box[0] * box[1] > best_box[0] * best_box[1]:
|
|
best_box = box
|
|
break
|
|
|
|
return best_box
|
|
|
|
|
|
def main():
|
|
path = sys.argv[1]
|
|
img = Image.open(path).convert("RGB")
|
|
|
|
if "--accent" in sys.argv:
|
|
h = sys.argv[sys.argv.index("--accent") + 1].lstrip("#")
|
|
accents = [tuple(int(h[i:i+2], 16) for i in (0, 2, 4))]
|
|
else:
|
|
accents = candidate_accents(img)
|
|
|
|
if not accents:
|
|
sys.exit("no saturated colour in the image to use as an accent")
|
|
|
|
for accent in accents:
|
|
box = find_box(img, accent)
|
|
if box:
|
|
print("%dx%d+%d+%d" % box)
|
|
return
|
|
|
|
tried = ", ".join("#%02X%02X%02X" % a for a in accents)
|
|
sys.exit("no enclosed rectangle found (tried %s)" % tried)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|