diff --git a/site/content/_index.md b/site/content/_index.md
@@ -14,10 +14,6 @@ install_primary = "yay -S ommp"
install_secondary_label = "Anywhere else"
install_secondary = "cargo install ommp"
-shot_main = "img/01-main.png"
-shot_help = "img/02-help.png"
-shot_search = "img/03-search.png"
-
# Each feature is keyed to the accent colour its tab actually uses in
# src/ui/widgets/tab_bar.rs, so the page and the app agree.
[[extra.features]]
diff --git a/site/sqzass.toml b/site/sqzass.toml
@@ -1,10 +1,9 @@
title = "ommp"
description = "Oh My Music Player — a terminal music player written in Rust. No MPD, no daemon."
-# The whole URL, path included: sqzass splits this into an origin and a base
-# path, and prefixes every page and asset URL with the path. Serving from a
-# repository subpath needs nothing else.
-base_url = "https://sqzer-x.github.io/ommp"
+# Custom domain, so there is no repository subpath to prefix. static/CNAME is
+# copied through verbatim and is what tells GitHub Pages to serve it here.
+base_url = "https://ommp.sqzer.com"
[search]
# One page. A full-text index nobody fetches is just bytes.
diff --git a/site/static/CNAME b/site/static/CNAME
@@ -0,0 +1 @@
+ommp.sqzer.com
diff --git a/site/static/css/main.css b/site/static/css/main.css
@@ -160,6 +160,7 @@ a:hover { color: var(--cyan); }
margin: clamp(3rem, 7vw, 5rem) auto 0;
}
.shot-hero img { box-shadow: 0 2rem 5rem -1.5rem #00000099; }
+.shot-detail img { box-shadow: 0 1.5rem 4rem -2rem #00000099; }
/* ── Bands ────────────────────────────────────────────────────────── */
@@ -216,11 +217,18 @@ a:hover { color: var(--cyan); }
/* ── Split sections ───────────────────────────────────────────────── */
-.split { display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: center; }
+/* Copy beside its list, and the screenshot underneath at the full width of the
+ wrap. These shots are close-ups of a modal whose contents are a fixed number
+ of characters — squeezed into a column beside the copy they are simply too
+ small to read, which defeats the point of showing them. */
+.split {
+ display: grid; gap: clamp(2rem, 5vw, 3.5rem); align-items: start;
+ margin-bottom: clamp(2.25rem, 4vw, 3rem);
+}
@media (min-width: 56rem) {
- .split { grid-template-columns: 22rem 1fr; }
- .split-reverse .split-text { order: 2; }
+ .split { grid-template-columns: 1fr minmax(16rem, 22rem); }
}
+.split-text .sec-lede { margin-bottom: 0; }
.filters { list-style: none; margin: 0; padding: 0; display: grid; gap: .5rem; }
.filters code {
@@ -258,6 +266,10 @@ kbd {
display: grid; gap: 1rem; margin-bottom: 2rem;
grid-template-columns: repeat(auto-fit, minmax(17rem, 1fr));
}
+/* The clone URL is longer than a third of the wrap. Scrolling it inside the
+ card hides the end behind no affordance at all, so wrap it instead — the
+ whole string is what gets copied either way. */
+.install-card .cmd { white-space: normal; overflow-wrap: anywhere; }
/* ── Footer ───────────────────────────────────────────────────────── */
diff --git a/site/static/img/01-main.avif b/site/static/img/01-main.avif
Binary files differ.
diff --git a/site/static/img/01-main.png b/site/static/img/01-main.png
Binary files differ.
diff --git a/site/static/img/02-help.avif b/site/static/img/02-help.avif
Binary files differ.
diff --git a/site/static/img/02-help.png b/site/static/img/02-help.png
Binary files differ.
diff --git a/site/static/img/03-search.avif b/site/static/img/03-search.avif
Binary files differ.
diff --git a/site/static/img/03-search.png b/site/static/img/03-search.png
Binary files differ.
diff --git a/site/static/img/og.png b/site/static/img/og.png
Binary files differ.
diff --git a/site/templates/landing.html b/site/templates/landing.html
@@ -13,7 +13,7 @@
<meta property="og:title" content="{{ site.title }} — Oh My Music Player">
<meta property="og:description" content="{{ page.description }}">
<meta property="og:url" content="{{ page.permalink }}">
-<meta property="og:image" content="{{ site.origin }}{{ asset('img/01-main.png') }}">
+<meta property="og:image" content="{{ site.origin }}{{ asset('img/og.png') }}">
<meta name="twitter:card" content="summary_large_image">
<link rel="icon" href="{{ asset('img/logo.png') }}">
@@ -56,8 +56,8 @@
</div>
<figure class="shot shot-hero">
- <img src="{{ asset('img/01-main.png') }}" alt="ommp playing a track: library on the left, queue in the middle, album art and track details on the right"
- width="2400" height="1350" fetchpriority="high">
+ <img src="{{ asset('img/01-main.avif') }}" alt="ommp playing a track: library on the left, queue in the middle, album art and track details on the right"
+ width="3840" height="2160" fetchpriority="high">
</figure>
</header>
@@ -98,10 +98,12 @@
<!-- ── Search ────────────────────────────────────────────────────────── -->
<section class="band" data-section="Search">
- <div class="wrap split">
- <div class="split-text">
- <h2 class="sec-title">Find it while you type</h2>
- <p class="sec-lede">Results narrow on every keystroke. Narrow further with a field, or by format.</p>
+ <div class="wrap">
+ <div class="split">
+ <div class="split-text">
+ <h2 class="sec-title">Find it while you type</h2>
+ <p class="sec-lede">Results narrow on every keystroke. Narrow further with a field, or by format.</p>
+ </div>
<ul class="filters">
<li><code>artist:yorushika</code></li>
<li><code>album:elma</code></li>
@@ -109,28 +111,30 @@
<li><code>*.flac</code></li>
</ul>
</div>
- <figure class="shot">
- <img src="{{ asset('img/03-search.png') }}" alt="The search overlay filtering a library down to fifty of fifty-two tracks"
- width="2400" height="1350" loading="lazy">
+ <figure class="shot shot-detail">
+ <img src="{{ asset('img/03-search.avif') }}" alt="The search overlay filtering a library down to fifty of fifty-two tracks"
+ width="2400" height="1560" loading="lazy">
</figure>
</div>
</section>
<!-- ── Keys ──────────────────────────────────────────────────────────── -->
<section class="band band-alt" data-section="Keys">
- <div class="wrap split split-reverse">
- <div class="split-text">
- <h2 class="sec-title">Keys where you expect them</h2>
- <p class="sec-lede">Vim motions for the lists, media keys for the music. The mouse works too — click a tab, double-click a track, <kbd>Ctrl</kbd>-drag a border to resize.</p>
+ <div class="wrap">
+ <div class="split">
+ <div class="split-text">
+ <h2 class="sec-title">Keys where you expect them</h2>
+ <p class="sec-lede">Vim motions for the lists, media keys for the music. The mouse works too — click a tab, double-click a track, <kbd>Ctrl</kbd>-drag a border to resize.</p>
+ </div>
<ul class="keys">
{%- for k in x.keys %}
<li><kbd>{{ k.key }}</kbd><span>{{ k.desc }}</span></li>
{%- endfor %}
</ul>
</div>
- <figure class="shot">
- <img src="{{ asset('img/02-help.png') }}" alt="The keybinding overlay, two columns covering playback and navigation"
- width="2400" height="1350" loading="lazy">
+ <figure class="shot shot-detail">
+ <img src="{{ asset('img/02-help.avif') }}" alt="The keybinding overlay, two columns covering playback and navigation"
+ width="3100" height="1700" loading="lazy">
</figure>
</div>
</section>
diff --git a/site/tools/deploy.sh b/site/tools/deploy.sh
@@ -28,6 +28,6 @@ if git -C "$WORK" diff --cached --quiet; then
else
git -C "$WORK" commit -q -m "Deploy the landing page"
git -C "$WORK" push origin gh-pages
- echo "published → https://sqzer-x.github.io/ommp/"
+ echo "published → https://ommp.sqzer.com/"
fi
git -C "$REPO" worktree remove "$WORK" --force
diff --git a/site/tools/shoot.sh b/site/tools/shoot.sh
@@ -1,91 +1,111 @@
#!/usr/bin/env bash
-# Capture ommp screenshots for the landing page.
+# Capture ommp screenshots for the landing page, and crop them to what the
+# page actually shows.
#
# Runs the real binary in a real terminal: album art only renders as an image
# under a graphics protocol, and Ghostty is the only installed terminal that
# speaks one. Anything else would show the block-character fallback.
#
+# Two font sizes, because the two kinds of shot want opposite things:
+#
+# 14 the wide shot. Everything fits — full album names, and all thirteen
+# fields of the info panel including the sample rate and bit depth.
+# At 20 the panel stops at "Disc #" and the best detail is lost.
+# 20 the modal close-ups. A modal is sized as a percentage of the terminal
+# but its contents are a fixed number of characters, so a wider terminal
+# just adds empty space inside it. 20 is where the columns sit tight.
+#
# The window is fullscreen for about fifteen seconds and then closes. Your
-# player settings are backed up and restored — the app rewrites them on exit,
-# and this script deliberately changes a few for the shot.
+# player settings and your default sink's mute state are backed up and
+# restored — the app rewrites the former on exit, and this script changes both.
set -euo pipefail
-export XDG_RUNTIME_DIR=/run/user/1000
-export WAYLAND_DISPLAY=wayland-1
+export XDG_RUNTIME_DIR=${XDG_RUNTIME_DIR:-/run/user/1000}
+export WAYLAND_DISPLAY=${WAYLAND_DISPLAY:-wayland-1}
HERE=$(cd "$(dirname "$0")" && pwd)
-OUT=${OUT:-/tmp/ommp-shots}
-FONT_SIZE=${FONT_SIZE:-20}
-OMMP=${OMMP:-/home/himesama/Documents/ommp/target/release/ommp}
+RAW=${RAW:-$HOME/.cache/ommp-shoot/shots}
+OUT=${OUT:-$HERE/../static/img}
+OMMP=${OMMP:-$HERE/../../target/release/ommp}
MONITOR=${MONITOR:-DP-1}
STATE="$HOME/.config/ommp/state.json"
-mkdir -p "$OUT"
-BAK="$OUT/state.json.bak"
+mkdir -p "$RAW" "$OUT"
+BAK="$RAW/state.json.bak"
[ -f "$STATE" ] && [ ! -f "$BAK" ] && cp -a "$STATE" "$BAK"
+SINK=$(pactl get-default-sink)
+SINK_WAS_MUTED=$(pactl get-sink-mute "$SINK" | grep -o 'yes\|no')
+
cleanup() {
pkill -x ommp 2>/dev/null || true
- sleep 0.4
+ sleep 0.5
[ -f "$BAK" ] && cp -a "$BAK" "$STATE"
+ [ "$SINK_WAS_MUTED" = yes ] || pactl set-sink-mute "$SINK" 0
}
trap cleanup EXIT
-python3 "$HERE/prep-state.py" "$STATE"
-
# --background matches the logo's #0A051E so padding is not a different black.
-# --palette pins the ANSI colours the app draws selections and the progress bar
-# with, so the shot does not depend on whatever theme happens to be configured.
-ghostty \
- --font-family="JetBrainsMono Nerd Font" \
- --font-size="$FONT_SIZE" \
- --background=0A051E \
- --foreground=E8E6F0 \
- --palette=6=#00ffff --palette=14=#00ffff \
- --palette=5=#c850ff --palette=13=#c850ff \
- --fullscreen=true \
- --window-decoration=none \
- --window-padding-x=16 --window-padding-y=12 \
- --confirm-close-surface=false \
- -e "$OMMP" &
-
-# The splash runs a fixed two seconds; wait it out plus a beat for the first draw.
-sleep 5
-
-shoot() { grim -o "$MONITOR" "$OUT/$1.png" && echo " -> $1.png"; }
+# --palette pins the two ANSI colours the app draws selections and the progress
+# bar with, so the shot matches the page's palette rather than whatever theme
+# happens to be configured.
+launch() {
+ ghostty \
+ --font-family="JetBrainsMono Nerd Font" \
+ --font-size="$1" \
+ --background=0A051E \
+ --foreground=E8E6F0 \
+ --palette=6=#00ffff --palette=14=#00ffff \
+ --palette=5=#c850ff --palette=13=#c850ff \
+ --fullscreen=true \
+ --window-decoration=none \
+ --window-padding-x=16 --window-padding-y=12 \
+ --confirm-close-surface=false \
+ -e "$OMMP" >/dev/null 2>&1 &
+ # The splash runs a fixed two seconds; wait it out plus a beat for the draw.
+ sleep 5
+}
-# Mute the player's audio stream at the server, not in the app: the volume
-# meter is part of the shot, so it has to read a real level while staying
-# silent. The stream exists from startup, before anything plays.
-SINK_INPUT=""
-for _ in $(seq 20); do
- SINK_INPUT=$(pactl -f json list sink-inputs 2>/dev/null \
- | python3 -c 'import json,sys
-try:
- for si in json.load(sys.stdin):
- if si["properties"].get("application.process.binary") == "ommp":
- print(si["index"]); break
-except Exception:
- pass')
- [ -n "$SINK_INPUT" ] && break
- sleep 0.25
-done
-if [ -n "$SINK_INPUT" ]; then
- pactl set-sink-input-mute "$SINK_INPUT" 1
- echo " muted sink-input $SINK_INPUT"
-else
- echo " WARNING: could not find the player audio stream; it will be audible" >&2
-fi
+stop() { pkill -x ommp 2>/dev/null || true; sleep 1; }
-# Start playback so the status bar reads Playing with a partly filled progress
-# bar, rather than Stopped at 0:00.
+# ── The wide shot ─────────────────────────────────────────────────────────
+# Playback has to be running for the status bar to read Playing over a partly
+# filled progress bar. rodio does not create its sink-input until that moment,
+# so there is no per-stream target to mute in advance — mute the whole sink
+# across the shot instead and put it back afterwards.
+python3 "$HERE/prep-state.py" "$STATE"
+pactl set-sink-mute "$SINK" 1
+launch 14
wtype " "
sleep 9
+grim -o "$MONITOR" "$RAW/01-main.png"
+stop
+pactl set-sink-mute "$SINK" 0
-shoot 01-main
+# ── The modals ────────────────────────────────────────────────────────────
+# Nothing plays here, so nothing is audible.
+python3 "$HERE/prep-state.py" "$STATE"
+launch 20
+wtype -M ctrl -k h -m ctrl; sleep 1.2; grim -o "$MONITOR" "$RAW/02-help.png"
+wtype -k Escape; sleep 0.5
+wtype -M ctrl -k s -m ctrl; sleep 0.8; wtype -d 40 "yorushika"; sleep 1.2
+grim -o "$MONITOR" "$RAW/03-search.png"
+stop
-wtype -M ctrl -k h -m ctrl; sleep 0.8; shoot 02-help; wtype -k Escape; sleep 0.5
-wtype -M ctrl -k s -m ctrl; sleep 0.8; wtype -d 40 "yorushika"; sleep 1.0
-shoot 03-search; wtype -k Escape; sleep 0.5
+# ── Crop and encode ───────────────────────────────────────────────────────
+# Kept at the captured 3840-wide scale rather than downsampled: the pixel grid
+# of terminal text is already sharp, and resampling it both blurs the glyphs
+# and *inflates* the PNG by inventing intermediate colours.
+#
+# The modals are cropped to themselves. Uncropped they are a small rectangle in
+# the middle of a 4K frame, and no amount of page width makes them readable.
+crop() { magick "$RAW/$1.png" -crop "$2" +repage -strip "$OUT/$3.png"; }
+
+magick "$RAW/01-main.png" -strip "$OUT/01-main.png"
+crop 02-help 3100x1700+380+250 02-help
+crop 03-search 2400x1560+720+300 03-search
-echo "done: $OUT"
+for f in "$OUT"/0*.png; do
+ printf " %-16s %-11s %s\n" "$(basename "$f")" \
+ "$(magick identify -format '%wx%h' "$f")" "$(du -h "$f" | cut -f1)"
+done
diff --git a/src/ui/widgets/help_modal.rs b/src/ui/widgets/help_modal.rs
@@ -36,7 +36,7 @@ const NAVIGATION: &[(&str, &str)] = &[
("d", "Remove from queue"),
("c", "Clear queue"),
("", ""),
- ("Ctrl+drag border", "Resize panels"),
+ ("Ctrl+drag", "Resize panels"),
("Double-click", "Play queue track"),
("", ""),
("Esc", "Close modal"),
@@ -78,7 +78,7 @@ fn render_column(bindings: &[(&str, &str)], theme: &Theme) -> Vec<Line<'static>>
}
Line::from(vec![
Span::styled(
- format!(" {:18}", key),
+ format!(" {:16}", key),
Style::default().fg(Color::Yellow).add_modifier(Modifier::BOLD),
),
Span::styled(desc.to_string(), Style::default().fg(theme.fg)),
|