aboutsummaryrefslogtreecommitdiff
Commit message (Collapse)AuthorAgeFilesLines
* fix(custom-comments): correct comment-reformat and inline-borderCraig Jennings16 hours3-25/+34
| | | | | | cj/comment-reformat printed "No region was selected" on every call, even successful ones, because the message sat outside the if. Its fill-column shrink used a raw setq and restore, so an error mid-join left fill-column permanently at -3. It now signals a user-error when there's no region and dynamically binds fill-column, which restores itself. cj/--comment-inline-border sized the right decoration from text-length parity, leaving even-length and empty text two columns short and misaligning stacked dividers. It now fills the exact remaining width. Both fixes ship with tests, and the inline-border test asserts exact width across parities.
* fix(custom-datetime): make sortable-time actually sortableCraig Jennings16 hours2-3/+4
| | | | sortable-time-format was 12-hour with AM/PM ("%I:%M:%S %p %Z"), so "01:00:00 PM" sorted before "09:00:00 AM". I switched it to 24-hour "%H:%M:%S %Z", matching sortable-date-time, and updated the test that asserted the AM/PM output.
* fix(custom-buffer-file): error when copy-link has no file to copyCraig Jennings16 hours2-10/+14
| | | | cj/copy-link-to-buffer-file did nothing in a non-file buffer, while every sibling copy command signals a user-error. The silent no-op gave no feedback either way. Now it signals a user-error too, and I updated the two tests and the commentary that had documented the silence as intended.
* refactor(selection-framework): drop the overridden vertico sort customCraig Jennings16 hours1-1/+3
| | | | vertico-sort-function was set to history-alpha, but vertico-prescient-mode owns sorting inside every vertico session and overrode it, so the custom never took effect. I removed it (leaving a note) and kept prescient's frecency sorting, which is the intended behavior.
* refactor: retire unreferenced modules to an archive directoryCraig Jennings17 hours12-8/+73
| | | | | | A reachability sweep from init.el found eight .el files that nothing in the config loads or references. I moved them under archive/ (off the load-path, still tracked in git) so they stay readable and restorable rather than deleted. archive/README.org records the rule (every .el outside archive/ should be actively used) and why each file was retired. I retired show-kill-ring, duet-config, and mu4e-org-contacts-setup from modules/, and eplot, profile-dotemacs, titlecase, titlecase-data, and edit-indirect from custom/. Retiring show-kill-ring also removed the orphaned M-K -> M-S-k translation in keyboard-compat, which had left M-K a dead key. Its tests now assert seventeen translations and guard M-K's absence.
* fix(custom-buffer-file): stop leaking the MIME handle in the email viewerCraig Jennings17 hours2-15/+45
| | | | | | cj/view-email-in-buffer dissected the message into a handle, then signaled a user-error when it found no displayable part, before the mm-destroy-parts cleanup. The handle leaked on every image-only or attachment-only email. Wrap the body in unwind-protect so the handle is always destroyed. While here, the eager top-level require of mm-decode only fed the mm-handle-type macro at compile time. The viewer already requires mm-decode at runtime before any mm-* call, so I moved it to eval-when-compile plus declare-function (matching the ps-print block above). Opening Emacs no longer loads mm-decode unless you view an email.
* refactor(ui-config): drop the duplicate nerd-icons stanzaCraig Jennings18 hours1-9/+0
| | | | nerd-icons is owned by nerd-icons-config.el (:demand t, plus the dir-color advice and the completion and ibuffer integration). This bare :defer t re-declaration did nothing, so I removed it and its Commentary bullet.
* fix(ui-theme): create persist/ before writing the saved themeCraig Jennings18 hours2-2/+21
| | | | cj/theme-write-file-contents guarded on file-writable-p, which returns nil when the target's directory doesn't exist. On a fresh machine persist/ isn't there yet, so the theme write failed silently and the choice never persisted. Create the parent directory first, and still return nil if the path isn't writable.
* fix(undead-buffers): guard kill-other-window against a lone windowCraig Jennings18 hours2-8/+15
| | | | | | | | In a single-window frame cj/kill-other-window killed the buffer being viewed: other-window no-ops, delete-window was guarded, but the kill still ran on the current buffer. Signal a user-error instead, matching the sibling cj/kill-other-window-buffer. Past the guard there's always another window, so delete-window drops its own check. The single-window test asserted the broken behavior, so I rewrote it to expect the user-error and an untouched buffer. Also documented the prefix-argument undead-mark in cj/kill-buffer-or-bury-alive.
* fix(font-config): daemon emoji images and a re-runnable font listCraig Jennings18 hours2-16/+74
| | | | | | emojify-display-style was computed once from env-gui-p inside the :defer'd :config block. In a daemon that block runs before any GUI frame exists, so the style latched to unicode and GUI frames never got image emoji. Recompute it per-frame from server-after-make-frame-hook, mirroring the emoji-fontset setup above. cj/display-available-fonts called special-mode at the end, leaving the buffer read-only, so the second invocation errored on erase-buffer. Rewrite it under inhibit-read-only.
* feat: add takuzu-config and wire it into initCraig Jennings20 hours2-0/+18
| | | | | | Wire the standalone Takuzu (Binairo) game with use-package and require it at startup so M-x takuzu is available. The package lives in its own repo and loads by :load-path. Switch to :vc once it is published.
* refactor(local-repository): drop the dead archive-init pathCraig Jennings30 hours3-111/+41
| | | | localrepo-initialize, localrepo--car-member, and the repository-id/priority defcustoms were never called: early-init.el owns adding the local archive to package-archives, with its own localrepo-location constant. The module is now just cj/update-localrepo-repository. It targets localrepo-location, so there's one archive path instead of a divergent copy. The car-member test goes with its subject. A new test pins that the mirror update uses localrepo-location.
* refactor(keybindings): drop the no-op C-x C-f rebindCraig Jennings30 hours1-2/+0
|
* fix(system-utils): correct swapped args in the run-on-file messageCraig Jennings30 hours1-1/+1
|
* fix(config-utilities): detect native-comp with the right predicate, clear ↵Craig Jennings30 hours2-10/+25
| | | | | | eln-cache cj/recompile-emacs-home selected native compilation with (boundp 'native-compile-async), but that's a function, so boundp is always nil. The command silently byte-compiled every time, even on native-comp builds. Detection now goes through cj/--native-comp-p, which uses native-comp-available-p. The native cleanup also deleted <dir>/eln, but the real cache is eln-cache/, so a stale cache survived. Both are pinned by tests: a regression on the detection helper, and the cache test now targets eln-cache.
* fix(keyboard-compat): register terminal arrow-key setup on tty-setup-hookCraig Jennings30 hours2-2/+13
| | | | The arrow-key decodings ran on emacs-startup-hook, but input-decode-map is terminal-local and a daemon's startup hook fires once with no tty. So every emacsclient -t frame opened later missed the decodings, and arrow keys broke in terminal frames. tty-setup-hook runs for each new tty frame, daemon and non-daemon alike. The GUI half already frame-scopes itself. A test pins the hook so a regression back to emacs-startup-hook fails.
* docs(spec): reconcile init-load-graph to the shipped LSP decisionCraig Jennings31 hours1-14/+20
| | | | The spec owned the LSP and tree-sitter decisions but still recommended consolidating under prog-lsp. That inverted what shipped: prog-lsp was dead code nothing required, so the fix folded it into prog-general and deleted it. Update the programming target, module table, and ownership note to record prog-general as the sole LSP owner and tree-sitter gated to 'prompt. The keyword stays DOING for the remaining load-graph work.
* refactor(lsp): make prog-general the single owner of LSP configCraig Jennings35 hours5-303/+242
| | | | | | | | | | prog-lsp.el was required by nothing. The (require 'prog-lsp) line was commented out and later deleted, so none of its config ever ran. prog-general's LSP block was the only policy in effect, which is why the build/cache file-watch ignore list added in April never applied and the "watch all files?" prompt kept firing. Fold prog-lsp's settings into prog-general and delete the module. This adopts its quiet, performance-first policy. Doc popups, symbol highlighting, snippets, signature help, and modeline diagnostics go off. Idle-delay drops to 0.5, the eldoc provider is stripped from the global hook, and read-process-output-max rises to 1MB. The file-watch ignore list now applies: the daemon carries all thirteen build/cache patterns, six of which lsp-mode already ships by default. Language-specific server variables and the lsp-deferred mode hooks stay in the per-language modules. Consolidate the two prog-lsp test files into one test-prog-general-lsp.el: twelve tests covering the file-watch and eldoc helpers across Normal, Boundary, and Error, plus the load-time invariants.
* fix(auto-dim): dim the remaining org faces, and test for the next oneCraig Jennings47 hours2-0/+87
| | | | | | | | | | | | Four rounds of this bug had one shape: a face nobody enumerated, sitting ahead of a mapped face in a face list and outranking it. A table header renders as (org-table-header org-table), so the mapped org-table underneath never got a say. So this round adds a test instead of another guess. It fontifies a representative org buffer, collects every face the buffer actually uses (including the line-prefix and wrap-prefix that org-indent hangs its faces on), and fails on anything neither mapped nor deliberately excluded. It caught org-checkbox while I was writing it. Newly dimmed: org-table-header, org-formula, org-checkbox, org-checkbox-statistics-done, org-headline-done, org-drill-visible-cloze-face. org-indent joins org-hide and org-superstar-leading on the -hide face. All three resolve to the background colour, which is what makes folded text, leading stars and indent prefixes invisible. Flat-dimming any of them reveals what I hid. bold, italic and underline stay unmapped on purpose. They carry no foreground even through inheritance, so they take their colour from default and dim for free.
* fix(auto-dim): dim org-superstar's heading stars and list bulletsCraig Jennings48 hours2-0/+44
| | | | | | | | Bullets were the last thing left lit in an unfocused org window. org-superstar puts its own face ahead of the org face beneath, so a heading star renders as (org-superstar-header-bullet org-level-1) and outranks the org-level-1 we already dim. Three of its four faces flat-dim. The fourth, org-superstar-leading, takes the -hide face instead: its foreground is the background colour, which is exactly what keeps hidden leading stars invisible. Flat-dimming it would reveal stars I chose to hide. A test says so, so nobody completes the set later. That closes the dimming work. The parenthesised text from the first report turned out to be org-code and org-verbatim, already covered.
* docs(spec): cancel the org workflow doctor, it has no jobCraig Jennings2 days1-2/+31
| | | | | | | | | | A doctor that refuses to install anything buys nothing that isn't already there. Every external binary is guarded at its point of use, with a message that names it: hugo-config guards hugo and the file-manager opener, org-webclipper guards pandoc, org-export-config guards zathura, and ox-pandoc guards itself upstream. Missing packages surface at load or first use. Path checks were the only new capability, and warning about them at startup contradicts the spec's own goal of keeping startup quiet, while an on-demand check nobody remembers to run is worth nothing. The spec's Problem section rested on a premise nobody verified: that none of these dependencies was checked anywhere. It came from grepping for our own warn helper by name instead of reading the modules for the behavior. The body stays as written with the correction marked, and a postmortem records that the Reuse dimension named the helper that made the feature redundant, then dismissed it in one clause. No code changed.
* fix(flycheck): lint ledger buffers, which nothing ever didCraig Jennings2 days3-12/+69
| | | | | | | | | | flycheck-ledger registers a ledger checker, but a checker only runs where flycheck-mode is on, and flycheck-mode was hooked to sh-mode and emacs-lisp-mode only. There's no global-flycheck-mode. So an unbalanced transaction in a financial file produced no warning at all, while the ledger module's commentary advertised linting. The hook goes in flycheck-config, not ledger-config, so that list stays the one answer to "where is flycheck turned on?". Verified on an unbalanced fixture: flycheck now reports "Transaction does not balance" with the $10.00 remainder, on the right line. I also accepted the whole-buffer sort the audit flagged. ledger-sort-startkey keys on the ISO date alone, and sort-subr is stable, so a save orders by date and leaves same-day transactions where they were typed. The audit note records both.
* fix(test-runner): reject a sibling dir that shares the test dir's prefixCraig Jennings2 days2-1/+23
| | | | | | | | cj/test--do-focus-add-file tested containment with a bare string-prefix-p on two truenames, so tests-old/foo.el passed as being inside tests/ and joined the focus list. The correct helper was already there. cj/test--file-in-directory-p wraps the directory in file-name-as-directory first, and the other two call sites already used it. This one didn't. The helper even had a sibling-rejection test. It was proven correct while the call site two hundred lines away never called it, which is why the new test drives the real focus-add path with a tests-old/ directory on disk rather than testing the helper again.
* fix(auto-dim): dim the built-in link faces in unfocused windowsCraig Jennings2 days2-0/+36
| | | | | | link and link-visited were absent from auto-dim-other-buffers-affected-faces, so links in help, info, and customize buffers stayed lit while the rest of the window faded. They're distinct from org-link, which the earlier org pass covered. Both carry :underline t, and the dim face sets no underline, so the relative remap drops the colour and keeps the cue. A second test pins that: if a theme ever gives auto-dim-other-buffers an :underline, dimmed links stop looking like links and the suite says so.
* test(ledger): audit ledger-config and pin its current behaviorCraig Jennings2 days2-0/+194
| | | | | | | | The linting doesn't run. flycheck-ledger loads and registers the ledger checker, but flycheck-mode is hooked to sh-mode and emacs-lisp-mode only, and there's no global-flycheck-mode. I checked a live ledger-mode buffer: checker registered, flycheck-mode nil. Unbalanced transactions and typo'd account names produce no warning, on a financial file, while the module commentary claims linting. Three smaller findings. Every save reorders the whole file, because ledger-mode-clean-buffer calls ledger-sort-buffer over point-min to point-max. The demoted error around it lets a save proceed after a partial rewrite, with nothing to roll back. Reconcile has no confirmation and clears whole transactions. Five characterization tests pin what exists now, including the clean-on-save default the audit questions. I fixed nothing: the guardrail choices are a separate task, and only the flycheck gap is a defect rather than a preference.
* docs(design): survey where a curated "?" help menu fitsCraig Jennings2 days1-0/+96
| | | | | | | | | | The mechanical half of the "?" convention task. I probed ten modes from their live keymaps rather than from source, because a package's define-key calls don't always agree with what lands in the map. Four modes have both ? and H free, so nov, eat, and the two elfeed modes can adopt the convention without displacing anything. Three bind ? to something small that adopting it would displace. mu4e-headers is the one real conflict: its ? marks mail unread, a state-mutating command in the marking family. magit already binds ? to its own dispatch. The convention isn't new, it's magit's habit generalized. I ran the probe in a throwaway batch Emacs rather than the daemon, since probing calibredb and mu4e in a live session would force-load them. mu4e isn't an ELPA package and needed its site-lisp path.
* docs(design): map Org module ownership and load boundariesCraig Jennings2 days1-0/+138
| | | | | | | | Sixteen modules make up the Org workflow, and nothing wrote down which owns what. Answering "who sets this variable?" meant grepping. The note maps each module to its behavior, the shared variables with more than one writer, and the init.el load order. The load-boundary facts are the useful part. Four modules append to org-capture-templates, four touch org-agenda-files, and five register org-protocol handlers. quick-video-capture loads twenty-four lines before org-config because it's grouped with the media modules. Every Org module loads eagerly, including the five its own headers call optional. I recorded four follow-ups in the note instead of fixing them: a tracked .bak file under modules/, a missing header on the agenda debug module, no collision check on org-protocol handlers, and that eager loading.
* docs(spec): draft the org workflow doctor specCraig Jennings2 days1-0/+231
| | | | | | | | A read-only cj/org-workflow-doctor that reports whether the Org workflow's paths, binaries, and lazily-loaded packages are present. Nothing checks them today, so a missing prerequisite surfaces mid-capture in whatever shape the first module to trip over it produces. One probed finding is why the spec exists. The obvious package check is featurep, and it's wrong here: this config loads Org packages lazily, so org-noter and org-web-tools report featurep nil while locate-library finds them. A doctor built on featurep would report false failures for exactly the packages the config defers, which teaches you to ignore it. Both decisions are resolved, so it's ready for spec-review.
* docs(specs): flip the two music specs to IMPLEMENTEDCraig Jennings3 days2-4/+6
| | | | | | I ran both live VERIFYs and found no issues: the fancy player UI (hero, serif, cover art, advancing bar, plain-text fallback) and the radio-browser queue-first model (n / t / m picks, save on v, offline and empty searches). Both specs carry the keyword, a dated history line, and the mirrored Metadata status.
* feat(music): always dock the playlist at the bottomCraig Jennings3 days3-90/+87
| | | | | | | | The F10 playlist docked as a right-side column when the frame was wide enough, splitting the frame three ways. That's annoying and often unexpected, so the frame's shape no longer decides where the playlist goes. I deleted the chooser (cj/--music-playlist-side) and the right-dock width knobs rather than leaving them wired to nothing, and a test asserts they're gone. cj/preferred-dock-direction stays, since ai-term and eat still dock by frame shape. The old side-chooser test is replaced by one that stubs cj/side-window-display and asserts what the toggle asks for, so it runs headless.
* docs(auto-dim): note that ANSI-coloured terminal spans stay litCraig Jennings3 days1-0/+8
| | | | | | The commentary claimed EAT buffers dim like any other buffer. Background and uncoloured text do, but ANSI-coloured spans keep their colour: EAT attaches them as anonymous face plists with a literal foreground, and face-remap-add-relative only reaches named faces. I went looking for the missing face name in this alist before working out there isn't one. The note says so now, and points at an overlay as the way in.
* fix(auto-dim): flat-dim the remaining org structure facesCraig Jennings3 days2-8/+64
| | | | | | | | The earlier fix covered only the eleven faces the task named, so the document header (#+TITLE:, #+AUTHOR:, #+ARCHIVE: and their values), inline markup, drawers, planning lines, tables, and the fold indicator all stayed lit in unfocused windows. I enumerated the faces a fontified org buffer actually uses, rather than adding only the ones I could see were wrong. Fifteen were missing. org-todo and org-priority stay out of the flat-dim set. They're keyword class, and dimming them would erase the status colour the org-faces-*-dim variants preserve. Two tests pin that exclusion, and a third pins org-hide to the -hide face so folded text stays hidden.
* fix(auto-dim): dim org headings, links, and tags in unfocused windowsCraig Jennings3 days2-0/+51
| | | | | | | | org-level-1..8, org-link, and org-tag were missing from auto-dim-other-buffers-affected-faces, so they stayed lit while the rest of an unfocused window faded. I flat-dimmed them rather than giving them -dim variants. The active theme gives all eight heading levels one foreground and no height or weight, so no level-by-colour signal is lost. The fork remaps relatively, so org-link keeps its underline and the stars and org-indent keep conveying depth. That premise is theme-dependent, and the comment says so. A second test pins the keyword and priority faces to their -dim variants, so a later change can't quietly flat-dim those too.
* feat(music): move radio to a C-; m r prefix, lowercase the menu keysCraig Jennings4 days2-11/+42
| | | | | | The two radio search commands were reachable only as n/t inside the playlist buffer, and R jumped straight into the manual name+URL entry. r is now a radio prefix with the same letters the playlist's Radio row uses: n searches by name, t by tag, m is the manual entry. The rest of the menu's uppercase keys moved to lowercase homes: v shows the playlist (was M), u shuffles (was Z), and repeat-playlist moved from r to l to free the radio prefix. In the playlist buffer, save moved from w to v.
* feat(music): queue radio picks and save on requestCraig Jennings4 days11-500/+729
| | | | | | | | The radio-browser lookup and the manual station creator no longer write .m3u files at pick time. Picks become url tracks in the queue, carrying the station name, uuid, and favicon as track properties, and play immediately. The display and cover-art layers read the properties first and fall back to the on-disk metadata, so existing station files keep working. Saving is the normal playlist save, now on w. The radio feature's S=stop rebind had silently shadowed the old S=save binding. An all-stream queue saves into the MPD playlist dir, and the station name pre-fills the prompt with no -Radio suffix. A custom emitter writes the station metadata back out as .m3u comment lines, since the stock EMMS writer emits bare URLs and would lose names and cover art on reload. I removed the write-at-pick machinery (station-m3u, write-stations, disambiguate-name, the -Radio filename suffix) and the orphaned safe-filename helper. An empty name at the save prompt now signals an error instead of writing a hidden .m3u.
* feat(music): fancy GUI player with hero, cover art, and bar (phase 3)Craig Jennings6 days2-48/+305
| | | | | | | | | | Phase 3 is the fancy visual layer from the prototype. In a graphical frame with cj/music-fancy-ui on, the player draws a now-playing hero (cover image, a serif amber title, a subtitle, and a progress bar) with thumbnailed serif rows below it. A TTY frame, or the toggle off, falls back to phase 1's plain text. The progress bar had a rendering but no data source. I added cj/music--mpv-get-property, which does an IPC round-trip that reads mpv's reply, so a file's bar fills from percent-pos. The old cj/music--mpv-command only sent commands and threw the reply away. A stream has no duration, so it shows "on air". The bar redraws on a ~1s timer that runs only across a playing span and skips when the buffer isn't visible. I split cj/music--header-text into shared playlist and controls pieces with a text-versus-fancy dispatcher, so the two render paths build from one place and can't drift. The hero replaces the Current line when a track plays. Rows gain a cover thumbnail and a serif name. Cover art comes from phase 2's non-blocking cj/music-art--for-track. A separate idle-timer pre-warm fetches the real art off the render path, so a slow favicon host never freezes playback start. The serif family defaults to the nov reading view's, and the amber comes from the themed warning face so the dupre theme owns it. The block-bar renderer has Normal, Boundary, and Error tests. The images, faces, and the bar advancing are a live check. The full suite is green.
* feat(music): cover-art fetch and cache for the player (fancy UI phase 2)Craig Jennings6 days7-22/+434
| | | | | | | | | | Phase 2 gives each track a local cover-image path so phase 3's GUI has art to draw. A radio station uses its logo, a local file a sibling cover image, and anything without either falls back to a shipped vinyl placeholder. I consolidated the .m3u parse into one richer cj/music--m3u-entries that reads :name, :uuid, and :favicon per station. Phase 1's cj/music--m3u-labels is now a thin projection of it, so names and art share a single cached disk read. New stations capture their favicon into a #RADIOBROWSERFAVICON line at creation, so most need no lookup later. A legacy station with only a UUID resolves its favicon through a byuuid call. The render path never touches the network. cj/music-art--for-track reads only the cache and returns the placeholder until art exists. cj/music-art--ensure does the blocking fetch off that path. A fetched response is validated as an actual image before it's cached, so an HTML error page or an empty body becomes the placeholder, not a poisoned cache entry. Only http and https URLs are fetched, so an external favicon field can't reach a file:// resource. Art lands under data/music-art/, keyed by UUID or a file hash. cj/music-clear-art-cache empties it, with no automatic expiry. The pure helpers carry the tests: the parser, the cache key, the favicon URL, and the image validation, each with Normal, Boundary, and Error cases. The fetch, the byuuid lookup, and the placeholder fallback are verified live against a real station. Local files use a sibling cover image for now. Embedded-tag extraction is deferred. The full suite is green.
* feat(music): show station and track names, not stream URLs (fancy UI phase 1)Craig Jennings6 days6-204/+435
| | | | | | | | | | The EMMS playlist showed the raw stream URL for every radio track, so the buffer read like a debug log. This is phase 1 of the fancy player: real names, still plain text. It ships on its own and becomes the fallback for the image and GUI phases. I split the old cj/music--track-description into a pure, name-only cj/music--display-name that the header's Current line and the row renderer both call, so the two never drift. A url track resolves to its #EXTINF label (read once from all the playlist .m3u files into a cached map) or a tidied host. A file shows Artist - Title or its filename. The row renderer adds a dim nerd-icon glyph and the duration as right-aligned meta with an :align-to space, so it survives a window resize. The header line stays clean: just the name plus an on-air or duration suffix. The pure helpers carry the tests: display-name, tidy-host, m3u-labels, bar-fill, format-meta, Normal/Boundary/Error each (32 tests across four files). The glyph, the aligned meta, and the disk-backed name map are the thin composition, verified live against 44 real stations. The progress bar's visual and its live elapsed source come in phase 3. The bar-fill helper is the tested pure core it will feed. The full suite is green.
* docs(specs): flip fancy music-player UI spec READY -> DOINGCraig Jennings6 days1-2/+3
|
* docs(specs): review and respond fancy music-player UI spec (READY)Craig Jennings6 days1-17/+55
| | | | | | | | | | I reviewed the fancy music-player UI spec, then dispositioned all seven findings: five accept, two modify, none rejected. Two were blockers. The progress bar had a chosen rendering and cadence but no data source: a stream has no duration, and for a local file the elapsed position isn't wired. The playing-time display is off and the mpv IPC helper is send-only. I narrowed it to mpv's percent-pos over the socket the seek commands already use, with streams rendering "on air" and no fill. That avoids reviving the EMMS timer I turned off on purpose. The named row renderer, cj/music--track-description, is also the header's now-playing source and EMMS's description function, so glyph and meta would have leaked into both. I split it into a pure name-only helper the header and the row share, with the glyph, aligned meta, and image added on the row alone. The rest tightened scope: favicon-at-creation applies to search-created stations only, fetched art is validated before caching, meta right-aligns with :align-to so it survives a resize, the placeholder is a shipped vinyl SVG with per-station tinting left to vNext, and the nerd-icons dependency gets a require and a TTY fallback. I flipped DRAFT to READY.
* docs(specs): resolve fancy music-player UI decisions (5/5)Craig Jennings6 days1-6/+7
|
* docs(specs): add fancy music-player UI spec + prototype (DRAFT)Craig Jennings6 days2-0/+369
| | | | | | | | A spec for the hi-fi look Craig picked from the prototype: the EMMS playlist buffer gains names instead of stream URLs, cover art, a serif now-playing hero with a progress bar, and a warm amber palette, degrading to plain text in a terminal frame. I verified the cover-art path is reachable (radio-browser stations carry a favicon URL, retrievable at search time or by UUID) and framed the work as three layers: a name and text base that ships value on its own and doubles as the terminal fallback, cover-art fetch and cache, then the fancy GUI render. Five design decisions are open, each with a recommended call. The three-direction prototype (minimal, fancy, modern) lands beside the spec as its companion.
* feat(music): radio row for name, tag, and manual station searchCraig Jennings6 days3-36/+77
| | | | | | | | I turned the radio-browser lookup into a Radio row in the playlist buffer: n searches by station name, t searches by tag/genre, m enters a station by hand. Tag search is new (it was a vNext item), and both name and tag hit radio-browser's search endpoint with a name= or tag= field. Freeing n and t moved two keys: single mode goes from t to s, and stop goes from s to S. The header gains a Radio row and the Mode row now reads [s] single. Created station files carry a -Radio suffix (Adroit_Jazz_Underground-Radio.m3u). The pure pieces and the search-URL field are unit-tested (20). Name and tag searches are live-verified. The picker and playback stay a manual check.
* feat(music): radio-browser search command + keybinding (phase 2)Craig Jennings6 days2-0/+176
| | | | | | I wired up the interactive side of the radio-browser lookup: cj/music-radio-search, bound to S in the EMMS playlist buffer (s stays emms-stop). It searches, lists the matches annotated with codec, bitrate, country, votes, and tags, and lets you pick several one at a time. Selection is a completing-read loop with a "[done]" sentinel that removes each pick from the pool, so a station name with a comma can't be mis-split. Each pick is written into the save directory (no-URL stations skipped and named, colliding filenames disambiguated by UUID), then the selection plays through mpv, interrupting whatever was on. The dedup and write-collect logic are unit-tested. The picker and playback are left to a manual check.
* feat(music): radio-browser search client + emitter (phase 1)Craig Jennings6 days3-2/+259
| | | | | | | | I built the first phase of the radio-browser lookup: the pure pieces and the network client behind the search command, with no UI yet. It lives in modules/music-config.el, alongside the existing radio-station creator. The client is url.el plus json-parse-string, trying a pinned host and falling back to one from /json/servers. The pure layer parses a response into station plists, and a non-JSON body signals a clear user-error rather than a stack trace. It picks a station's stream URL (url_resolved, then url), emits the .m3u in the existing radio format with the radio-browser UUID, formats the completion annotation (codec, bitrate, country, votes, tags), and disambiguates a colliding filename by UUID. Names are newline-stripped so an odd one can't inject extra m3u lines. The pure pieces are unit-tested (14). The network client is smoke-tested against the live API.
* docs(specs): review + respond the radio-browser lookup spec (READY)Craig Jennings6 days1-12/+44
| | | | | | I reviewed the spec and folded the review back in. The review found seven gaps, one blocking: completing-read-multiple splits its selection on commas, and radio-browser station names contain commas. A comma'd name would be mis-selected silently. Resolving all seven: selection is now a loop-based single-select that never round-trips through a delimiter, the emitter skips a station with no usable stream URL, same-name picks in one search disambiguate by UUID, the server client has a defined fallback, and create-and-play interrupts current playback. The annotation format is codec, bitrate, country, votes, and tags, with no play count. Decisions read 5 of 5 and findings 7 of 7, so the spec flips to READY.
* fix(recording): stop wf-recorder start-race producing 0.5s fragment filesCraig Jennings6 days3-3/+122
| | | | | | | | On Wayland the video start path signalled any running wf-recorder with pkill -INT and then waited a fixed 0.1s before launching a new one. Releasing the compositor capture takes longer than that, so the new wf-recorder launched into a still-held grab, died immediately, and ffmpeg finalized a ~0.5-second fragment .mkv. Seeing nothing record, I'd press the key again and burst out a pile of fragments until the capture finally freed. I replaced the fixed wait with a poll that waits until no wf-recorder remains (capped at 2s), the same move the stop path already made when it swapped its own fixed wait for cj/recording--wait-for-exit. Two supporting changes came with it. wf-recorder stderr no longer goes to /dev/null, so a failed grab shows up in the process buffer. And the sentinel now tells a ~0.5s failed start from a real stop, saying "failed to start, try again" instead of silently clearing, so one failure doesn't snowball into a blind-retry burst. The poll and the fail-fast timing are unit-tested. The live capture needs a manual check.
* docs(specs): add radio-browser lookup UI prototypeCraig Jennings6 days2-0/+194
| | | | | | The prototype is a standalone HTML mockup of the four minibuffer screens for the radio-browser lookup: query, station list, multi-select, and created-and-playing. I drew it with real jazz-search data in the vertico and marginalia style. It shows the two candidate-annotation formats the design picks between. The spec's References section links to it. This sets the convention for a spec's visual companion: docs/specs/<slug>.prototype.html, tracked alongside the spec.
* docs(specs): add radio-browser station lookup spec (DRAFT)Craig Jennings6 days1-0/+162
| | | | | | A spec for searching radio-browser.info from Emacs and turning a selection into a playable radio .m3u, reusing the existing writer, multi-directory sourcing, and mpv path. The player can already read and play radio playlists but has no native way to discover stations, so every station is hand-entered from a browser. I verified the API live (server discovery and station search) and matched the output to the existing radio .m3u format. All four design decisions are resolved: built-in url.el, multi-select writing one file per station, saving into the MPD playlist directory, and create-and-play. The spec has two build phases and is ready for spec-review.
* feat(music): source M3U playlists from multiple directoriesCraig Jennings6 days4-31/+155
| | | | | | | | The MPD radio-stream playlists moved into version control at ~/.local/share/mpd/playlists/, while the 24 local-library playlists stay at ~/music/. MPD has a single playlist_directory and its load only sees that one, so the union has to happen at the Emacs layer. I added cj/music-m3u-roots, a precedence-ordered list of directories the reading path unions (get-m3u-files, basenames, select, load). Missing directories are skipped. On a basename collision the earlier root wins, so a local playlist shadows a same-named radio one. Saving and radio-station creation still target the single cj/music-m3u-root. Radio .m3u hold stream URLs, which the track reader and the mpv subprocess already handle, so nothing else changed to play them. The two tests that pinned the old single-directory contract now bind the list. Their "missing directory signals an error" cases became "missing directory is skipped" to match the new behavior.