Jumping from a music player into Emacs with emacsclient
- YT :: https://www.youtube.com/watch?v=580DxF2M7EE
- Original title :: Integrating Emacs with Music Player
A recreational programming session on Sowon2, the custom Jai music player Tsoding runs during stream breaks. The feature: click a song's thumbnail and have the editor open the exact line of the playlist .ini file that defines it. The idea is stolen from Jonathan Blow's codex_view example, which calls emacsclient the same way. It looks like a one-liner and turns into source-location tracking in the INI parser, a compiler-assisted refactor of the UI widget API, external process spawning, and a hot-reloaded config entry — a small feature that touches every subsystem.
The starting point
Playlists are a plain .ini file: each section name is a song ID, with fields for thumbnail, track file, artist, title and a Bandcamp link rendered as a QR code, plus playlist membership. There is no playlist UI. Instead of building one, he cut the corner both ways: paste a song ID from the INI into the player and it jumps to that song; click a thumbnail and the ID is copied to the clipboard so you can find it in the file. Today's work replaces the second half with a direct editor jump.
Locations through the INI parser
The parser threw the file away after parsing — "who needs that stinky nerdy file". To jump anywhere he needs locations, but only for sections, not individual fields. The INI type is Table mapping name to section, and he cannot add a field to a type he does not own, so it becomes a struct wrapping the table plus a Source_Code_Location.
He notes with approval that Jai ships that type in the compiler's own modules — the same type the compiler uses for its own diagnostics. Few languages provide one.
Filling it in is easy because the parse loop already has the file path and line number. The column falls out of arithmetic: the trimmed line's data pointer minus the raw line's data pointer gives the leading whitespace. Both line and column need +1 because editors count from one.
The widget refactor
Clicking is handled by place_texture_element, which took a text argument and copied it to the clipboard itself when clicked. That is one hardcoded action baked into a general widget. He rips the text out and returns a plain boolean — clicked or not — leaving the caller to decide what to do. It now behaves like a button.
His self-assessment, at length: "I don't know why I shoved it inside of the function... I could not predict that I'll need this kind of stuff and I just hardcoded the thing." A detail he spots while touching the call sites: the thumbnail is placed by two separate calls (real thumbnail, default fallback), so the result needs an accumulating clicked flag rather than one return value.
The recurring theme: each change kickstarts a compiler-assisted refactor, and by mid-stream two are running at once. The point he draws out of it —
Estimating the amount of work you need to do for the task goes in parallel with implementing the task. Implementing the task is estimating the task.
— followed by the sharper version: why is the process of software development managed by people who don't understand the process of software development?
A Jai plugin detour
A print call with the wrong number of arguments failed at runtime rather than compile time, which he was certain used to be checked. Cause: the check lives in a metaprogram plugin (Check) that keys off a print_like note on the function, and the default metaprogram loads it automatically — but a custom first.jai does not. Fix: fetch the plugin, call do_error_checking inside the message loop and finish_error_checking after. He calls this the most inconvenient way to use plugins, and immediately grants that the language is unfinished, which is precisely why Jai is not public: one person complaining is easy to ignore, a hundred thousand is not.
Also learned: the check only fires when the format string is a literal, not a constant holding the same string.
Calling the editor
Jai's run_command from the Process module does the spawning. He is briefly scandalised that Blow's codex_view interpolates the file path straight into a format string, then talks himself out of it: in C that is dangerous because the format specifier declares the type and a mismatch misinterprets memory; in Jai the types come from the values, so you cannot confuse the language that way. Worst case is a runtime complaint in the log — an acceptable failure mode.
First attempt froze the whole GUI: emacsclient waits for the buffer to be closed. -n (return immediately) fixes it. Failure with no server running is fine — it logs and the app moves on.
Config, and Vim
The command line goes into the hot-reloadable runtime config as editor.command_line, e.g. emacsclient -n +%1 %2. Splitting it into argv uses break_command_into_strings from the process module, whose doc comment cheerfully admits it should be more robust — it handles quotes only at the start of an argument, no backslash escapes. He notes his own shlex.h for C does the same job and round-trips (split and join), but relies on Blow's since Sowon2 is not being released. Empty command line falls back to the old clipboard-copy behaviour.
Vim works too, after a fight. Plain vim +LINE file hijacks the terminal and blocks the UI, exactly as expected. --remote needs a running instance started with --servername, and --remote itself takes no server-name parameter — the pairing is vim --servername NAME --remote file, which he calls confusing as hell. Chat helped. Switching between Emacs and Vim happens live via config hot reload, with no restart.
An aside that lands: after digging through Vim's client/server machinery — "after that people make fun of Emacs for being too bloated? Vim has literally feature parity with Emacs. It's literally Emacs."
He also checks whether gf2 (an Emacs-less GDB frontend wrapper) uses the same mechanism for its GVim sync, hoping to steal a jump-to-location trick. It turns out to only run :ls and :pwd remotely to discover open files, not jump.
Odds and ends
- The stream was scripted, in the sense that the whole plan was already written as an issue in Tatr, his own C issue tracker — closed at the end of the session. A
streamtag holds candidate topics; a new one was added on the spot: on a config syntax error, jump to the offending line using this same mechanism. - Immediate follow-on realisation: the same click-to-jump can point at the style config, not just the playlist. "The possibilities are endless."
- On null-terminated strings, needed to talk to raylib: a single bad decision half a century ago is now infrastructure. And no, AI will not rewrite it away — "what code do you think the AI was trained on? It's deep in the embedding space."
- On selling Sowon2 to other streamers: no. It is "aggressively optimized for Tsoding streams" and unconcerned with usability elsewhere; generalising it would turn it into an abstract scriptable multimedia framework and put him in competition with StreamElements. Enterprise software is bloated because every user has their own annoying use case.
- The genuinely happy take on being replaced: everyone can generate their own code now, so nobody needs him, so he can finally program for himself and does not even have to open source anything — point Claude at the YouTube video instead.
- His HHKB has survived ten years, tea, and disassembly; the replacement cables keep dying within a year. Planned obsolescence.