Sync config from arch

- 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
This commit is contained in:
asepharyana
2026-09-23 15:19:12 +07:00
commit 1cdb82a76f
300 changed files with 78143 additions and 0 deletions
@@ -0,0 +1,80 @@
#!/usr/bin/env bash
# A stand-in for the Bitwarden CLI, used only to capture screenshots.
#
# The plugin resolves `bw` from PATH, so putting this earlier on PATH points it
# at a fixture vault instead of a real one. Nothing here talks to Bitwarden,
# reads a keyring, or touches the network -- which is the point: the README
# screenshots can show a populated vault without showing anyone's credentials.
set -uo pipefail
FIXTURES="$(dirname "$(readlink -f "$0")")/../fixtures.json"
j() { python3 -c "
import json,sys
d=json.load(open('$FIXTURES'))
sys.stdout.write(json.dumps(d[sys.argv[1]]))" "$1"; }
# The vault state the fixture reports. `unlocked` for the populated shots;
# `unauthenticated` drives the login screen, `locked` the unlock screen.
DEMO_STATUS="${QSBW_DEMO_STATUS:-unlocked}"
case "${1:-}" in
--version) echo "2026.2.0-demo" ;;
status) python3 -c "
import json,sys
d=json.load(open('$FIXTURES'))
st=dict(d['status']); st['status']=sys.argv[1]
if sys.argv[1]=='unauthenticated':
st['userEmail']=''; st['userId']=''
sys.stdout.write(json.dumps(st))" "$DEMO_STATUS" ;;
# The new generator reaches for `bw serve` first. Exiting immediately is the
# bind-failure path, which is exactly the fallback we want exercised here.
serve) exit 1 ;;
sync) echo "Syncing complete." ;;
lock) echo "Your vault is locked." ;;
unlock) echo "demo-session-token-not-real" ;;
generate)
# Roughly honour --passphrase so the generator screenshot looks right.
if [[ " $* " == *" --passphrase "* ]]; then echo "Correct-Horse-Battery-Staple"
else echo "Xq7X2mFk9TbW4e"; fi ;;
list)
case "${2:-}" in
items) j items ;;
folders) j folders ;;
organizations) j organizations ;;
*) echo "[]" ;;
esac ;;
get)
case "${2:-}" in
totp) echo "418 623" | tr -d ' ' ;;
password) echo "placeholder" ;;
# Attachment bytes never come from the fixture file -- the panel only
# needs a file to appear where it asked for one.
attachment)
out=""
while [ $# -gt 0 ]; do
if [ "$1" = "--output" ]; then out="${2:-}"; fi
shift
done
[ -n "$out" ] || exit 1
printf 'placeholder attachment, not real vault data\n' > "$out"
echo "Saved $out" ;;
item) python3 -c "
import json,sys
d=json.load(open('$FIXTURES'))
want=sys.argv[1]
for it in d['items']:
if it['id']==want: print(json.dumps(it)); break
else: print(json.dumps(d['items'][0]))" "${3:-i1}" ;;
*) echo "{}" ;;
esac ;;
send)
case "${2:-}" in
list) j sends ;;
create) echo '{"object":"send","id":"s3","name":"New Send","type":0,"accessUrl":"https://vault.bitwarden.com/#/send/demo3","accessCount":0,"maxAccessCount":null,"deletionDate":"2026-08-28T10:00:00.000Z","passwordSet":false,"disabled":false,"text":{"text":"placeholder","hidden":false}}' ;;
delete) echo "Send deleted." ;;
*) echo "[]" ;;
esac ;;
encode) cat ;;
create|edit|delete) echo '{"object":"item","id":"new"}' ;;
*) echo "{}" ;;
esac
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# A stand-in for libsecret's secret-tool, used only to capture screenshots.
#
# Without this the fixture shell talks to the real OS keyring: it would read
# the operator's actual Bitwarden session into the demo panel, and the shots
# would depend on whether that entry happened to exist. Neither belongs in a
# screenshot harness, so the keyring is faked too.
#
# A session lookup succeeds with an obvious placeholder -- the panel needs a
# non-empty session before it will load any items -- and every other lookup
# reports "not stored", so PIN and fingerprint unlock show as unconfigured.
#
# The placeholder carries the running boot id, because that is the shape the
# panel now demands of a remembered session: a token from another boot is
# refused and cleared. Without the prefix the fixture shell would come up on
# the lock screen and there would be nothing to photograph.
set -uo pipefail
account=""
prev=""
for arg in "$@"; do
[[ "$prev" == "account" ]] && account="$arg"
prev="$arg"
done
case "${1:-}" in
lookup)
if [[ "$account" == "session" ]]; then
echo "$(cat /proc/sys/kernel/random/boot_id) demo-session-token-not-real"
exit 0
fi
exit 1 ;;
store) cat >/dev/null; exit 0 ;;
clear) exit 0 ;;
*) exit 1 ;;
esac
@@ -0,0 +1,213 @@
#!/usr/bin/env bash
# Capture README screenshots against a fixture vault.
#
# Restarts the Omarchy shell with demo/bin ahead of it on PATH, so the plugin
# resolves `bw` to the shim and shows made-up data. Your real vault is never
# read, and the shell is restored on the way out -- including if this script
# is interrupted.
#
# ./demo/capture.sh [output-dir] (default: docs/screenshots)
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
OUT="${1:-$REPO/docs/screenshots}"
IPC=(qs -p /usr/share/omarchy/shell/shell.qml ipc call io.github.elevate08.qs-bitwarden-cli)
for tool in grim magick wtype hyprctl quickshell /usr/bin/python3; do
command -v "$tool" >/dev/null || { echo "missing required tool: $tool" >&2; exit 1; }
done
mkdir -p "$OUT"
restore() {
echo "restoring the real shell..."
# The fixture vault's SSH key gets projected to the same directory the real
# one uses. The real shell rewrites its own keys on the next load but will
# not remove a file it never wrote, so a demo key would sit there for good.
rm -f "${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub"
pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true
sleep 1
omarchy restart shell >/dev/null 2>&1 || true
}
trap restore EXIT INT TERM
# start_shell <vault-state>
#
# The fixture shell is restarted per vault state rather than driven between
# them: the panel reads `bw status` once on open, so the logged-out screen
# cannot be reached from a running unlocked instance.
start_shell() {
echo "starting shell with the fixture vault ($1)..."
pkill -f "quickshell -n -p /usr/share/omarchy/shell" 2>/dev/null || true
sleep 1
PATH="$REPO/demo/bin:$PATH" QSBW_DEMO_STATUS="$1" \
nohup quickshell -n -p /usr/share/omarchy/shell >/dev/null 2>&1 &
sleep 6
# Park the pointer so hover states and tooltips stay out of the shots.
hyprctl dispatch movecursor 100 100 >/dev/null 2>&1 || true
}
# Crop to the panel itself rather than a fixed box. The panel resizes with its
# content, and -- more importantly -- a loose crop would put whatever is behind
# it (windows, filenames, terminal scrollback) into the published image.
shot() { # shot <name>
sleep 1
grim "$OUT/.raw.png"
local box err
# Keep the locator's own reason. Swallowing it turned a theme change into
# six identical "could not locate the panel border" lines and no clue why.
if box="$(/usr/bin/python3 "$REPO/demo/find_panel.py" "$OUT/.raw.png" 2>/tmp/find_panel.err)"; then
magick "$OUT/.raw.png" -crop "$box" +repage "$OUT/$1.png"
echo " wrote $1.png ($box)"
else
err="$(cat /tmp/find_panel.err)"
echo " SKIPPED $1: ${err:-could not locate the panel border}" >&2
fi
rm -f /tmp/find_panel.err
if [ -n "${QSBW_KEEP_RAW:-}" ]; then mv -f "$OUT/.raw.png" "$OUT/.raw-$1.png" 2>/dev/null || true
else rm -f "$OUT/.raw.png"; fi
}
# open_detail <search text>
#
# Narrows the list to one item and opens it. Typing the name is steadier than
# counting Down presses: the list is sorted favourites-first and then by name,
# so an added fixture would silently shift every offset.
#
# The Down is what hands focus back from the search field to the key catcher --
# with one result it clamps to the only row -- and `e` on the list opens the
# detail. (`e` again, on the detail, opens the form.)
open_detail() {
wtype "/" 2>/dev/null; sleep 1
wtype "$1" 2>/dev/null; sleep 2
wtype -k Down 2>/dev/null; sleep 1
wtype "e" 2>/dev/null; sleep 2
}
# Back to an empty list from wherever open_detail left us.
clear_search() {
wtype -k Escape 2>/dev/null; sleep 1
wtype "/" 2>/dev/null; sleep 1
wtype -M ctrl -k a -m ctrl 2>/dev/null
wtype -k BackSpace 2>/dev/null; sleep 1
wtype -k Down 2>/dev/null; sleep 1
}
# --- SSH signing approval ---------------------------------------------------
#
# The approval screen exists only while a real signing request is waiting, so
# it cannot be navigated to -- it has to be raised. `ssh-add -T` asks the agent
# to sign one challenge with one key and nothing else, which is the smallest
# request that produces this prompt.
#
# Everything here is the fixture vault: the key is the throwaway pair in
# fixtures.json, and the socket belongs to the fixture shell started above.
# The request is denied rather than approved, so no signature is ever made.
capture_ssh_approval() {
local sock="${XDG_RUNTIME_DIR:-/run/user/$(id -u)}/qs-bitwarden-cli/ssh-agent.sock"
local pub="${XDG_DATA_HOME:-$HOME/.local/share}/qs-bitwarden-cli/ssh/Demo Deploy Key.pub"
# The helper starts with the panel and projects its public keys after the
# vault loads, so wait for the file rather than guessing at a delay.
local waited=0
while [ ! -S "$sock" ] || [ ! -f "$pub" ]; do
sleep 1; waited=$((waited + 1))
if [ "$waited" -ge 30 ]; then
echo " skipped 13-ssh-approval: no agent socket or projected key after ${waited}s" >&2
echo " (is 'Act as your SSH agent' enabled in shell.json?)" >&2
return 0
fi
done
"${IPC[@]}" open >/dev/null 2>&1; sleep 2
# In the background: it blocks until the prompt is answered, which is the
# point -- the prompt has to still be on screen when the shot is taken.
SSH_AUTH_SOCK="$sock" ssh-add -T "$pub" >/dev/null 2>&1 &
local asker=$!
sleep "${QSBW_SSH_SETTLE:-4}"
shot 13-ssh-approval
# Deny it. Escape is the approval screen's own deny, so nothing is signed.
wtype -k Escape 2>/dev/null; sleep 1
wait "$asker" 2>/dev/null || true
"${IPC[@]}" close >/dev/null 2>&1 || true
}
# QSBW_ONLY_SSH=1 captures the approval shot alone. It is the only shot that
# depends on timing rather than on a keystroke, so it is the one that gets
# iterated on, and re-running the whole sequence to retake it costs a minute
# and five shells.
if [ -n "${QSBW_ONLY_SSH:-}" ]; then
start_shell unlocked
capture_ssh_approval
echo "done -> $OUT"
exit 0
fi
# --- logged out -------------------------------------------------------------
# No setup shot. The wizard appears only while a required tool is missing, and
# it watches for the install and moves on by itself the moment one lands. The
# fixture environment has every dependency -- that is what makes the rest of
# these shots work -- so the screen correctly advances straight past itself.
# Photographing it means deliberately hiding `jq` or `bw` from the shell's
# PATH, which breaks the vault reads every other shot depends on.
start_shell unauthenticated
"${IPC[@]}" open >/dev/null 2>&1; sleep 4
shot 12-login
"${IPC[@]}" close >/dev/null 2>&1 || true
# --- locked -----------------------------------------------------------------
start_shell locked
"${IPC[@]}" open >/dev/null 2>&1; sleep 4
shot 11-locked
"${IPC[@]}" close >/dev/null 2>&1 || true
# --- unlocked, populated vault ----------------------------------------------
start_shell unlocked
"${IPC[@]}" open >/dev/null 2>&1; sleep 4
shot 01-vault-list
# One of each item type the panel draws differently. A login has credentials
# and a TOTP; a card and an identity have the field blocks added in 1.7.0, and
# an identity is the one that shows the address as a single copyable block.
open_detail "GitHub"
shot 02-login-detail
wtype "e" 2>/dev/null; sleep 2
shot 03-edit-item
wtype -k Escape 2>/dev/null; sleep 1
clear_search
open_detail "Demo Card"
shot 04-card-detail
clear_search
open_detail "Dana Demo"
shot 05-identity-detail
clear_search
wtype "f" 2>/dev/null; sleep 2
shot 06-folder-drawer
wtype -k Escape 2>/dev/null; sleep 1
wtype "t" 2>/dev/null; sleep 2
shot 07-type-filter
wtype -k Escape 2>/dev/null; sleep 1
wtype "g" 2>/dev/null; sleep 5
shot 08-generator
wtype -k Escape 2>/dev/null; sleep 1
wtype -M alt -k s -m alt 2>/dev/null; sleep 3
shot 09-sends
wtype -k Escape 2>/dev/null; sleep 1
wtype -M alt -k comma -m alt 2>/dev/null; sleep 3
shot 10-settings
"${IPC[@]}" close >/dev/null 2>&1 || true
capture_ssh_approval
echo "done -> $OUT"
@@ -0,0 +1,198 @@
#!/usr/bin/env bash
# Compose preview.png from the captured screenshots.
#
# The preview used to be assembled by hand, which meant adding a panel to it
# was an image-editing job and nobody could tell which screenshots a given
# preview.png was built from. This script is the recipe: run demo/capture.sh
# first, then this.
#
# ./demo/compose-preview.sh [screenshot-dir] [output]
# ./demo/compose-preview.sh --badge-only (reuse the cached base)
#
# The base -- panels and title, everything that comes from screenshots -- is
# cached beside the shots. Only the callout changes when a release wants a
# different phrase, and rebuilding six panels to redraw one banner made trying
# wordings slower than it needed to be.
#
# Everything it reads is fixture data by construction -- capture.sh only ever
# runs against demo/bin/bw and demo/fixtures.json -- so nothing here can put a
# real vault into a published image.
set -euo pipefail
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BADGE_ONLY=0
args=()
for a in "$@"; do
case "$a" in
--badge-only) BADGE_ONLY=1 ;;
*) args+=("$a") ;;
esac
done
SHOTS="${args[0]:-$REPO/docs/screenshots}"
OUT="${args[1]:-$REPO/preview.png}"
BASE="$SHOTS/.preview-base.png"
command -v magick >/dev/null || { echo "missing required tool: magick" >&2; exit 1; }
# Sampled from the preview this replaces, so the rebuild is the same design
# rather than an approximation of it.
BG='#060400'
ACCENT='#F2A502'
TITLE='Bitwarden Vault Plugin'
# ImageMagick's own name for the face, which is not the fontconfig family
# name. `magick -list font` is the list it will accept; override if your
# machine names it differently.
FONT="${QSBW_PREVIEW_FONT:-CaskaydiaMono-NF-Bold}"
FONT_BODY="${QSBW_PREVIEW_FONT_BODY:-CaskaydiaMono-NF-Regular}"
# The callout that fills the empty corner under the first column. It is the
# one element here that is not a screenshot, so it borrows the panels' own
# vocabulary -- same accent, same square border, same black ground -- and
# earns its place by naming what the release added.
# Set either from the environment to try a phrase without touching the file:
# QSBW_BADGE_TITLE='SSH Agent Support' ./demo/compose-preview.sh --badge-only
BADGE_KICKER="${QSBW_BADGE_KICKER:-NEW}"
BADGE_TITLE="${QSBW_BADGE_TITLE:-Cards & Identities}"
# Filled with the accent and lettered in the page's own black, rather than
# outlined like the panels. A seventh accent-bordered rectangle read as one
# more screenshot; this cannot be mistaken for one.
BADGE_FG="$BG"
BADGE_BG="$ACCENT"
# Width kept clear at the right of the title band for the callout. The page
# title is centred in what is left rather than in the whole width, so the gap
# between the two does not depend on how long the badge phrase happens to be
# -- and a badge that outgrows this is told to shrink rather than silently
# shunting into the title.
BADGE_ZONE=640
GAP=28 # between panels, and around the whole thing
TITLE_SIZE=96
# Three columns, top to bottom. The vault list carries the folder drawer
# because that shot shows both at once; the plain list would be redundant
# beside it.
# A selection, not the whole set. capture.sh takes a shot of every screen so
# the documentation has one; this picks the handful that say what the plugin
# is at a glance, and leaves out the ones that look like every other panel's
# equivalent (login, setup, locked, the filter drawers).
#
# Grouped to keep the three columns near the same height -- the page is as tall
# as its tallest column, and an unbalanced one leaves a corner of empty black.
COL1=(01-vault-list 08-generator)
COL2=(04-card-detail 09-sends)
COL3=(10-settings 13-ssh-approval)
# Not `magick ... | grep -q`: grep exits on the first match, magick takes a
# SIGPIPE for it, and `set -o pipefail` reports the successful match as a
# failed pipeline.
grep -qx " Font: $FONT" <<<"$(magick -list font)" || {
echo "magick does not know the font '$FONT'." >&2
echo "Pick one from \`magick -list font\` and set QSBW_PREVIEW_FONT." >&2
exit 1
}
column() { # column <name>...
local files=() f
for f in "$@"; do
if [ -f "$SHOTS/$f.png" ]; then files+=("$SHOTS/$f.png")
else echo " missing $f.png, leaving it out" >&2; fi
done
[ ${#files[@]} -gt 0 ] || { echo "no screenshots for a column" >&2; exit 1; }
# -background so the gap between stacked panels is the page colour, not white.
magick "${files[@]}" -background "$BG" -gravity north -splice "0x${GAP}" \
-append -chop "0x${GAP}" miff:-
}
work="$(mktemp -d)"; trap 'rm -rf "$work"' EXIT
if [ "$BADGE_ONLY" = 1 ]; then
[ -f "$BASE" ] || { echo "no cached base at $BASE; run without --badge-only first" >&2; exit 1; }
cp "$BASE" "$work/page.png"
else
column "${COL1[@]}" > "$work/c1.miff"
column "${COL2[@]}" > "$work/c2.miff"
column "${COL3[@]}" > "$work/c3.miff"
# Columns side by side, each hung from the top. The gravity has to be north
# for the append itself: +append centres shorter images vertically unless told
# otherwise, which left the third column floating in the middle of the page.
magick "$work/c1.miff" "$work/c2.miff" "$work/c3.miff" \
-background "$BG" -gravity west -splice "${GAP}x0" \
-gravity north +append -chop "${GAP}x0" "$work/panels.png"
magick "$work/panels.png" \
-background "$BG" \
-gravity south -splice "0x${GAP}" \
-gravity west -splice "${GAP}x0" \
-gravity east -splice "${GAP}x0" \
"$work/framed.png"
# The title as its own image, the width of the page, rather than annotated on
# to it. `-annotate` with a gravity places from an origin this composition
# keeps moving, and the text ended up off the left edge; a label of a known
# size cannot land anywhere but where it is appended.
WIDTH="$(magick identify -format '%w' "$work/framed.png")"
magick -background "$BG" -fill "$ACCENT" -font "$FONT" \
-pointsize "$TITLE_SIZE" label:"$TITLE" "$work/title-text.png"
magick "$work/title-text.png" -background "$BG" -gravity center \
-extent "$((WIDTH - BADGE_ZONE))x$((TITLE_SIZE * 2))" \
-gravity east -splice "${BADGE_ZONE}x0" "$work/title.png"
# -depth 8: the label pushes the pipeline to 16-bit, which triples the file
# size of a flat-colour image for nothing a viewer can see.
magick "$work/title.png" "$work/framed.png" -background "$BG" -append \
"$work/page.png"
cp "$work/page.png" "$BASE"
# The callout straddles the bottom of the title band, which a badge-only run
# has no way to measure -- the pieces are gone by then. Record it.
magick identify -format '%h' "$work/title.png" > "$BASE.anchor"
fi
[ -f "$BASE.anchor" ] || { echo "no anchor beside $BASE; rebuild the base" >&2; exit 1; }
TITLE_H="$(cat "$BASE.anchor")"
# --- the callout ------------------------------------------------------------
#
# Drawn as its own image and composited, rather than annotated on to the page:
# it overlaps the panel above it by design, and a composite is the only way to
# put something over a region that already has pixels in it.
# No -size here: `label:` with an explicit size scales the text to fill it,
# which turned a 34pt kicker into a 500pt "NEW" across the whole badge. The
# point size governs, and the padding is added afterwards.
magick -background "$BADGE_BG" -fill "$BADGE_FG" \
-font "$FONT" -pointsize 26 -interword-spacing 10 \
label:"$BADGE_KICKER" "$work/kicker.png"
magick -background "$BADGE_BG" -fill "$BADGE_FG" \
-font "$FONT" -pointsize 44 label:"$BADGE_TITLE" "$work/badge-title.png"
# A solid block, not an outline: the panels are all accent-bordered rectangles
# on black, so one more of those disappears among them. Inverting it -- accent
# ground, black letters -- is what makes it read as a label on the image
# rather than a part of it.
magick "$work/kicker.png" "$work/badge-title.png" \
-background "$BADGE_BG" -gravity west -append \
-bordercolor "$BADGE_BG" -border 22 \
"$work/badge.png"
# Top right: the title is centred, so the corner beside it is empty, and
# hanging the badge below the band's edge lets it clip the first panel of the
# third column -- which is what keeps it looking placed rather than laid out.
PAGE_W="$(magick identify -format '%w' "$work/page.png")"
BADGE_BW="$(magick identify -format '%w' "$work/badge.png")"
BADGE_BH="$(magick identify -format '%h' "$work/badge.png")"
BADGE_X=$((PAGE_W - BADGE_BW - GAP))
# Centred in the title band rather than overlapping anything. The two
# neighbours here are both load-bearing -- the page title to its left and the
# panel header below it -- and dipping into either one cost more than the
# overlap was worth.
BADGE_Y=$(((TITLE_H - BADGE_BH) / 2))
if [ "$BADGE_BW" -gt "$((BADGE_ZONE - GAP))" ]; then
echo " warning: the badge is wider than the ${BADGE_ZONE}px reserved for it" >&2
echo " and will crowd the page title. Shorten the phrase or raise BADGE_ZONE." >&2
fi
magick "$work/page.png" "$work/badge.png" -geometry "+${BADGE_X}+${BADGE_Y}" \
-composite -depth 8 -strip "$OUT"
magick identify "$OUT"
@@ -0,0 +1,126 @@
#!/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()
@@ -0,0 +1,290 @@
{
"status": {
"serverUrl": "https://vault.bitwarden.com",
"lastSync": "2026-08-21T10:00:00.000Z",
"userEmail": "demo@example.com",
"userId": "00000000-0000-0000-0000-000000000000",
"status": "unlocked"
},
"folders": [
{
"object": "folder",
"id": "f-infra",
"name": "Infrastructure"
},
{
"object": "folder",
"id": "f-fin",
"name": "Finance"
},
{
"object": "folder",
"id": "f-social",
"name": "Social"
}
],
"organizations": [
{
"object": "organization",
"id": "org-acme",
"name": "Acme Corp",
"status": 2
}
],
"items": [
{
"object": "item",
"id": "i1",
"organizationId": null,
"folderId": "f-social",
"type": 1,
"name": "GitHub",
"favorite": true,
"login": {
"username": "demo-user",
"password": "hunter2-not-real",
"totp": "otpauth://totp/demo",
"uris": [
{
"uri": "https://github.com"
}
]
}
},
{
"object": "item",
"id": "i2",
"organizationId": null,
"folderId": "f-social",
"type": 1,
"name": "Reddit",
"favorite": false,
"login": {
"username": "demo-user",
"password": "placeholder",
"totp": null,
"uris": [
{
"uri": "https://reddit.com"
}
]
}
},
{
"object": "item",
"id": "i3",
"organizationId": "org-acme",
"folderId": "f-infra",
"type": 1,
"name": "Acme VPN",
"favorite": false,
"login": {
"username": "d.demo@example.com",
"password": "placeholder",
"totp": "otpauth://totp/demo",
"uris": [
{
"uri": "https://vpn.acme.example"
}
]
},
"attachments": [
{
"object": "attachment",
"id": "a3",
"fileName": "acme-vpn.ovpn",
"size": "8192",
"sizeName": "8 KB",
"url": "https://example.invalid/a3"
}
]
},
{
"object": "item",
"id": "i4",
"organizationId": null,
"folderId": "f-infra",
"type": 1,
"name": "Home Assistant",
"favorite": true,
"login": {
"username": "admin",
"password": "placeholder",
"totp": null,
"uris": [
{
"uri": "https://homeassistant.local:8123"
}
]
}
},
{
"object": "item",
"id": "i5",
"organizationId": null,
"folderId": "f-fin",
"type": 1,
"name": "Example Bank",
"favorite": false,
"login": {
"username": "demo-user",
"password": "placeholder",
"totp": "otpauth://totp/demo",
"uris": [
{
"uri": "https://bank.example.com"
}
]
}
},
{
"object": "item",
"id": "i6",
"organizationId": null,
"folderId": null,
"type": 2,
"name": "Recovery Codes",
"favorite": false,
"notes": "Placeholder note. No real data here.",
"secureNote": {
"type": 0
},
"attachments": [
{
"object": "attachment",
"id": "a1",
"fileName": "recovery-codes.txt",
"size": "412",
"sizeName": "412 B",
"url": "https://example.invalid/a1"
},
{
"object": "attachment",
"id": "a2",
"fileName": "backup-key.pem",
"size": "3204",
"sizeName": "3.13 KB",
"url": "https://example.invalid/a2"
}
]
},
{
"object": "item",
"id": "i7",
"organizationId": null,
"folderId": "f-fin",
"type": 3,
"name": "Demo Card",
"favorite": false,
"card": {
"cardholderName": "D. Demo",
"brand": "Visa",
"number": "4111111111111111",
"expMonth": "12",
"expYear": "2030",
"code": "123"
}
},
{
"object": "item",
"id": "i8",
"organizationId": "org-acme",
"folderId": "f-infra",
"type": 1,
"name": "Acme Grafana",
"favorite": false,
"login": {
"username": "d.demo@example.com",
"password": "placeholder",
"totp": null,
"uris": [
{
"uri": "https://grafana.acme.example"
}
]
}
},
{
"object": "item",
"id": "i9",
"organizationId": null,
"folderId": null,
"type": 4,
"name": "Dana Demo",
"favorite": false,
"identity": {
"title": "Ms",
"firstName": "Dana",
"middleName": "R",
"lastName": "Demo",
"username": "danademo",
"company": "Example Industries",
"email": "demo@example.com",
"phone": "+1 555 0100",
"ssn": "000-00-0000",
"passportNumber": "X1234567",
"licenseNumber": "D-0000-0000",
"address1": "1 Example Way",
"address2": "Suite 200",
"address3": "",
"city": "Springfield",
"state": "IL",
"postalCode": "62701",
"country": "US"
}
},
{
"object": "item",
"id": "i-ssh-1",
"organizationId": null,
"folderId": "f-infra",
"type": 5,
"name": "Demo Deploy Key",
"notes": null,
"favorite": false,
"collectionIds": [],
"reprompt": 0,
"revisionDate": "2026-08-20T10:00:00.000Z",
"creationDate": "2026-08-20T10:00:00.000Z",
"sshKey": {
"privateKey": "-----BEGIN OPENSSH PRIVATE KEY-----\nb3BlbnNzaC1rZXktdjEAAAAABG5vbmUAAAAEbm9uZQAAAAAAAAABAAAAMwAAAAtzc2gtZW\nQyNTUxOQAAACBGTamxk2fLE7NZekQoaoQkLjAbzaNDSJrO8clPlNnoXAAAAJhQ/dXxUP3V\n8QAAAAtzc2gtZWQyNTUxOQAAACBGTamxk2fLE7NZekQoaoQkLjAbzaNDSJrO8clPlNnoXA\nAAAEAqEDoJ+o48bC8qt9agrYLugGkX0IjZdM/iT5kK0Q0BGUZNqbGTZ8sTs1l6RChqhCQu\nMBvNo0NIms7xyU+U2ehcAAAAEGRlbW9AZXhhbXBsZS5jb20BAgMEBQ==\n-----END OPENSSH PRIVATE KEY-----\n",
"publicKey": "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEZNqbGTZ8sTs1l6RChqhCQuMBvNo0NIms7xyU+U2ehc demo@example.com",
"keyFingerprint": "SHA256:WJN+07USrkd8tFp3vVJBXAjwolWfskqqzl+UPp5GH30"
}
}
],
"sends": [
{
"object": "send",
"id": "s1",
"name": "Wifi password for guests",
"type": 0,
"accessUrl": "https://vault.bitwarden.com/#/send/demo1",
"accessCount": 1,
"maxAccessCount": 5,
"deletionDate": "2026-08-24T10:00:00.000Z",
"passwordSet": true,
"disabled": false,
"text": {
"text": "placeholder",
"hidden": true
}
},
{
"object": "send",
"id": "s2",
"name": "Onboarding checklist",
"type": 0,
"accessUrl": "https://vault.bitwarden.com/#/send/demo2",
"accessCount": 0,
"maxAccessCount": null,
"deletionDate": "2026-08-28T10:00:00.000Z",
"passwordSet": false,
"disabled": false,
"text": {
"text": "placeholder",
"hidden": false
}
}
]
}