<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Aaron Sun</title>
    <description>The latest articles on DEV Community by Aaron Sun (@aaronsuns).</description>
    <link>https://dev.to/aaronsuns</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4237847%2F427db20e-d1e8-4a67-ba11-ec6e46323300.png</url>
      <title>DEV Community: Aaron Sun</title>
      <link>https://dev.to/aaronsuns</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/aaronsuns"/>
    <language>en</language>
    <item>
      <title>Three small open-source Mac tools for working next to AI coding agents</title>
      <dc:creator>Aaron Sun</dc:creator>
      <pubDate>Wed, 11 Nov 2026 21:41:26 +0000</pubDate>
      <link>https://dev.to/aaronsuns/three-small-open-source-mac-tools-for-working-next-to-ai-coding-agents-1ml6</link>
      <guid>https://dev.to/aaronsuns/three-small-open-source-mac-tools-for-working-next-to-ai-coding-agents-1ml6</guid>
      <description>&lt;p&gt;These days most of my coding happens with several Claude Code sessions running at once, plus a browser the agents sometimes need to use. Three things kept costing me time, so I built three small tools for them. They're free, MIT-licensed, and as of today public.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ex5qwkyu8p8oppe23n.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fm7ex5qwkyu8p8oppe23n.png" alt="Pullbell, Hopback and Tabrelay" width="800" height="450"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Pullbell: a GitHub inbox in the menu bar
&lt;/h2&gt;

&lt;p&gt;Review requests and comments on my PRs got lost in email, and with agents opening PRs for me there are more of them than ever. &lt;strong&gt;Pullbell&lt;/strong&gt; sits in the macOS menu bar and raises a banner only for the things that need me: someone requests my review, comments or replies on my PR, approves it or asks for changes, a PR becomes ready to merge or gets a conflict, a deploy on the default branch fails. Everything else (bots, CI runs) goes into a quiet list.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;⌃⌘G opens a searchable queue, ⌃⌘P my open PRs and the ones waiting for my review, with CI and review state.&lt;/li&gt;
&lt;li&gt;Watch everything, or only the repos of the teams you pick; mute the noisy ones.&lt;/li&gt;
&lt;li&gt;Signs in with GitHub's device flow; the token lives in the macOS Keychain.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://pullbell.zexa.se" rel="noopener noreferrer"&gt;https://pullbell.zexa.se&lt;/a&gt; · &lt;a href="https://github.com/aaronsuns/pullbell" rel="noopener noreferrer"&gt;https://github.com/aaronsuns/pullbell&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Hopback: which Claude Code session needs me?
&lt;/h2&gt;

&lt;p&gt;With five or six VS Code windows each running a Claude Code session, the question is always "which one is waiting for me?" &lt;strong&gt;Hopback&lt;/strong&gt; answers it from the menu bar: &lt;code&gt;⚠1 ⚙2 · ▢8&lt;/code&gt; means one session waits for an approval or an answer, two are working, eight windows are open.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A small Claude Code plugin (hooks) tells Hopback when a session starts working, finishes, or waits for you, and you get a banner.&lt;/li&gt;
&lt;li&gt;⌃⌥1–9 jumps to a window, ⌃⌥0 to the next session that needs you.&lt;/li&gt;
&lt;li&gt;⌃⌘C searches open sessions, ⌃⇧⌘C searches your whole history, including the conversation text. Type &lt;code&gt;?&lt;/code&gt; and a description and it asks Claude (read-only tools) to find the session you mean.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://hopback.zexa.se" rel="noopener noreferrer"&gt;https://hopback.zexa.se&lt;/a&gt; · &lt;a href="https://github.com/aaronsuns/hopback" rel="noopener noreferrer"&gt;https://github.com/aaronsuns/hopback&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  Tabrelay: let an agent use your real Chrome
&lt;/h2&gt;

&lt;p&gt;Headless browsers are great until the site needs your login. &lt;strong&gt;Tabrelay&lt;/strong&gt; is a small Chrome extension plus a CLI that lets a script or an AI agent drive your real, logged-in Chrome through the DevTools Protocol, without raising windows or stealing focus while you keep working.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;TAB&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="nv"&gt;$(&lt;/span&gt;tabrelay open https://example.com/&lt;span class="o"&gt;)&lt;/span&gt;   &lt;span class="c"&gt;# a background window; prints the tab id&lt;/span&gt;
tabrelay &lt;span class="nb"&gt;eval&lt;/span&gt; &lt;span class="s2"&gt;"$TAB"&lt;/span&gt; &lt;span class="s1"&gt;'document.title'&lt;/span&gt;       &lt;span class="c"&gt;# "Example Domain"&lt;/span&gt;
tabrelay shot &lt;span class="s2"&gt;"$TAB"&lt;/span&gt; page.png
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;ul&gt;
&lt;li&gt;Only sites on an allow-list you set in the extension's options (empty by default).&lt;/li&gt;
&lt;li&gt;A local token, and requests from web pages are refused.&lt;/li&gt;
&lt;li&gt;Includes example scripts for posting to a few social sites, with a disclaimer: what you automate is your responsibility, and each site's rules apply.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;a href="https://tabrelay.zexa.se" rel="noopener noreferrer"&gt;https://tabrelay.zexa.se&lt;/a&gt; · &lt;a href="https://github.com/aaronsuns/tabrelay" rel="noopener noreferrer"&gt;https://github.com/aaronsuns/tabrelay&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  How they're built
&lt;/h2&gt;

&lt;p&gt;Pullbell and Hopback are Tauri 2 apps (Rust + a small React UI), universal builds for Apple silicon and Intel, with signed auto-updates from GitHub releases. Tabrelay is a Manifest V3 extension and a single-file Python CLI. All three have CI and tag-driven releases.&lt;/p&gt;

&lt;p&gt;They aren't notarized yet (no Apple Developer ID), so the first launch needs one &lt;code&gt;xattr&lt;/code&gt; command; the READMEs show it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;About AI:&lt;/strong&gt; I designed these and wrote the specs; most of the code was written by Claude Code under my direction, then reviewed and tested by me.&lt;/p&gt;

&lt;p&gt;If you try one, I'd love to hear what's missing or confusing — issues on GitHub are the best place.&lt;/p&gt;

</description>
      <category>showdev</category>
      <category>opensource</category>
      <category>productivity</category>
      <category>ai</category>
    </item>
    <item>
      <title>I'm still learning Swedish, so I built a dual-subtitle extension for Nordic public TV</title>
      <dc:creator>Aaron Sun</dc:creator>
      <pubDate>Wed, 11 Nov 2026 00:50:17 +0000</pubDate>
      <link>https://dev.to/aaronsuns/im-still-learning-swedish-so-i-built-a-dual-subtitle-extension-for-nordic-public-tv-3he1</link>
      <guid>https://dev.to/aaronsuns/im-still-learning-swedish-so-i-built-a-dual-subtitle-extension-for-nordic-public-tv-3he1</guid>
      <description>&lt;p&gt;I'm a developer living in Sweden, and I'm still learning Swedish. The best practice material I've found isn't a course: it's public TV. SVT Play, UR, NRK and DR have huge free libraries, and almost every programme comes with subtitles in the original language.&lt;/p&gt;

&lt;p&gt;The problem is the player. It shows one subtitle track at a time, and there is no subtitle in my language. Swedish-only subtitles are too hard at the start, and pausing to look things up kills the flow. Tools like Language Reactor solve this for Netflix and YouTube, but not for the Nordic broadcasters. So I built &lt;strong&gt;Nordic Dual Subs&lt;/strong&gt;: a free browser extension that shows the broadcaster's own subtitle line together with a translation in your language, perfectly in sync.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxflty1k58850yfqk2hgf.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fxflty1k58850yfqk2hgf.jpg" alt="Two subtitle lines on SVT Play: the original Swedish line and a translation under it" width="800" height="490"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Two lines at once&lt;/strong&gt; on SVT Play, SVT Barn, UR Play, NRK TV and DR TV: the original Swedish, Norwegian or Danish line, and a translation (29 languages, including Arabic, Persian, Somali, Tigrinya, Ukrainian and Chinese, right-to-left included).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Click any word&lt;/strong&gt;: the video pauses and you get the meaning and the base form (&lt;em&gt;sprang → springa&lt;/em&gt;), with an English gloss from the open Folkets lexikon dictionary.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Save words with their sentence&lt;/strong&gt;, review them with simple spaced repetition, or export them to Anki as CSV.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Practice keys&lt;/strong&gt;: A / S / D for previous / repeat / next line, Shift+S replays the line slowly.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Burned-in subtitles&lt;/strong&gt;: some foreign films have the Swedish text in the picture. The extension reads it locally with Tesseract (WebAssembly) and translates it.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it works (the parts that were interesting to build)
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Getting the subtitles.&lt;/strong&gt; Each broadcaster exposes its subtitle files (WebVTT and friends) through the same APIs its own player uses. The content script works out which programme is playing (all five sites are single-page apps, so it follows URL changes), the background worker fetches and parses the subtitle file, and an overlay is drawn inside the site's own player so it also works in fullscreen. The site's own subtitles are hidden so nothing overlaps.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Translation without a translation bill.&lt;/strong&gt; I wanted this to run at zero cost. Each browser translates lines with Google's free web endpoint from its own IP, and sends &lt;code&gt;[line, translation]&lt;/code&gt; pairs to a small backend. A line becomes &lt;em&gt;trusted&lt;/em&gt; only when installs from &lt;strong&gt;two different networks&lt;/strong&gt; submitted the identical translation (Google is deterministic, so honest clients agree, and one client can't plant a translation). Trusted lines are then served to everyone, keyed by a hash of the line, so the server never hands out subtitle text to someone who doesn't already have it. When Google rate-limits a user, a capped DeepL fallback covers the lines near the playhead.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The backend is Cloudflare's free tier.&lt;/strong&gt; One Worker plus a D1 (SQLite) database: anonymous installs with HMAC session tokens, the shared translation cache, optional Google sign-in for syncing saved words, and Stripe for a future optional Pro plan. Free tiers have hard daily limits, so a small GitHub Action checks D1 and Workers usage every morning and mails me before anything gets close. One lesson: my end-to-end tests were quietly writing thousands of rows to the production database. They now run against an in-process mock API, with a guard that fails the test if anything tries to reach production.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;One codebase, three stores.&lt;/strong&gt; The same source builds a Chrome/Edge package and a Firefox package. The Firefox build swaps the MV3 service worker for background scripts, drops the &lt;code&gt;offscreen&lt;/code&gt; API (OCR runs in Firefox's background page instead, which has a DOM), and declares Firefox's data-collection consent. Firefox for Android gets touch controls (⏮ ↺ ⏭ 🐢) instead of keyboard shortcuts. Publishing goes through each store's API, with one catch: Microsoft only lets the API publish an Edge extension after its first version was published by hand.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Things outside my control.&lt;/strong&gt; DR TV only plays when you're logged in, and some programmes on all three broadcasters are geo-restricted to their own country. The first user email I got was exactly that: "it doesn't work on DR TV". The extension now says &lt;em&gt;why&lt;/em&gt; (log in to DR, or "this programme is only available in Denmark") instead of staying silent.&lt;/p&gt;

&lt;h2&gt;
  
  
  Built with a lot of AI help
&lt;/h2&gt;

&lt;p&gt;Full disclosure: I built this with heavy help from an AI coding assistant. It wrote much of the code and the tests, ran the end-to-end tests against real SVT and NRK programmes, and handled most of the store paperwork. I decided what to build, tested it on my own TV evenings, and pushed back when something was wrong. It's been a fun way to ship a side project in the evenings without cutting corners on tests.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;

&lt;p&gt;It's free (everything is free during the launch period; the dual subtitles stay free), no account needed.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Website and guides (16 languages): &lt;a href="https://dualsubs.zexa.se?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;https://dualsubs.zexa.se?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Chrome Web Store: &lt;a href="https://chromewebstore.google.com/detail/ejmbcllddngiakhpcpajkajeghmdapko?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch" rel="noopener noreferrer"&gt;https://chromewebstore.google.com/detail/ejmbcllddngiakhpcpajkajeghmdapko?utm_source=devto&amp;amp;utm_medium=article&amp;amp;utm_campaign=launch&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;Edge and Firefox (desktop + Android) versions are in store review right now.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you're learning a Nordic language, I'd love to hear what's missing. And if you've built something similar for other broadcasters, I'm curious how you handled subtitles and translation costs.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>showdev</category>
      <category>javascript</category>
      <category>cloudflare</category>
    </item>
    <item>
      <title>I replaced Spotify for my family with a music server I built, and its own phone apps</title>
      <dc:creator>Aaron Sun</dc:creator>
      <pubDate>Tue, 10 Nov 2026 20:29:04 +0000</pubDate>
      <link>https://dev.to/aaronsuns/i-replaced-spotify-for-my-family-with-a-music-server-i-built-and-its-own-phone-apps-2f8m</link>
      <guid>https://dev.to/aaronsuns/i-replaced-spotify-for-my-family-with-a-music-server-i-built-and-its-own-phone-apps-2f8m</guid>
      <description>&lt;p&gt;I ran Navidrome at home for a long time. The server was never the problem. The phone was: every client I tried had its own bugs, and they always showed up in the same two places, playing in the background and playing in the car. My family uses music mostly in the car and on walks, so "it works when the screen is on" wasn't good enough.&lt;/p&gt;

&lt;p&gt;So I built my own server and its apps together, and called it &lt;strong&gt;Melarka&lt;/strong&gt;. It has replaced Spotify for us. This post is how it's put together and what I learned. It's open source (AGPL-3.0): &lt;a href="https://github.com/aaronsuns/melarka" rel="noopener noreferrer"&gt;github.com/aaronsuns/melarka&lt;/a&gt;.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;AI disclosure:&lt;/strong&gt; Melarka was built with a lot of help from Claude Code, and this post was drafted with Claude too. I designed it, steer it, review what goes in, and test it on our phones every day.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8mnnkyvgkaj99swesx8.jpg" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Ff8mnnkyvgkaj99swesx8.jpg" alt="Melarka on a phone: home, now playing with synced lyrics" width="585" height="1266"&gt;&lt;/a&gt;&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Your folders, untouched.&lt;/strong&gt; Point it at the music you already have. It never rewrites files or tags; titles, tags, lyrics and fixes live in its own database.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;An account per person.&lt;/strong&gt; Everyone at home gets their own favorites, playlists, personal radio and nightly picks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Synced lyrics&lt;/strong&gt;, and anyone can nudge lyrics that run early or late.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Native apps.&lt;/strong&gt; An Android app in Kotlin and an iPhone app: background play, lock-screen and car controls, gapless, and an offline cache of favorites.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Car autoplay.&lt;/strong&gt; When the phone connects to the car over Bluetooth, the favorites start playing by themselves, and the current lyric line shows on the car's display.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Optional YouTube.&lt;/strong&gt; Search YouTube or follow channels, and download songs into your own library with yt-dlp.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  How it's built
&lt;/h2&gt;

&lt;p&gt;The server is one Go binary with SQLite, shipped as one Docker container. Install is one command:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -fsSL https://raw.githubusercontent.com/aaronsuns/melarka/main/deploy/install.sh &lt;span class="o"&gt;|&lt;/span&gt; sh -s -- /path/to/your/music
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It streams the original file, or transcodes to AAC on the fly with ffmpeg when the phone needs it.&lt;/p&gt;

&lt;p&gt;The web app is React. The phone apps are a mix that turned out to be the main design decision: &lt;strong&gt;the screens are the server's own web app, and playback is native.&lt;/strong&gt; The page sends "play this queue" to the native side over a small bridge; the native player (Media3 on Android, AVFoundation on iOS) owns the audio, the lock screen and the car.&lt;/p&gt;

&lt;p&gt;That split is why it works in the car. The web page can die, the screen can be off, the app can be in the background, and the music keeps going, because nothing about playback depends on the page. And every new server feature shows up in the apps on day one, because the screens come from the server.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that took the longest
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Never starting music on its own.&lt;/strong&gt; A player that resumes when a Bluetooth device connects is a nice feature in the car and a terrible one at 6 a.m. in a quiet house. The rule now: only a person (or the car autoplay they turned on) starts sound, and there are tests for every path that can start playback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Offline that actually works.&lt;/strong&gt; The favorites are cached on the phone and the player falls back to them when the network goes, instead of stopping or skipping through the whole queue.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Emulator tests in CI.&lt;/strong&gt; The Android app has an end-to-end test on an emulator against a real server: sign in, play a favorite, kill the page's renderer, background the app, go offline. Keeping that test reliable on CI machines was its own project.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The honest catch
&lt;/h2&gt;

&lt;p&gt;Melarka has its own API, so it is &lt;strong&gt;not Subsonic-compatible&lt;/strong&gt;: your existing Subsonic clients won't work with it, and it only makes sense if you want its apps. It's also young, about a month public. The Android APK is on the GitHub releases page (F-Droid is submitted but waiting until the app is older), and the iPhone app installs with SideStore, not the App Store.&lt;/p&gt;

&lt;p&gt;If you run a home server and your family still pays for Spotify, I'd love to hear what you think, and especially what breaks. A one-minute tour is on &lt;a href="https://melarka.zexa.se" rel="noopener noreferrer"&gt;melarka.zexa.se&lt;/a&gt;.&lt;/p&gt;

</description>
      <category>selfhosted</category>
      <category>opensource</category>
      <category>go</category>
      <category>android</category>
    </item>
  </channel>
</rss>
