<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom"><id>https://oni.sqzer.com/ommp/atom.xml</id><title>ommp</title><subtitle>Oh My Music Player — a standalone terminal music player built with Rust and ratatui</subtitle><link rel="self" href="https://oni.sqzer.com/ommp/atom.xml"/><updated>2026-11-12T12:56:48+09:00</updated><entry><id>urn:git:b27cc1a2e688ca93c931dfd84201b12d7b7da21b</id><title>Update landing page identity and links for debuman</title><published>2026-11-12T12:56:48+09:00</published><updated>2026-11-12T12:56:48+09:00</updated><author><name>debuman</name><email>110329244+debuman@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/b27cc1a2e688ca93c931dfd84201b12d7b7da21b.html"/><content type="text">Update landing page identity and links for debuman
</content></entry>
<entry><id>urn:git:0f46823fb0aaa2e7dd4df2ad99173b5c4b50ae87</id><title>Update the AUR package to 0.1.3</title><published>2026-08-02T00:59:07+09:00</published><updated>2026-08-02T00:59:07+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/0f46823fb0aaa2e7dd4df2ad99173b5c4b50ae87.html"/><content type="text">Update the AUR package to 0.1.3
</content></entry>
<entry><id>urn:git:cfd5e8f7f89e43f02ca951a0115f99f9c54d734b</id><title>Release 0.1.3</title><published>2026-08-02T00:53:32+09:00</published><updated>2026-08-02T00:53:32+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/cfd5e8f7f89e43f02ca951a0115f99f9c54d734b.html"/><content type="text">Release 0.1.3

The help modal&#39;s key column is sixteen characters wide and Ctrl+drag border is
sixteen characters long, so the field padded to nothing and the description
began where the label ended:

  Ctrl+drag borderResize panels

Shortening the label to Ctrl+drag restores the gap. The binding is unchanged.
</content></entry>
<entry><id>urn:git:ef862c9961b719aed39c1e1fd8e0815d42bfe88a</id><title>Put the copy button inside the box, and centre the strike on the text</title><published>2026-08-01T22:51:16+09:00</published><updated>2026-08-01T22:51:16+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/ef862c9961b719aed39c1e1fd8e0815d42bfe88a.html"/><content type="text">Put the copy button inside the box, and centre the strike on the text

The button was a second bordered element beside the command, so a command and
its button read as two things. The border and the background belong to one box
now, with the command and the button inside it, which is how the reference site
does it.

The rule was positioned at a fixed .62em from the top of the list item, which
put it above the middle of the letters. It is drawn on a span around the text
instead: an inline box is the height of the text itself, so 50% of it lands on
the letters rather than on the line box, which sits lower and taller.
</content></entry>
<entry><id>urn:git:d4f81cca51acf4dc3ffef39a5a016a3f8e969338</id><title>Give the commands a copy button, and let the setup cross itself out</title><published>2026-08-01T22:40:20+09:00</published><updated>2026-08-01T22:40:20+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/d4f81cca51acf4dc3ffef39a5a016a3f8e969338.html"/><content type="text">Give the commands a copy button, and let the setup cross itself out

Clicking the command box grew it by the width of the word copied and then shrank
it again. The button is its own element beside the box now, and the feedback is
the icon swapping to a tick, so nothing moves. The prompt stays in a pseudo-
element, which never reaches the clipboard.

Install reuses the hero&#39;s command component instead of a card grid that stretched
an eleven-character command across half the page.

The setup you skip is crossed out a line at a time as the section comes into
view. The rule is a drawn pseudo-element rather than text-decoration, which
cannot be animated, and the markup ships already struck: the script un-strikes it
on load and puts it back, so a reader without JavaScript sees the finished state
rather than four live-looking instructions. Reduced motion strikes all four at
once.

Cut the three paragraphs of fine print under Install. The binary size, the Nerd
Font requirement, the graphics protocols and the platform caveats are all in the
README, which is where someone goes for them.
</content></entry>
<entry><id>urn:git:2335747957fa73d727cc9d8c7a765203e4df6a98</id><title>Rewrite the page as a landing page rather than a README</title><published>2026-08-01T22:24:52+09:00</published><updated>2026-08-01T22:24:52+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/2335747957fa73d727cc9d8c7a765203e4df6a98.html"/><content type="text">Rewrite the page as a landing page rather than a README

The page led with a spec list and then enumerated features: seven cards for the
seven browse tabs, three benchmark numbers, a search section, a keybinding
table, and a troubleshooting block, in that order. Nothing on it said what the
program is for or who would want it, and one line defended the methodology
behind its own benchmark, which nobody had questioned.

Four directions were written and judged against three lenses — a stranger, the
person this is actually for, and a copy editor. The one that won opens on the
thing that makes someone want it.

The hero says what it plays and where. The first section you scroll into is the
album art with a sentence naming what you are looking at, so the screenshot has
a reason to be there. Then the setup you do not do, struck out, against the one
line you type. Then browsing, with the tab strip set as type in the app&#39;s own
seven accents rather than seven cards. Then the small decisions — the untagged
file that still has a home, the playlist entry that survives an unmounted drive
— which are the reasons someone keeps a tool. Then the keys. Then install.

Cut: the whole benchmark band; the feature card grid; the keybinding table that
retyped the screenshot beside it; the requirements section, whose two real
warnings now sit inside Install as prose, where the person about to run the
command is; the GPU-acceleration aside about Alacritty and xterm; the git clone
command nobody uses to install a music player.

Every claim was checked against the source again: b opens the playlist modal
only while something is playing and adds the playing track, h and l move
between panes, the binary is 7.0M against libasound, libc, libgcc_s and libm,
and the info panel has thirteen labels.
</content></entry>
<entry><id>urn:git:19203c2b68db46a34653e4f2420abdc9fcca99be</id><title>Reset the deploy branch to the remote before building on it</title><published>2026-08-01T21:50:07+09:00</published><updated>2026-08-01T21:50:07+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/19203c2b68db46a34653e4f2420abdc9fcca99be.html"/><content type="text">Reset the deploy branch to the remote before building on it

GitHub commits to gh-pages itself whenever the Pages custom domain changes — it
wrote a Delete CNAME and a Create CNAME during the move to ommp.sqzer.com. The
deploy script fetched but checked out the local branch, which had not moved, so
the next deploy built on a fork and its push was rejected.
</content></entry>
<entry><id>urn:git:dbf58134484d32b88e4011ba46730e85923e7b23</id><title>Give each section the measure its content wants</title><published>2026-08-01T21:48:25+09:00</published><updated>2026-08-01T21:48:25+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/dbf58134484d32b88e4011ba46730e85923e7b23.html"/><content type="text">Give each section the measure its content wants

One width for everything meant a section built around a 3840px screenshot was
held to the same line length as a paragraph, which left the copy pinned to one
edge with dead margin beyond it. There are two measures now, assigned per
section the way the reference site does it: 68rem for prose-led sections, 78rem
for the ones built around a grid or a screenshot, and 84rem for the hero shot,
which is the product and is allowed to be the widest thing on the page.

The split sections take the wide measure and a 26rem copy column instead of a
fixed 22rem, so the text has a comfortable line and the outer margin at 1920
drops from 416px to 336px.

Both containers now carry a 92% floor. Without it a viewport between the two
caps — 1128 to 1288px, which is most laptops — pins the wide sections 20px off
the screen edge while the prose sections stay 96px in, and a 76px stagger
between neighbouring section headings reads as a mistake rather than a system.
With it the two track each other and nothing touches the edge.

The install boxes are stretched to a common height. The clone URL wraps to two
lines, which left one box hanging 24px below its neighbours.

Reviewed section by section at 1280, 1440 and 1920 against real renders.
</content></entry>
<entry><id>urn:git:f9e4785a349236d1d88697bc4c322c9c15fffe54</id><title>Shoot every screenshot at the same size, and stop shrinking the second one</title><published>2026-08-01T21:15:44+09:00</published><updated>2026-08-01T21:15:44+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/f9e4785a349236d1d88697bc4c322c9c15fffe54.html"/><content type="text">Shoot every screenshot at the same size, and stop shrinking the second one

The modal shots were taken at font-size 14 like the wide shot, not 20. Twenty
was chosen to make the keybindings read once cropped, but the crops are gone and
all it did was make the app look stretched next to the shot above it. Every
screenshot on the page is now the density the program actually runs at here.

The reversed section keeps its own column tracks. Reordering alone left the
figure in the 22rem track, so the keybinding shot came out half the size of the
search shot for no reason other than which side it sat on.
</content></entry>
<entry><id>urn:git:8748f0017cdb9da8ff7730465e40d3e643fcc9d3</id><title>Open the page on the splash screen, and put the screenshots back as they were</title><published>2026-08-01T21:08:24+09:00</published><updated>2026-08-01T21:08:24+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/8748f0017cdb9da8ff7730465e40d3e643fcc9d3.html"/><content type="text">Open the page on the splash screen, and put the screenshots back as they were

The hero fills with ommp&#39;s own boot screen. The splash scatters music notes
across a dark field while the library loads; tools/note-field.py reproduces that
scatter from the same hash the app uses, the same four glyphs and the same three
colours. A note falls where (7x + 13y) mod 37 == 0, and shifting either axis by
37 adds a multiple of 37 to the hash, so a 37x37 tile repeats without a seam.
It is rendered here rather than shipped as text because the glyphs live in the
CJK fonts, not in the page&#39;s latin-subset JetBrains Mono. The two radial washes
that stood in for it are still there, over the top.

The screenshots go back to whole frames. Cropping to the modals was meant to
make them readable and instead made them the loudest thing on the page. The
layout goes back with them: copy in a fixed column, screenshot in the rest,
alternating sides. Only the hero shot earns the full width — below it the
screenshots illustrate a point the copy already makes.

Both modal shots are now taken with playback running. One reading Stopped
beside two reading Playing looks like a mistake rather than a screenshot.
</content></entry>
<entry><id>urn:git:3ce49f6d15fd49e599e790ec60f9c541fd5e956f</id><title>Correct two false claims on the page, and fix keyboard copy, the card and the icon</title><published>2026-08-01T20:42:56+09:00</published><updated>2026-08-01T20:42:56+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/3ce49f6d15fd49e599e790ec60f9c541fd5e956f.html"/><content type="text">Correct two false claims on the page, and fix keyboard copy, the card and the icon

An audit of the live site turned up two sentences that describe software this
is not.

The Artists card said tracks are grouped by album artist where tagged and track
artist where not. Index::build keys by_artist on the track artist alone;
album_artist_of() is called once, for the by_album key. A compilation tagged
album_artist=Various Artists, artist=Yorushika files under Yorushika in the
Artists tab. The sentence was true, but about the Albums tab — so it has moved
there, and Artists now says what it does.

The lede promised it picks up where you left off. SavedState holds volume,
shuffle, repeat, pane widths, playlists, info view and right split; no queue, no
current track, no position. Startup calls ReplaceQueue over the whole library.
The claim is now the one the program can keep.

The install commands were click-only, which left the page&#39;s primary call to
action unreachable by keyboard. They take a tab stop, a button role and
Enter/Space now. The role is assigned from the script rather than the markup:
copying only works once the script has run, so without it they are plain text
and should not claim otherwise.

og:image was a raw 4K screenshot scaled to a card — at the size a feed renders
it, nothing was legible and the name appeared nowhere. It is now composed: the
wordmark, the headline, and the app&#39;s own progress bar. Legible at 240px.

The favicon was the same 723x421 wordmark, which is a smear in a tab, and
/favicon.ico had nothing behind it. Both are answered by one square mark cut
from the wordmark&#39;s own &#39;O&#39;, carrying the logo&#39;s pink-to-cyan, shipped as an
.ico at the site root.
</content></entry>
<entry><id>urn:git:0a5cb58b1ddb3da82913743d863277534d2d6949</id><title>Reshoot the screenshots and move the site to ommp.sqzer.com</title><published>2026-08-01T20:19:16+09:00</published><updated>2026-08-01T20:19:16+09:00</updated><author><name>sqzer</name><email>sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/0a5cb58b1ddb3da82913743d863277534d2d6949.html"/><content type="text">Reshoot the screenshots and move the site to ommp.sqzer.com

The help modal&#39;s key column was widened to 18 characters, which pushed the
longest row past the modal&#39;s inner width and clipped &#39;Focus prev / next pane&#39;
and &#39;Info panel: clock / art&#39;. Back to 16, with a shorter label for the one
binding that needed the extra space.

The wide shot was taken at font-size 20, where the info panel stops at &#39;Disc #&#39;
and album names truncate mid-word. At 14 the full thirteen fields fit, sample
rate and bit depth included, and nothing is cut. The modal shots stay at 20:
a modal is a percentage of the terminal but its contents are a fixed number of
characters, so a wider terminal only adds empty space inside it.

Screenshots now ship at the captured 3840-wide scale instead of downsampled to
2400. Resampling terminal text both blurs the glyphs and inflates the PNG by
inventing intermediate colours — the 4K originals were smaller than the
downscaled ones. As AVIF the three come to 428K against 1.8M as PNG, with no
visible difference at 1:1. og:image gets its own 1200x630 PNG, which is the
size the card renders and the one format crawlers can all read.

The two modal close-ups moved out of the half-width split and under it at the
full width of the wrap. Squeezed beside the copy they were unreadable, which
defeated the point of showing them.

Also stops the clone URL scrolling out of sight inside its install card.
</content></entry>
<entry><id>urn:git:47ab1a509bebdb9bf944215d73aacb89bf5be6be</id><title>Trim the page title and script the deploy</title><published>2026-08-01T19:26:00+09:00</published><updated>2026-08-01T19:26:00+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/47ab1a509bebdb9bf944215d73aacb89bf5be6be.html"/><content type="text">Trim the page title and script the deploy

The title concatenated the site description, which already repeats in the
meta tag. deploy.sh does the build, the doctor run and the gh-pages mirror in
one step so the branch never drifts from site/.
</content></entry>
<entry><id>urn:git:a1f1b09da51f16ff9ac87619d8205ec96b78b1df</id><title>Add the landing page, and refresh what the README got wrong</title><published>2026-08-01T19:24:00+09:00</published><updated>2026-08-01T19:24:00+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/a1f1b09da51f16ff9ac87619d8205ec96b78b1df.html"/><content type="text">Add the landing page, and refresh what the README got wrong

site/ is a sqzass project that builds the page at sqzer-x.github.io/ommp.
Every colour in the stylesheet is lifted from the application&#39;s own source —
BG_DARK, the logo&#39;s pink-to-cyan gradient, the progress bar&#39;s violet, and one
accent per tab — so the page cannot drift away from the thing it advertises.
Prose is set in Bricolage Grotesque and anything that is literally the app
(commands, keys, tab names, the status bar) in JetBrains Mono, the font ommp
is designed for. Both are self-hosted latin subsets, 128 KB in total.

The screenshots are new. The one in the README dated from February and showed
a UI that no longer exists: identical borders on every pane before focus was
made visible, a nine-field info panel where there are now thirteen, and the
tab colours before they were fixed. site/tools/shoot.sh reproduces them —
Ghostty is the only terminal installed here that speaks a graphics protocol,
so it is the only way to capture real album art rather than the block-character
fallback.

Three README claims had gone stale: album art has not been read from a
directory scan since it started coming out of the file&#39;s own tags, the
bottom-right pane renders track info rather than lyrics, and the crate is
installable from crates.io and the AUR now rather than only from git.

Also widens the help modal&#39;s key column, which was exactly as wide as
&quot;Ctrl+drag border&quot; and so left no gap before the description.
</content></entry>
<entry><id>urn:git:b316a9813b04c64fa287a4cb96219b142cddb767</id><title>Ignore the whole .claude directory</title><published>2026-08-01T18:31:38+09:00</published><updated>2026-08-01T18:31:38+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/b316a9813b04c64fa287a4cb96219b142cddb767.html"/><content type="text">Ignore the whole .claude directory

Two entries listing individual files did the same job as one directory
rule, and would have needed a third the next time something landed there.
</content></entry>
<entry><id>urn:git:a707cf53e205f98957e32db1a01a98722a26160a</id><title>Keep the local worklog out of the repo</title><published>2026-08-01T18:27:56+09:00</published><updated>2026-08-01T18:27:56+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/a707cf53e205f98957e32db1a01a98722a26160a.html"/><content type="text">Keep the local worklog out of the repo
</content></entry>
<entry><id>urn:git:9143476093204319e5063dad7a3d1c40c185b131</id><title>State the licence in the badge instead of deriving it</title><published>2026-08-01T18:21:11+09:00</published><updated>2026-08-01T18:21:11+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/9143476093204319e5063dad7a3d1c40c185b131.html"/><content type="text">State the licence in the badge instead of deriving it

The badge asked shields.io for GitHub&#39;s detected licence, and GitHub&#39;s
image proxy cached the answer for four hours — so the README kept showing MIT
well after the relicense, which is the one thing a licence badge must not get
wrong. A literal badge cannot go stale, and it is edited alongside LICENSE and
Cargo.toml when the licence actually changes.
</content></entry>
<entry><id>urn:git:4d09cf9e82d048943d84b28f2ea2e6443b001692</id><title>Update the AUR package to 0.1.2</title><published>2026-08-01T18:17:44+09:00</published><updated>2026-08-01T18:17:44+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/4d09cf9e82d048943d84b28f2ea2e6443b001692.html"/><content type="text">Update the AUR package to 0.1.2
</content></entry>
<entry><id>urn:git:cbb8040e1886c2b62b63d49db43a97c2e4860bfe</id><title>Release 0.1.2</title><published>2026-08-01T18:14:27+09:00</published><updated>2026-08-01T18:14:27+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/cbb8040e1886c2b62b63d49db43a97c2e4860bfe.html"/><content type="text">Release 0.1.2

Documentation only: the README now describes terminal support by graphics
protocol rather than recommending Kitty by name, and carries the AUR
packaging files. Bumped so the crates.io page shows the corrected text.
</content></entry>
<entry><id>urn:git:59e693fe46cf76c4bc1b762e8fa305af4cb73b1f</id><title>Describe terminal support by image protocol, not by name</title><published>2026-08-01T18:08:40+09:00</published><updated>2026-08-01T18:08:40+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/59e693fe46cf76c4bc1b762e8fa305af4cb73b1f.html"/><content type="text">Describe terminal support by image protocol, not by name

The README told people to use Kitty. What actually decides whether album
art is a real image is the graphics protocol the terminal implements — Kitty
graphics, Sixel or iTerm2 — and Ghostty, WezTerm, Konsole, foot and others
all qualify. The protocol in use is detected at startup.

GPU acceleration is not the criterion and the README now says so: Alacritty
is GPU-accelerated and implements none of them, while xterm is not and
supports Sixel.

The AUR package drops its kitty optdepend for the same reason; naming one
terminal was misleading and a terminal is not really an optional dependency
of a terminal application.
</content></entry>
<entry><id>urn:git:e2a94beb25e643bcea35a1a30eaa24f3a2ce24c9</id><title>Add the AUR packaging files</title><published>2026-08-01T18:00:17+09:00</published><updated>2026-08-01T18:00:17+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/e2a94beb25e643bcea35a1a30eaa24f3a2ce24c9.html"/><content type="text">Add the AUR packaging files

PKGBUILD builds from the tagged GitHub tarball with --frozen against the
committed Cargo.lock, runs the test suite in check(), and installs the
binary, LICENSE and README. Verified with makepkg: the package builds and
lists only /usr/bin/ommp plus the licence and doc.

Runtime dependencies come from ldd on the release binary — alsa-lib via
rodio, plus glibc and gcc-libs. Nerd Fonts and kitty are optdepends because
the interface degrades rather than fails without them.
</content></entry>
<entry><id>urn:git:fe95286fa8a69df1316763866a60dbb0762b00f4</id><title>Say what to do when stdin or stdout is not a terminal</title><published>2026-08-01T17:55:09+09:00</published><updated>2026-08-01T17:55:09+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/fe95286fa8a69df1316763866a60dbb0762b00f4.html"/><content type="text">Say what to do when stdin or stdout is not a terminal

Raw mode failed with a bare &quot;No such device or address&quot;, which says
nothing about the cause. Check both streams up front and explain that ommp
needs an interactive terminal and cannot run through a pipe or a redirect.

Version bumped to 0.1.1: crates.io versions are immutable, so 0.1.0 cannot
carry this.
</content></entry>
<entry><id>urn:git:87ad44bba361533c12ca96c32c1dab87c81d115a</id><title>Relicense under Apache 2.0</title><published>2026-08-01T17:40:14+09:00</published><updated>2026-08-01T17:40:14+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/87ad44bba361533c12ca96c32c1dab87c81d115a.html"/><content type="text">Relicense under Apache 2.0

LICENSE is the canonical text from apache.org verbatim, with only the
appendix placeholders filled in:

    Copyright 2026 sqzer (hyukjin an)

Cargo.toml, the README and the About box follow.

The crate also excludes the 1.2 MB screenshot, which everyone installing
would otherwise download for an image crates.io cannot resolve from a
relative README path. Packaged size drops from 1.5 MiB to 349 KiB.
</content></entry>
<entry><id>urn:git:d6d7962221054204361e29cdaac2e66984a5c3eb</id><title>Read the About box version from Cargo.toml</title><published>2026-08-01T17:32:26+09:00</published><updated>2026-08-01T17:32:26+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/d6d7962221054204361e29cdaac2e66984a5c3eb.html"/><content type="text">Read the About box version from Cargo.toml

It was a literal, so it would have kept claiming 0.1.0 after the next
version bump.
</content></entry>
<entry><id>urn:git:3a6429990ef360ddc4ae6df626181f656590d55a</id><title>Tell the user the splash can be skipped, and skip it outright</title><published>2026-08-01T17:26:06+09:00</published><updated>2026-08-01T17:26:06+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/3a6429990ef360ddc4ae6df626181f656590d55a.html"/><content type="text">Tell the user the splash can be skipped, and skip it outright

The splash could always be dismissed with a key, but nothing said so.
A dim italic &quot;Press any key to skip&quot; now sits at the bottom, fading in and
out with the rest rather than appearing at full strength. Terminals too short
to fit it below the centred block simply do not get it.

The key now drops the splash immediately instead of jumping to the fade-out.
Half a second of animation after the prompt says the key skips is not what it
promises.
</content></entry>
<entry><id>urn:git:25db58e89789322ca2be5a7e91aecc5573e4e7a0</id><title>Let the splash always play its full timeline</title><published>2026-08-01T17:20:26+09:00</published><updated>2026-08-01T17:20:26+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/25db58e89789322ca2be5a7e91aecc5573e4e7a0.html"/><content type="text">Let the splash always play its full timeline

Cutting the hold short when the scan finished saved about a second of
startup, but the animation is there to be watched, not to fill time. The
timeline is fixed again at half a second of fade-in, a hold, and half a
second of fade-out.

A keypress still skips ahead to the fade-out, which it always did.

Starting the scan before the terminal query and the audio device open is
kept: it costs nothing and only means the library is ready sooner behind the
animation.
</content></entry>
<entry><id>urn:git:9e51afb3605b901755bcad54865dded790170e76</id><title>End the splash once the library is ready</title><published>2026-08-01T17:14:47+09:00</published><updated>2026-08-01T17:14:47+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/9e51afb3605b901755bcad54865dded790170e76.html"/><content type="text">End the splash once the library is ready

The scan already ran on its own thread alongside the splash, so the two
were overlapped — but the splash held for a fixed two seconds regardless.
With the tag cache the scan finishes in tens of milliseconds, which made the
animation essentially the whole startup wait.

Completing the scan now skips the splash forward to its fade-out, the same
jump a keypress already performed, so a warm start takes about a second
instead of two and the fade still plays. A scan slower than the hold is
unaffected: the timeline runs to its normal end.

The scan is also spawned before the terminal image-protocol query and the
audio device open, both of which round-trip outside the process and neither
of which it needs to wait for.
</content></entry>
<entry><id>urn:git:5309fc5c99d371a2d035996dd6a4d6b21bd3d9e0</id><title>Scan in parallel and cache parsed tags between runs</title><published>2026-08-01T17:06:59+09:00</published><updated>2026-08-01T17:06:59+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/5309fc5c99d371a2d035996dd6a4d6b21bd3d9e0.html"/><content type="text">Scan in parallel and cache parsed tags between runs

Scanning opened and header-parsed every file on every launch, one at a
time. Measured on 5,000 files:

    serial, no cache      362 ms
    parallel, no cache    137 ms
    parallel + cache       18 ms   (31 ms with a cold page cache)

Files are now parsed across all cores with std::thread::scope, and parsed tags
are stored in $XDG_CACHE_HOME/ommp/library.json. An entry is reused when the
file&#39;s modification time and size still match, so a repeat scan costs one stat
per file instead of an open and a header parse; retagging or replacing a file
changes both and re-reads just that one.

The cache is never load-bearing: an unreadable, corrupt or older-version file
yields an empty map and everything gets parsed, and it is written via a
temporary file and renamed so an interrupted save keeps the previous one. All
5,000 tracks were verified byte-identical between a cached load and a fresh
parse.

The benchmark numbers understate the cold-disk case: the test set was
hardlinked, so it shared 50 inodes. Against genuinely distinct files the
uncached path measured 0.72 ms per track, about 3.6 seconds for 5,000.
</content></entry>
<entry><id>urn:git:d4e77dde3a0dd010e63847bd941a9b748e6e336a</id><title>Precompute library views instead of rebuilding them every frame</title><published>2026-08-01T16:57:11+09:00</published><updated>2026-08-01T16:57:11+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/d4e77dde3a0dd010e63847bd941a9b748e6e336a.html"/><content type="text">Precompute library views instead of rebuilding them every frame

The derived views — artists, albums, genres, formats, directories — were
recomputed from scratch on every access, each walking all tracks and cloning
strings, and the render loop asks for several of them per frame. Measured on
a synthetic library at 160x48:

    tracks      before      after
     1,000    0.815 ms    0.409 ms
    10,000    1.981 ms    0.313 ms
    50,000    7.966 ms    0.337 ms

Frame cost is now flat in library size rather than linear: the work happens
once when the library is built, and lookups are a hash and a slice.

path_to_index was a linear scan, so restoring a 1,000-entry playlist against
a 10,000-track library cost ten million path comparisons; it is a map now.
Extension search became an index lookup (1.594 ms to 0.008 ms at 50k), and
the general search no longer lowercases five fields of every track per
keystroke (4.62 ms to 1.98 ms). The queue pane asked
playlists.iter().any(contains(..)) per visible row per frame; App keeps a
membership set.

Track::from_path had lofty decode and allocate every embedded cover — around
a megabyte a track — only to drop it, since album art is read separately when
the view needs it. Skipping it takes 50 real FLACs from 3.8 ms to 0.6 ms.

The event loop redrew on its 50 ms timeout whether or not anything had
changed, and drew between individual mouse-motion events so the unbounded
channel grew faster than it drained. It now redraws only when an event
changed something and the queue is empty, with the splash exempt because it
animates. Event::LibraryReady is boxed so the channel element is not sized to
a whole library with its index.
</content></entry>
<entry><id>urn:git:bb40b5bd6c6dbda79cc1a3e82422c74e6ea1571d</id><title>Replace the Ctrl+E chord prefix with direct Ctrl shortcuts</title><published>2026-08-01T16:41:15+09:00</published><updated>2026-08-01T16:41:15+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/bb40b5bd6c6dbda79cc1a3e82422c74e6ea1571d.html"/><content type="text">Replace the Ctrl+E chord prefix with direct Ctrl shortcuts

Search, help, about and library sync are now Ctrl+S, Ctrl+H, Ctrl+A and
Ctrl+L instead of a two-keystroke chord. About moved off i because terminals
send Ctrl+I as the same byte as Tab (0x09) and Ctrl+M as Enter (0x0D), so
neither can be bound; Ctrl+H is fine, since Backspace is 0x7F rather than
0x08, and Ctrl+S is safe because raw mode clears IXON.

The Ctrl arms have to be matched before the unmodified ones: the existing
global bindings match on any modifier, so Ctrl+S would have toggled shuffle.

Keyboard resize mode is gone — dragging a panel border already did the same
job. The drag now requires Ctrl, so an ordinary click one column from a
border selects a list row instead of silently starting a resize.

Ctrl+C now quits from anywhere including an open modal, and Ctrl-modified
keys no longer type a literal character into the search and playlist name
fields.
</content></entry>
<entry><id>urn:git:3e8f73864576e9125d2be9f35286fbf7086d9ddd</id><title>Fix keybinding docs and modals that silently clipped content</title><published>2026-08-01T15:58:34+09:00</published><updated>2026-08-01T15:58:34+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/3e8f73864576e9125d2be9f35286fbf7086d9ddd.html"/><content type="text">Fix keybinding docs and modals that silently clipped content

The help modal never listed p (toggle the info panel), h/l, Esc or Ctrl+C,
and its 23 entries did not fit an 80x24 terminal — with no scrolling, the
tail including &quot;q: Quit&quot; was simply cut off. It is two columns now. The
README was missing c and Ctrl+C.

The playlist modal drew every playlist from the top and let the selection
walk past the bottom edge, so on a short terminal Enter toggled a playlist
that was not on screen. The search modal reported its full height as the
result viewport even though title_bottom reserves a row, which stopped
keyboard auto-scroll one result short and mapped clicks on the count footer
to a row that was never drawn.

The tab bar had six colours for seven tabs, so the modulo wrapped and
Playlists rendered in Queue&#39;s blue; hit-testing checked only the x range, so
clicking the bar&#39;s border switched tabs, and the divider columns were
attributed to the following tab.

lyrics_pane renders track metadata, not lyrics — the name outlived the
feature. Renamed, and its scroll offset, written by seven handlers and read
by none, is now applied: the field list grew to 13 rows and did not fit the
pane&#39;s default height.
</content></entry>
<entry><id>urn:git:40ce1fb2337c46b7c1e7423070f40851ebf38e90</id><title>Collapse the five duplicated list panes into one</title><published>2026-08-01T15:54:22+09:00</published><updated>2026-08-01T15:54:22+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/40ce1fb2337c46b7c1e7423070f40851ebf38e90.html"/><content type="text">Collapse the five duplicated list panes into one

artists, albums, genre, format and playlists were 942 lines across five
files. Normalising identifiers made three of them byte-identical over all
185 lines; the other two differed only inside the row-render closure and one
call in the Enter arm. They are now one ListPane parameterised by rows and an
activation, with the per-tab differences expressed as rows_for/activate. The
seven-arm matches in handler.rs and ui/mod.rs collapse with them.

That duplication was actively hiding bugs. Only one of the five clamped the
selection at render time, and it survived only because a separate clamp ran
on library reload. Every list clamped its scroll offset to count - 1 rather
than count - viewport, so scrolling to the end left a single row above a
screen of blank space — the correct formula already existed in the search
modal and had never been back-ported. The format pane called
get_tracks_by_format inside its render closure, rescanning and lowercasing
every track for every visible row on every frame; it is one pass now. The
albums pane emitted unpadded columns, so a CJK title and an ASCII one put the
artist column at different offsets.

get_tracks_by_album matched on the album title alone while get_albums pairs
each album with its artist, so two different records named &quot;Greatest Hits&quot;
merged and clicking either queued both.

fit_to_width, format_time and centered_rect had two to four copies each and
now live in ui::text.
</content></entry>
<entry><id>urn:git:3de0dadf2030be54bcea7dfd04a7a4a9a8366aaa</id><title>Delete code that was never compiled or never read</title><published>2026-08-01T15:48:11+09:00</published><updated>2026-08-01T15:48:11+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/3de0dadf2030be54bcea7dfd04a7a4a9a8366aaa.html"/><content type="text">Delete code that was never compiled or never read

album_artists_pane.rs and search_pane.rs were not declared in panes/mod.rs,
so 393 lines were never compiled — which is why nothing ever flagged that
search_pane.rs references three AppAction variants that do not exist and
would fail to build if it were wired up. They were also the only reason three
Library album-artist methods carried allow(dead_code).

Their backing state went with them: search_mode was never assigned true by
compiled code, so search_query stayed empty and search_results stayed empty
forever, making two branches unreachable and a remap in replace_library
pointless. QueueState::scroll_offset was written in three places while the
queue pane used its own. Six of the theme&#39;s sixteen fields were never read,
hidden behind a blanket allow(dead_code). Track::lyrics survived the removal
of the lyrics feature. The queue pane&#39;s mouse handler computed a hit test
whose body was a single comment, duplicating what handler.rs already does.

Event::Resize carried dimensions nobody used (the next draw re-queries the
terminal) and AudioEvent::TrackError a message nobody displayed. No
allow(dead_code) remains in the tree.
</content></entry>
<entry><id>urn:git:9a4c1e8c642bd141661d2a23fe884a1bd00143be</id><title>Make focus follow the click, and make it visible</title><published>2026-08-01T15:44:37+09:00</published><updated>2026-08-01T15:44:37+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/9a4c1e8c642bd141661d2a23fe884a1bd00143be.html"/><content type="text">Make focus follow the click, and make it visible

border_focused and border_unfocused were both Cyan, so every pane computed
a focus-dependent border colour and got the same answer — there was no way to
see which pane had focus at all. They are now distinct.

Focus itself was re-derived from the mouse on every 200ms tick, from a
position latched on the first mouse event and never cleared. Pressing Tab
moved focus for one frame before the next tick dragged it back under the
pointer, and the same clobber fired on any mouse event including the scroll
wheel, which also cancelled the auto-focus after adding to the queue. Focus
now changes on a left click and nothing else; hover still highlights rows.

The hit-testing was duplicated verbatim between the mouse handler and the
tick refresh, so it is one PaneHit now. A click on the library pane&#39;s border
or on the blank space below a short list synthesized Enter unconditionally
and activated the previous selection; it is ignored unless the click landed
on a real row, and hover is bounded the same way. Double-click compared only
the row, so a click in one pane paired with a click at the same height in
another; it now requires the same cell and clears once it fires. Mouse events
during the splash were processed against the invisible UI.
</content></entry>
<entry><id>urn:git:e3b4a7ee271606ab38fba4d9ef678cc16816d62b</id><title>Fix playback: volume, queue semantics, shuffle, and repeat</title><published>2026-08-01T15:40:46+09:00</published><updated>2026-08-01T15:40:46+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/e3b4a7ee271606ab38fba4d9ef678cc16816d62b.html"/><content type="text">Fix playback: volume, queue semantics, shuffle, and repeat

The player thread ran two loops: an idle one matching only Play and Stop
that dropped every other command through a catch-all arm, and a playing one
that recursed into itself per track. Volume lived only in the playing loop,
so the restored volume sent at startup was discarded and every track that
ended on its own started the next at rodio&#39;s default full volume. The
recursion also left a stack frame and a still-connected Sink behind per
track. Both are now one flat loop with the sink, volume and clock as locals.

AddToQueue replaced the queue rather than appending, resetting the current
index to 0 without telling the engine — so picking anything mid-song wiped
the queue while the old track stayed audible and the UI named a different
one. Splitting it into AddToQueue and ReplaceQueue makes the name true; only
the startup load replaces.

Shuffle picked a random index on every advance, so it could repeat a track
immediately, skip others, and never terminate with repeat off. It is now a
real permutation with a position, anchored on the current track when toggled,
and previous walks back along it instead of stepping to current_index - 1.

RepeatMode::One was applied to the manual next action too, leaving no way to
skip forward while it was on, and a decode error routed straight back into
next — under repeat-one an infinite retry that saturated the event channel.
Advancing now distinguishes a user skip from a track ending, and repeated
failures give up.

Removing the playing entry left current_index pointing at the track that
slid into the slot while the removed one kept playing, so that track was
silently skipped; the selection was never clamped either, which killed both
j and d until you pressed k. Clearing the queue and reaching its end set the
UI to stopped without telling the engine, leaving audio running and the clock
climbing. A failure to open the output device was printed into the alternate
screen and swallowed; it now reaches the status bar.
</content></entry>
<entry><id>urn:git:f9d25e6adbbf8a682ad9833869da6eeb9d6175ca</id><title>Stop panics and silent data loss from destroying a session</title><published>2026-08-01T15:34:38+09:00</published><updated>2026-08-01T15:34:38+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/f9d25e6adbbf8a682ad9833869da6eeb9d6175ca.html"/><content type="text">Stop panics and silent data loss from destroying a session

Terminal restore only ran on the normal return path, so any panic left raw
mode, the alternate screen and mouse tracking on with the message scrolled
away — install a panic hook that restores first. A backend write error hit
the same class of problem, returning from the event loop before state was
saved; record it and fall through instead.

Playlists were pruned rather than preserved: replace_library dropped every
path the new library lacked and the pruned result was written to disk on
exit, so one unmounted drive erased them for good. Refuse a scan that comes
back empty against a non-empty library, and park unresolved paths so they
come back when the files do.

The watcher seeded its debounce timestamp with the current time and never
reset it after scanning, firing two full rescans about two seconds into
every session with nothing changed — each one yanking the list cursor to the
top. Only arm the debounce on a real event.

Other crashes: the search modal cached raw library indices that no rescan
path remapped, panicking on the next draw; play_next, play_prev and the
queue renderer indexed library.tracks unchecked, now funnelled through one
guarded play_queue_position; the border-drag math underflowed u16 when
dragged past the dashboard edge and called clamp with min &gt; max on a corrupt
pane_widths, both replaced by split_widths; and the splash key handler
subtracted from Instant without checked_sub.

State is now written via a temporary file and renamed, every field carries a
serde default so one unknown key no longer discards the whole file, and
pane_widths is validated on load like right_split already was.
</content></entry>
<entry><id>urn:git:c28ef671890883609ba3c7c50818c28c9b228f13</id><title>Read album art and metadata from the audio file itself</title><published>2026-08-01T15:27:39+09:00</published><updated>2026-08-01T15:27:39+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/c28ef671890883609ba3c7c50818c28c9b228f13.html"/><content type="text">Read album art and metadata from the audio file itself

Album art was looked up by scanning the track&#39;s directory for a jpg/png,
so files carrying an embedded cover in a directory without a loose image
showed nothing. Decode the picture out of the file&#39;s tags instead, keyed on
the track path rather than its directory, so per-track covers in a shared
directory also resolve.

Also surface the tag and stream fields that were already in the file but
unused: year, track/disc totals, comment, sample rate, bit depth, and
channel count. Fall back to the overall bitrate when a lossless format
reports no audio bitrate.
</content></entry>
<entry><id>urn:git:33727f4edb996d630343dfee79d170bc3534b3bc</id><title>Wire up entry point and add README with assets</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/33727f4edb996d630343dfee79d170bc3534b3bc.html"/><content type="text">Wire up entry point and add README with assets

Terminal setup and teardown, background library scan, and the main event
loop dispatching into the action handler and renderer. README covers
install, requirements, usage, and keybindings.
</content></entry>
<entry><id>urn:git:5aeea01f7c1878fa70c2636db895c0307e73f07a</id><title>Add TUI panes, widgets, layout, and theming</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/5aeea01f7c1878fa70c2636db895c0307e73f07a.html"/><content type="text">Add TUI panes, widgets, layout, and theming

Resizable three-column dashboard with tabbed library browsing by artist,
album, genre, format, directory, and playlist. Widgets cover the status
bar, progress bar, info pane with album art and clock, and the search,
playlist, help, and about modals.
</content></entry>
<entry><id>urn:git:439bfcd6f486260b0eb247c030157ca2420556f1</id><title>Add application state, action handling, and session persistence</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/439bfcd6f486260b0eb247c030157ca2420556f1.html"/><content type="text">Add application state, action handling, and session persistence

Central App struct holding library, queue, playlists, and playback state,
driven by an AppAction enum. Key and mouse handlers translate input into
actions. Volume, shuffle, repeat, pane layout, and playlists are saved to
disk on exit and restored on start.
</content></entry>
<entry><id>urn:git:53fbcf0468bac8e2754a5e0613e3d34d5e44a8d2</id><title>Add audio playback engine and event plumbing</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/53fbcf0468bac8e2754a5e0613e3d34d5e44a8d2.html"/><content type="text">Add audio playback engine and event plumbing

Playback thread wrapping rodio with play/pause/seek/volume control and
position reporting. Input and tick threads feed a single crossbeam
channel, so key, mouse, tick, audio, and library events share one queue.
</content></entry>
<entry><id>urn:git:e7fc72c56f69f3deb70e1e6248a910bfaae83f17</id><title>Add music library scanning, metadata, and filesystem watching</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/e7fc72c56f69f3deb70e1e6248a910bfaae83f17.html"/><content type="text">Add music library scanning, metadata, and filesystem watching

Recursive scanner over the music directory, track model with tags read
via lofty, embedded/sidecar cover art lookup, and a notify-based watcher
that reports library changes back to the event loop.
</content></entry>
<entry><id>urn:git:c60eaede041dea6f87d12021f57e619b18c1fb00</id><title>Initial commit: project scaffold, dependencies, and license</title><published>2026-08-01T14:31:13+09:00</published><updated>2026-08-01T14:31:13+09:00</updated><author><name>sqzer-x</name><email>110329244+sqzer-x@users.noreply.github.com</email></author><link rel="alternate" href="https://oni.sqzer.com/ommp/commit/c60eaede041dea6f87d12021f57e619b18c1fb00.html"/><content type="text">Initial commit: project scaffold, dependencies, and license

Rust 2021 edition binary crate built on ratatui + crossterm for the TUI,
rodio + symphonia for playback, and lofty for tag reading. MIT licensed.
</content></entry>
</feed>
