• Django

    ,

    LLM

    ,

    Today I Learned

    How I Work From Anywhere Without Losing My Place

    I’ve been running a new remote development setup for the last month or so, and the goal is simple: move between all of my devices without losing my place.

    The way I think about it, I have three default states:

    • At my desk on my Mac Studio
    • Home or out somewhere with my laptop or an iPad and a portable keyboard
    • Only my phone, at practice or running errands

    I wanted a setup that works across all three without rebuilding anything when I switch. I’d rather grab my iPhone or iPad with a portable keyboard than lug a laptop everywhere. If I’m at one of my kids’ practices and want to quickly follow up on something or implement an idea, I always have my phone with me.

    The stack is basically:

    • Tailscale - glues all of my devices onto one secure network
    • tmux - keeps my dev environment alive so I can leave and come back without rebuilding
    • Mosh - keeps remote connections resilient across flaky WiFi and network changes
    • cmux - Ghostty + Workspaces for macOS, the desktop side of this
    • Moshi - the best iPhone and iPad shell I have found

    The first three are the foundation. They work the same no matter which device I’m sitting at. cmux is how I connect from a desktop or laptop, and Moshi is how I connect from my phone or iPad.

    Tailscale ties the machines together

    Start with Tailscale. I’ve used Tailscale for almost a decade because it’s free, has clients for everything like my phone, iPads, Macs, Linux boxes, and even routers and TVs. It’s a low-friction way to access all of your devices from anywhere securely and privately.

    Tailscale gives me a private network across my devices, so I can connect to my Mac Studio at the office, my Mac Mini at home, my MacBook Air, and Linux boxes without opening SSH to the public internet.

    That’s a big part of why I’m comfortable with this setup.

    These machines are not sitting there with public SSH ports open. They’re only reachable from my tailnet. Tailscale handles the secure network piece, and I don’t have to mess with WireGuard directly.

    It just works.

    tmux keeps the work alive

    tmux is the core of the setup. I’ve been running it for the last couple of months because I wanted something I could reconnect to later, similar to screen, but better suited to the way I actually work.

    A lot of the time, I have something running for work, a client, or one of my own projects. I might have Django in one pane, logs in another, and Claude Code or Codex running somewhere else. With tmux, I can leave that entire development environment in place and come back to it later. Being able to do that has been worth the switch on its own.

    I’m probably not using 90% of what tmux was built for. Mostly I’m using it to create sessions, connect to them, and disconnect without losing my place. I can split panes and move between them when I need to, but I find myself doing that less and less. The persistence is the part that actually matters to me.

    Some alternatives to tmux are Zellij and Herdr. I attempted to use Zellij, but struggled with the basics, so I bailed and went back to tmux. I haven’t tried Herdr, but I’ve seen people talking about it and wanted to include it.

    Mosh makes remote sessions feel less fragile

    The bigger improvement came when I added Mosh.

    Before I left for PyCon US, I decided I didn’t want to spend the week fighting flaky hotel, airport, and conference WiFi.

    I remembered that Mosh solves a lot of the disconnect problems SSH has, so I installed it on my server, my work Mac Studio, my home Mac Mini, and my MacBook Air before I left (Homebrew on macOS, your package manager of choice on Linux).

    I connected to a session before getting on the plane. About an hour into the flight, I opened my laptop, and the session resumed automatically. I didn’t have to rebuild anything or start over. It worked better than I thought it would.

    Mosh and tmux make a great pair. Mosh keeps the connection resilient, and tmux keeps the actual working state alive.

    On a desktop or laptop, cmux ties it together

    When I’m at a real keyboard, cmux is how I connect. It gives me Ghostty terminals and workspaces on macOS, which makes it easy to keep several machines and sessions organized at once.

    Where cmux really earns its place is how it handles agents and shared sessions. One workflow I’ve come to rely on: I’ll start a tmux session on a remote machine, then connect to it in a second terminal window in cmux. Then I’ll have Claude or Codex connect to that same machine and attach to the same tmux session to do the actual work, like setting up a new Linux box or installing packages.

    The nice part is that when Claude hits an interactive prompt, I can just take over the tmux session directly. I’m already attached. I can paste in API keys, answer prompts, or make changes that I wouldn’t want to hand off to an agent. Claude normally tries to handle those moments itself, and it doesn’t always do it well. This way I stay in control of the sensitive parts without losing the automation for everything else.

    On my phone or iPad, Moshi is the missing piece

    When I only have my phone or iPad with me, the part I was missing was a good way to connect to my remote sessions.

    I had tried Termius before, and it was decent, but I kept running into small UI issues. CLI apps are not really designed for phones and tablets, so little things like getting to the right key at the right time can become annoying fast.

    Then I tried Moshi.

    Moshi is an iOS and iPad app for remote terminal connections. It also runs on Apple Silicon Macs as an iPad app, which is a nice bonus.

    Moshi has really good tmux support, and probably other multiplexers I haven’t tried yet. When I connect to a server that has tmux sessions running, Moshi prompts me with a list of those sessions before I even get to a shell. I don’t have to remember the command or go hunting around. I just pick the session and I’m back where I left off. That’s the feature that made the whole setup click.

    Moshi also has a good mobile UI for this kind of work. I created a couple of shortcuts, one for space and one for enter, and that gets me through most of what I need when I’m using Claude Code or Codex from my phone.

    Codex still has a few spots where it wants escape, and I haven’t fully figured that out yet. That may have been a connection issue, or it may be something I need to configure better.

    But for most of what I’m doing, it works shockingly well. I can open my phone, connect to an existing tmux session, check logs, nudge an agent, review what changed, or keep something moving without opening my laptop.

    Setup was easier than expected

    Moshi also handles setup nicely.

    When you connect to a server, it can show a QR code. You scan that from your phone, and it handles creating and copying the SSH key for you.

    That makes setup about as easy as I think it can be.

    On Apple devices, Moshi can also share connection settings across devices on the same Apple account. Once I had it set up, connecting from my iPad was painless. I hit the button, picked my session, and I was in.

    There is an Android version too. I haven’t tested it because I don’t have an Android device, but it’s there if you need it.

    Moshi is a paid app, unlike the rest of this stack, which is free or open source. It runs around $30 a year or around $80 for lifetime access, which felt reasonable given how much I’m using it.

    Voice input

    Moshi also supports voice input, which ended up being a bigger deal than I expected.

    On my Mac, I already use voice input a lot. I use tools like MacWhisper because voice to text has become a really good input method for me.

    Moshi brings that same idea to my phone and iPad.

    It can use Apple’s voice to text, and it can also run a Whisper model. That means I can connect to a remote session from my phone and use voice input to interact with the tools I already have running.

    Claude Code has voice support on the Mac, but that doesn’t really help when I’m remotely connected into another machine. Moshi having voice support built in makes the whole thing much more useful.

    Where I’m using it

    So far, I’ve been using this setup on Django TV, my job board, client work, and a bunch of one-off projects.

    I have a Mac Studio at the office and a Mac Mini at home, and I’m constantly moving between them from my MacBook Air. I’ve also been playing with direnv so I can jump into a project and have the right environment load on the right machine. It’s not really part of this stack, but it pairs well with it.

    The split helps me keep things organized. Client work lives on the Mac Studio at the office. Side projects and personal work live on the Mac Mini at home. I can connect to whichever one I need without mixing everything together on my laptop. It also helps my MacBook Air battery, because the heavier work is happening somewhere else.

    And because the sessions are always running, I don’t have to worry about rebooting my MacBook Air or losing a Claude Code or Codex session mid-task. If one machine is having a problem, I can exit and switch to the other one while everything I care about stays up and running.

    Review has also gotten easier. I can pull up GitHub’s web interface or the GitHub desktop app from wherever I am and review pull requests without needing my full dev environment in front of me. The work is already staged. I just need a browser.

    I can still work locally when I want to. There’s nothing wrong with that. But keeping these remote sessions open all the time has been a much better experience.

    The databases are already connected. The processes are already running. The agents are already in context. I can leave things in progress and come back later without rebuilding the whole setup.

    The glue code

    I’ve also been writing some shell aliases and scripts to smooth over the rough edges, because I don’t want to memorize tmux’s command flags.

    I have a set of tmux-* aliases that handle the things I actually do: create a new session, resume an existing one, and kill one when I’m done. That covers most of what I need without having to look anything up.

    On the cmux side, I’ve been experimenting with its CLI and API. I wrote cmux-dump and cmux-restore commands to save and reload workspace layouts. The idea is to build out profiles for each project so I can get everything running the way I want it with one command. That might mean starting a tmux session on a remote machine and connecting to it over Mosh, all from a single alias.

    All of this lives in my dotfiles repo and will keep changing as I figure out what actually works. I’m still early in this part of the setup.

    When and why you need each tool

    You don’t have to adopt all five of these at once. Each one solves a specific problem, and you can add them as you hit that problem.

    • Tailscale is the one to start with if your machines aren’t already on a shared network. You need it the moment you want to reach a machine that isn’t sitting in front of you without exposing SSH to the public internet. If all of your work happens on one machine, you can skip it for now.
    • tmux is what you need when losing your terminal state hurts. If closing a laptop lid or dropping a connection means rebuilding your dev environment, restarting servers, or losing an agent session mid-task, tmux fixes that. It’s also the piece that lets two terminals, or you and an agent, share the same session.
    • Mosh matters when your network is unreliable. Hotel WiFi, airports, conferences, tethering, or just moving between home and office. If you only ever connect over a stable wired network, plain SSH is fine. The moment you roam, Mosh is the difference between a session that survives and one you have to reconnect constantly.
    • cmux is for the desktop side, when you’re juggling several machines and sessions and want them organized into workspaces instead of a pile of terminal windows. Any terminal works here, but cmux makes the multi-machine setup easier to live in.
    • Moshi is for the phone and iPad side. You need it when you want to check on or nudge work from wherever you are. Its tmux session picker, key shortcuts, and voice input are what make a phone a usable terminal instead of a frustrating one.

    The short version: Tailscale gets you to the machine, tmux keeps the work alive on it, Mosh keeps the connection from breaking, and cmux and Moshi are the front doors from a desk and from a phone.

    This is not about working all the time

    The real reason I like this setup is not because I want to maximize every spare minute of my life.

    It’s almost the opposite.

    This setup gives me more flexibility.

    If I have 15 minutes at my son’s track meets or baseball practice, I can pull out my phone and reconnect to something already in progress. If I’m waiting on slow tests, client feedback, or a deploy, I don’t have to sit glued to my desk.

    I can make dinner. I can pack lunch. I can pick up one of my kids. I can run an errand.

    Then, when something is ready, I can check in, make the next change, and keep moving.

    It makes the work less jarring. I don’t have to reset everything on my laptop just to make one small change or direct an agent for two minutes.

    The best part is that I can just close my laptop or walk away from my desk and know that my Claude and Codex sessions are going to keep running. When I come back, whether it’s on my phone or back at my desk, I can resume right where I left off without worrying about a disconnected session or a timed-out agent.

    Why this feels different

    The built-in remote options from tools like Claude Code and Codex work sometimes, but I kept running into connection weirdness when one laptop was trying to coordinate across multiple machines.

    What I wanted was something that didn’t require rebuilding context every time I moved. This stack gives me that. The connection doesn’t break. The session doesn’t disappear. The agents don’t lose their place.

    That’s a different kind of flexibility than I had before. It’s not about squeezing more work into more hours. It’s about not paying a tax every time I pick up where I left off.

    This is what’s working well for me right now, but I’m genuinely curious what you’re using. If you have a tool that replaces one of these or collapses a few layers into one, I want to hear about it. If you try any of this and it works for you, let me know. And if I’m holding one of these tools wrong and there’s a better way, I’m all ears for that too.


    Written by Jeff via a mix of voice transcription and typed notes. Edited with Claude Code.

    Saturday June 13, 2026
  • Django

    ,

    Python

    ,

    LLM

    ,

    Today I Learned

    Django Chat Podcast: E201

    This week, I joined Will Vincent and Carlton Gibson on the Django Chat podcast for DjangoCon Europe Recap + Other News. You can also watch it on DjangoTV via YouTube.

    On this episode, we discussed DjangoCon Europe, which Carlton had attended. We caught up on what’s going on with Django News and the Django Software Foundation. We also spent a good chunk of time on AI workflows and tooling. And I got to have a little soapbox rant at the end, which is always fun for me.

    • Pydantic AI is the Django ORM for accessing LLM providers
    • CodexBar - If you are on a Mac, CodexBar will let you easily track and see your token usage across 20+ providers
    • Ghostty - After almost two decades of using iTerm2, I switched.
    • cmux - Built on Ghostty but a nice UX to switch between Agent sessions
    • MacWhisper - Still my favorite voice to text Mac app

    At the end of the episode, I recommended these two books:

    Conclusion

    I’m always excited and honored to be invited on the show, and I always enjoy it. I hope it was a good listen, and I especially appreciated getting to step on the soapbox one more time at the end.

    Thursday April 30, 2026
  • Today I Learned

    Jordan Baird's Ice beta fixed my macOS Tahoe menu bar issues

    If you use a Mac, you’ve probably noticed that the menu bar fills up with icons pretty quickly. Bartender and Ice (sadly, now an unfortunate name) are apps that let you manage and hide unwanted icons from your macOS menu bar so it stays clean and uncluttered.

    About a year ago, I switched from Bartender to Ice, which just happens to be open source, because there was some drama. Since then, I’ve been very happy with Ice.

    A couple of months ago, I upgraded to macOS Tahoe, and my menu bar stopped working correctly. The icon colors were all this weird shade of blue, so I couldn’t customize anything. After months of trying to figure it out, I noticed that it had been a while since Ice released a new version. That’s how open source goes sometimes.

    I was getting to the point of deciding whether to go back to Bartender or stick with Ice. Today, I noticed that there’s a Homebrew package for jordanbaird-ice@beta. I decided to give it a try, removed the old version, installed the beta, and to my surprise and delight, the problem was fixed.

    # remove the stable version
    brew remove jordanbaird-ice
    
    # install the beta
    brew install jordanbaird-ice@beta
    

    I’m hoping there’s a more official release soon. The new Bartender looks good too, so if Ice doesn’t keep getting updated, I might switch back. If you’ve run into this same issue, give the beta a try and let me know how it goes. And if you know of any alternatives to Bartender and Ice, I’d love to hear about those too.

    Thursday February 19, 2026
  • Today I Learned

    My displays keep rearranging and displayplacer fixed it

    Today I learned about displayplacer - “macOS command line utility to configure multi-display resolutions and arrangements.”

    In December, I upgraded to MacOS Tahoe and picked up the TS4 dock for my work machine.

    While my upgrade was painless, the biggest issue I’ve had is with my displays. Every time my displays go to sleep and then I come back from lunch or come back the next day to work, my orientation of my displays changes and it puts them in a random order.

    My displays should be arranged like this:

    [1] [2]
    [3] [4]
    

    But after waking from sleep, they end up like this:

    [2] [1]
    [3] [4]
    

    It’s really annoying. It takes a minute to fix and it’s just frustrating.

    I tried displayplacer today. I had Claude Code actually run the displayplacer list command. It figured out which displays and which settings needed to be changed. I had it record that and then I tried it out a few times where I manually dragged my displays around, put them in the wrong order, then ran the command, and it just magically worked.

    I saved the working configuration to a shell script:

    #!/usr/bin/env bash
    # Restore display layout: 4 monitors (2x 16" 1920x1080, 2x 73" 2048x1280)
    
    displayplacer \
      "id:F4AB0D6C-8E85-4E84-B5AB-C5B388536E3D res:1920x1080 hz:60 color_depth:8 enabled:true scaling:off origin:(0,0) degree:0" \
      "id:C9240C8E-A9D2-418A-89AC-28D3B5DEE5FC res:1920x1080 hz:60 color_depth:8 enabled:true scaling:off origin:(-1920,0) degree:0" \
      "id:B43E3352-ACB7-4163-A25B-2DDAE0174571 res:2048x1280 hz:60 color_depth:8 enabled:true scaling:off origin:(0,-1280) degree:0" \
      "id:B32F530C-62CF-4F0D-9997-80BF2B812AC8 res:2048x1280 hz:60 color_depth:8 enabled:true scaling:off origin:(-2048,-1280) degree:0"
    

    I also tried setting it up as a Stream Deck key and for some reason I couldn’t get that to work. But it’s fine. I can run a shell script and that takes a lot less time to get it in the right order.

    Wednesday February 18, 2026
  • LLM

    ,

    Today I Learned

    🤖 Trying Out GLM with Claude Code

    My friend Trey Hunner showed me the GLM set of models before Thanksgiving. While traveling to see family, I somehow messed up my Claude Code setup because of a wrapper I have with mise-en-place. I couldn’t use it for a while, and that made me realize I really need a backup for Claude Code.

    Why GLM?

    Z.AI’s GLM model is better than Gemini but not quite as good as Claude Code. It’s really fast (about twice as fast as Claude Code), seems like 90+ percent as good, and it’s really cheap. You can get an annual subscription for around $26-28 for the first year, and I’ve read online that it’s very hard to hit the limit even on the lowest tier subscription.

    This is a great option for folks who can’t quite justify the $100/mo Claude plan but occasionally hit limits on the $20/mo Claude plan. I actually subscribe to the $100/mo plan, but I ended up getting the GLM Pro Plan because I like the amount of usage it gives me. The plan I’m on has five times as much token usage as the base tier.

    Running Both Side by Side

    There’s a wrapper tool that lets you use Z.AI’s models as Claude Code’s backend, so you can run both Claude and Z.AI’s GLM in two different windows. Having a reasonable backup that I can switch between or run in parallel is nice.

    I never find speed to be an issue with Claude Code, but I’m definitely a fan of having options.

    API Access

    Claude doesn’t include API key access without paying extra. GLM solves this problem by giving you a raw API for a set price per month. Their API is OpenAI compatible, so I can work on agentic scripts using Open Coder or other applications without paying extra for Anthropic tokens.

    Getting Started

    To get started, run:

    bunx claude-glm-installer
    

    This will give you bash aliases that let you run various GLM models while still letting Claude default to the Anthropic backend:

    ccg              # Claude Code with GLM-4.6 (latest)
    ccg45            # Claude Code with GLM-4.5
    ccf              # Claude Code with GLM-4.5-Air (faster)
    cc               # Regular Claude Code
    

    Trey also told me about claude-code-router, which lets you change models on the fly without restarting Claude Code. This might work well if you want to switch between models mid-session.

    If you find yourself running out of Claude Code tokens a couple of times a week or even a couple of times a day, it might be worth checking out GLM as a backup or alternative.

    If you want to try out Z.AI’s GLM models, please use my invite link (affiliate) or feel free to use Trey Hunner’s invite link (affiliate) instead.

    Resources


    Written by Jeff. Edited with Grammarly and Claude Code.

    Wednesday December 10, 2025
  • Python

    ,

    Today I Learned

    Default Apps 2025

    Here are my Default Apps 2025, which builds from my Default Apps 2024 and Default Apps 2023 posts.

    Commentary

    The biggest changes for me this year were the addition of more AI tooling like coding assistants, VibeTunnel, and the MacWhisper voice translating app. A year ago, I was writing my own AI coding tools with Python. Today, I use voice-to-text to interact with multiple coding assistants that I can access from any browser connected to my Tailscale Tailnet. It’s a mouthful to explain, but coding looks very different now than it did a year or two ago for me.

    I’m not using Google Apps as much as I was a few years ago, though I still need to switch between Google Orgs for work and community organizations.

    Changes from 2024

    New Additions:

    • AI Coding Assistant (Claude Code, GitHub Copilot CLI, Codex)
    • VibeTunnel for accessing my terminal through any browser on my Tailnet
    • I started using Ghostty to run my AI sessions, but I find its lack of find (⌘f) support to be terribly frustrating.
    • I have used MacWhisper for years, but now it’s a daily driver. I can hold down the globe (fn) key on my keyboard and talk to my computer and quickly reply to people or even write blog posts with my voice. It’s incredibly cool and powerful.

    Apps Removed:

    • I dropped Polypane (Browser category)
    • I managed to not use Overcast (Podcasts category) for most of the year, but I’m still not happy with Apple’s Podcast app. I might be switching back.

    Previously inspired by: “Apps I’ve been using regularly this year.” Heavily inspired by Matt Stein and cataloged by Robb Knight’s App Defaults

    Wednesday October 29, 2025
  • LLM

    ,

    Today I Learned

    🤖 VibeOps: Using Claude Code on Cheap VPS Servers

    Today I came across Pieter Levels' post about “VibeOps,” a workflow that involves SSHing to a cheap VPS server and installing Claude Code directly on it. I’m running this setup on a cheap Hetzner box. While this approach might sound risky at first, when combined with a secure networking solution like Tailscale and properly configured as a Tailnet, it becomes both safe and worth experimenting with.

    I’ve also been exploring a complementary tool called Vibe Tunnel, which integrates well with Tailscale and my existing Tailnet setup. It provides an elegant way to remotely create or connect to an existing Claude Code session from my browser. I like the idea of letting Claude handle some routine remote tasks.

    Security is essential here. My Hetzner box uses their firewall product to block all ports except for SSL (443) and only allows me to access the box via Tailscale. If you aren’t using a firewall and a VPN product like Tailscale, you should not use a product like Vibe Tunnel to allow access to a running shell on your servers.

    While trying to install Vibe Tunnel, I ran into a PAM error, so I did what anyone would do with Claude Code available on the server: I let Claude fix it for me. I thought Claude could figure it out, but after 5 to 10 minutes of noble efforts, I found issue #499 and pull request #521, which point to a bigger issue with a potential fix coming soon.

    While I was on my server, I asked Claude to look at my Caddy setup for a bunch of parked domains, and it found some actionable changes and even fixed a few broken paths that weren’t obvious. This is exactly the kind of routine server maintenance where having Claude available directly on the server proves helpful.

    I also had Claude check to see if the machine was out-of-date, and I watched it install a month’s worth of updates. Claude even wrote a nice summary for me to share in this post:

    Analysis: The system is running a current Long Term Support release of Ubuntu with extended support until 2029. All installed packages are current with no pending updates available. No maintenance required at this time.

    Action Taken: System audit only - no changes were made to the system.

    All in all, I am a fan, even if I wouldn’t recommend this for any clients or just anyone.

    Wednesday August 6, 2025
  • Python

    ,

    Today I Learned

    Ditching Cloud APIs: How I Set Up Local Text-to-Speech with Kokoro TTS and Python

    Today, I fired up the Voices macOS app, which I occasionally use to convert blog posts or documentation text to audio files that I can take on the go. I usually use one of OpenAI’s Text to speech APIs, but today I noticed there was a new option called Kokoro, which is a local voice model. I was interested in the model and tried out a few voices. Two or three of them were amazing for a local model.

    After trying out a few blog posts and listening to them, I decided to find a Python version of the model. It might be slow to run, but at least I can create a pipeline to automate the process of generating voice files from arbitrary text files I have lying around.

    That’s when I found the Kokoro TTS application. I downloaded it and noticed that it needed a few things. Things like NumPy required to be installed, and a couple of things were out of sync, but the project mostly just worked. While it wasn’t quite as fast as a native Mac app, it was still good enough for me to run text files through it very quickly.

    Tonight, I decided to write up my notes on how to bootstrap and run the project.

    Installation

    git clone git@github.com:nazdridoy/kokoro-tts.git
    
    cd kokoro-tts
    
    uv add numpy
    
    uv sync 
    
    wget https://github.com/nazdridoy/kokoro-tts/releases/download/v1.0.0/voices-v1.0.bin
    
    wget https://github.com/nazdridoy/kokoro-tts/releases/download/v1.0.0/kokoro-v1.0.onnx
    

    Basic Usage

    Once installed, you can use Kokoro TTS from the command line:

    echo "Hello, this is a test of Kokoro TTS" | ./kokoro-tts /dev/stdin example-streaming.mp3 \
        --format mp3 \
        --voice af_heart
    

    If you already have an existing text file:

    ./kokoro-tts textfile.txt example-textfile.mp3 \
        --format mp3 \
        --voice af_heart
    

    Conclusion

    Kokoro TTS is a solid local text-to-speech option that runs entirely on your machine without requiring API calls or internet connectivity. While it may not be as fast as native Mac apps, the quality is impressive for a local model, and the ability to automate voice file creation from text files makes it valuable for anyone who wants to convert written content to audio format.

    One major advantage I discovered is that unlike most cloud APIs like OpenAI’s Text-to-speech that limit posts to around 3000 characters, Kokoro didn’t seem to have an upper limit on any of the content I tried it with. This makes it particularly useful for longer blog posts and documentation.

    The setup process is straightforward once you handle the missing dependencies, and the command-line interface makes it easy to integrate into existing workflows or automation scripts.


    Written by Jeff. Edited with Grammarly and Claude Code.

    Wednesday July 2, 2025
  • Today I Learned

    TIL Poppler's pdftoppm to convert PDF pages into PNG files

    Today I learned about pdftoppm, a simple CLI tool that can convert each page of a PDF into separate image files.

    My use case was to chop up a few big PDF reports to make OCR and data analysis easier, but scanning them a page at a time.

    Install

    I’m using Homebrew on macOS, but Poppler will also run on Linux and other operating systems.

    $ brew install poppler
    

    Quick Usage

    $ pdftoppm -png -rx 300 -ry 300 input.pdf output
    
    • -png -> save the output files as PNG images
    • -rx 300 -ry 300 -> set the output resolution to 300 dpi
    • input.pdf -> your source file which you want to process
    • output -> Your output file prefix which will produce output-1.png, output-2.png, …
    Wednesday May 21, 2025
  • Python

    ,

    Today I Learned

    🤷‍♂️ I miss Visual Basic

    I miss Visual Basic. I could build something meaningful with it in 15 to 30 minutes in a way that I have never seen anything since then, even come close to.

    “For example, I personally believe that Visual Basic did more for programming than Object-Oriented Languages did,” Torvalds wrote, “yet people laugh at VB and say it’s a bad language, and they’ve been talking about OO languages for decades. And no, Visual Basic wasn’t a great language, but I think the easy DB interfaces in VB were fundamentally more important than object orientation is, for example.”

    retool.com/visual-ba…

    Visual Basic 3 was the first programming language and application building framework that clicked with me. I never loved the language or the inconsistencies, but it just worked. I started with GW Basic, then Turbo Pascal in high school while prepping for college. My high school business teacher let me take an independent study where I made a simple video game in the spirit of Duck Hunt, where the Energizer bunny would explode into bloody bits when you clicked on it. She wasn’t prepared for that, and told me to spend the next half of the year just doing what I was doing.

    Visual Basic 6 was the last version I used before Microsoft effectively killed it with .dot by trying to turn it into something it was not. I had already started writing more web/PHP code to avoid Delphi and .net/C# code.

    Trying to build a Mac app today makes me sad. There isn’t a good visual way to create an app, which is both the strength and weakness of Visual Basic’s model. When you made a VB application, it looked like a Windows app. Anything I have ever dabbled with Xcode and other application frameworks looks like an unstyled/blank canvas, making me quickly feel fatigued.

    I know several people are trying to build one of these apps using Python, but it still feels like the infamous owl drawing, where step #1 is too basic, and the final step is impossible to get to. I’m hoping that AI can help fill in the “how to draw the owl steps,” but my concern is that modern-day frameworks are an impossibly hard bar to maintain and worth it for someone like myself, who isn’t paid to work on them.

    I love Python, but I miss Visual Basic.

    Sunday May 11, 2025
  • LLM

    ,

    Today I Learned

    🤖 Voice Dictation with AI and my MacWhisper Workflow

    I recently came across Simon Willison’s post about Matt Webb’s Apple Watch dictation setup on Interconnected. He records voice notes while running with the Whisper Memos app, then cleans up the transcript with Claude when he gets home.

    Matt Webb dictates notes into his Apple Watch while out running (using the new-to-me Whisper Memos app), then runs the transcript through Claude to tidy it up when he gets home.

    Matt’s usage of Diane is a neat trick that allows him to embed instructions while recording his notes while running. While I used to be in good enough shape to talk while running, the idea of dictating lectures is wild.

    My generic prompt to Claude, used every time, is now:

    you are Diane, my secretary. please take this raw verbal transcript and clean it up. do not add any of your own material. because you are Diane, also follow any instructions addressed to you in the transcript and perform those instructions

    [paste in transcript]

    Which means, when I’m talking through my lecture outline, I now finish by saying:

    ok Diane I think that’s it. it’s a talk, so please structure all of that into a high level outline so I can work on it. thanks.

    And I can mix in instructions like: oh Diane I meant to include that point in the last section. Please move it.

    It works super well.

    That inspired me to share the workflow I’ve been using for years.

    tl;dr My Workflow

    • Record thoughts with Apple Voice Memos on my iPhone using AirPods

    • Drag the audio file into MacWhisper to get a raw transcript

    • Copy the raw transcript into Obsidian as my writing buffer before and after each of the next steps

    • Paste the transcript into ChatGPT for cleanup

    • Run the text through Grammarly to spot grammar and style issues

    • Publish or iterate as needed


    Apple Voice Memos

    I use Apple Voice Memos on iOS, iPadOS, or macOS to capture ideas on the go. It syncs instantly across devices, so the recording is ready by the time I’m at my desk.

    MacWhisper

    Of all the transcription tools I’ve tried, MacWhisper is the most reliable. Once I’m at my Mac, I open Voice Memos, drag the file into MacWhisper, and it produces a transcript in seconds. MacWhisper is now available in the Mac App Store.

    There is even an iOS version called Whisper Transcription, which I have been trying out for the last few weeks. It has the advantage of allowing me to record directly into the app and then copy the transcript into something else. This is fine, but I haven’t found an option to save the audio file, which is concerning if my transcript gets too long.

    ChatGPT

    Next, I paste the raw transcript into ChatGPT to clean up filler words and pauses. My usual prompt looks like this:

    Please tidy up these voice notes. Remove any ums, ahs, and awkward pauses.
    
    <notes>
    ...
    </notes>
    
    <instructions>
    - Use my words and keep the spirit of my text
    - Avoid using en dashes or em dashes
    - ... your custom instructions to clean up your habits...
    </instructions>
    

    Grammarly

    After ChatGPT, I paste the text into the Grammarly app. As someone with dyslexia, Grammarly Pro helps me catch grammar mistakes and awkward phrasing so I can write in minutes, which used to take hours.

    Obsidian

    I copy the cleaned text into Obsidian. This step resolves formatting glitches from moving text between ChatGPT and Grammarly and allows me to make final tweaks.

    Publishing

    When I’m satisfied with the draft, I publish it. If it needs more work, I’ll run another quick round through ChatGPT and Grammarly until it’s ready.

    Improvements

    I have been trying out Whisper Transcription to skip a few steps by letting me record on my iPhone while sending the finished transcript to Obsidian or ChatGPT directly.

    I also want to add Matt’s “Diane” dictation trick with a more gender-neutral name, or maybe I’ll default to using “Simon” since I’m already using a half-dozen of Simon Willison’s AI/LLM tooling.

    I’m already using AI and LLM for many big and small tasks, so devising a more automated cleanup and preparation workflow shouldn’t be a big lift.

    Wednesday April 30, 2025
  • Python

    ,

    LLM

    ,

    Today I Learned

    🤖 Ideas for "The Levels of Claude" Notes

    Lately, I have been conversing more with friends, colleagues, clients, and online friends about Claude and coding tools.

    It turns out that everyone considers Claude to be something different when they ask, “Have you tried out Claude 3.7 Sonnet?” and that could mean through the website or the mobile app or people who are using it to write code or who are developing against one of Claude’s many REST APIs. Some people primarily interact with Claude by speaking in their voices while walking outside with their pets.

    I have even struggled to land at a good starting point, so I have been thinking through the various ways to explain how one might use Claude.

    • You can access Claude Chat through the website.
    • You can access Claude through the mobile app and use voice support.
    • You can copy one or more files into Claude.
    • You can access Claude through an IDE.
    • You use Claude Projects to organize your projects and might even have a support document you give Claude for more context.
    • You can access Claude through the Claude Desktop app and use voice support.
    • You have enabled MCP plugins/servers within the Claude Desktop app.
    • You can access the Claude Code CLI/terminal app.
    • You have enabled MCP with the Claude Code CLI/terminal app.
    • You are writing your own MCP servers in Python or JavaScript.
    • You have registered for Claude API access and are writing code against one of their REST APIs. (more on this later)

    I originally broke this down into five levels of Claude, but there were too many platforms and features that blurred together.

    Accessing Claude

    Accessing Claude can be confusing because there are many ways to access it.

    • Web interface
    • Mobile app
    • Desktop app
    • CLI/Terminal app
    • REST API (could be a third-party app too, that requires a developer token)

    Organization by experience level

    If we break these down by user experience or technical proficiency:

    • Beginner: Web interface, mobile app, IDE integrations
    • Intermediate: Desktop app, file uploads, REST API integration
    • Advanced: Claude Code CLI
    • Really Advanced: You are writing custom MCP implementations.

    Claude’s developer APIs

    Claude’s Developer APIs have their vibes and are helpful in various situations. I’m not going to deep dive into them, but from a bird’s eye view:

    • Text Completion API
    • Embeddings API
    • Messages API
    • Claude-in-the-Middle (Tools API and Agents)
    • Batch API

    These APIs can also vary based on passing them text, documents, images, or other types of files.

    I’m still amazed that I can send an image to Claude and ask it to give me back an HTML file and CSS, and it does a relatively decent job.

    Conclusion: Claude as a Spectrum

    I wasn’t sure how to end my notes, but I asked Claude to: “Please write a conclusion about Claude being more of a spectrum than just a service.” and this is what it told me:

    When discussing “Claude,” we’re not just talking about a single AI service but rather a spectrum of capabilities, interfaces, and integration points that serve different user needs, technical abilities, and use cases.

    What makes this spectrum particularly interesting is how it creates different experiences and understanding of what “Claude” actually is. For a casual user accessing the web interface, Claude is simply a helpful chat assistant. For a developer using the API or building MCP servers, Claude becomes an extensible AI platform with programmable capabilities.

    This spectrum explains why conversations about Claude can sometimes feel like people are talking about entirely different products. In a very real sense, they are. The Claude that helps a writer craft a story through the mobile app exists in a different context than the Claude that helps a developer debug code through an IDE integration.

    As the Claude ecosystem continues to evolve, this spectrum will likely expand further, with new interfaces, capabilities, and integration points creating even more varied experiences. Understanding Claude as a spectrum rather than a singular service helps contextualize these different experiences and provides a framework for navigating the growing complexity of AI assistants.

    The most powerful aspect of this spectrum may be that it allows Claude to meet users where they are - from the casual web user to the advanced developer building custom AI solutions. This flexibility transforms Claude from merely an AI service into an adaptable AI ecosystem that can evolve alongside user needs and technical capabilities.

    It’s not half bad.

    Wednesday March 5, 2025
  • Django

    ,

    Python

    ,

    LLM

    ,

    UV

    ,

    Today I Learned

    🤖 "Agents" are Cheat Codes

    Lately, I have been trying to wrap my brain around AI Agents, so as a starting point, I have been using Pydantic AI’s Agent class/framework to build “Agents”.

    “Agent” is a loaded term. Pydantic AI’s usage is more or less a system prompt and a good API around adding tool calls and working with existing LLMs.

    I have written several experimental projects to help me quickly research and find answers to several areas of Django that confuse people, including myself. These ask-one-question bots do their best to fetch the resources they need to answer your questions.

    The three I have published publicly are:

    None of these are official resources of the Django Software Foundation, nor should they be considered “official” or even “legal” answers to any questions that may arise.

    The pattern I landed on for building the system prompts and pulling remote data has been a practical, quick way for me to get feedback and ask questions based on our existing material. I can change a local copy of the bylaws and then ask the Agent questions to see if my potential changes might be comprehensive enough for the Agent to answer.

    It effectively feels like running tests on governance to see if the Agent picks up on my changes.

    Our Cheat Codes

    These are cheat codes for a quick one-file Agent that one can quickly stand up and ask questions.

    • UV is a cheat code because it can quickly create a one-file Agent with dependencies and the version of Python needed to run the demo baked in.
    • Pydantic AI’s Agent class is a nice wrapper around a system prompt and can even create a dynamic system prompt. Having a global system prompt has a nice feel to it too.
    • Pydantic’s BaseModel creates structured data responses as a cheat code for processing unstructured text. If you haven’t seen this pattern yet, you can’t unsee it.
    • The Jina AI for cleaning up HTML into Markdown is an AI I have wanted for a decade+. I use it in dozens of apps for free, saving me hours of work.
    • The Python libraries Typer, Rich, and httpx may not seem like they are doing much, and I’m underutilizing them, but their Developer Experience (DX) is great, and they just work.

    More areas to explore

    Pydantic AI supports dynamic System Prompts, which might save me a few extra templating steps. They didn’t really click for me before I was writing this post.

    When I wrote my Django Agents, I had Pydantic AI’s Multi-agent Applications feature in mind. In theory, I want to ask my Django Agents a question and have it route my question to the appropriate Agent to get an answer.

    Function Tools or Tool Call is what inspired me to try out Pydantic AI. Function Tools are a way to give LLMs the ability to get information outside of their memory and system prompts when needed. I built one for reading and writing to my work calendar to help me manage my schedule. I didn’t use them for my suite of Django Agents, but when mixed with more real-time data they could be helpful.

    We could also refactor each Agent using a reusable tool call so we could assemble one Agent that can gather the information needed to answer common Django Governance questions. I don’t know if that would be effective. In theory, it might not be a bad fit after looking at their DuckDuckGoSearchTool example.

    Sunday March 2, 2025
  • Django

    ,

    Python

    ,

    Today I Learned

    Python Click, django-click, and Typer notes

    One of the most significant Python innovations in my development toolchain was the Click utility, which simplified the creation of Python scripts. Click changed how I approach writing one-off Python scripts and made it easier for me to write better developer experiences around those scripts.

    Once I found django-click, writing Django management commands was a breeze, using the same Click API that I was already familiar with.

    Arguably, the second most significant innovation was the Typer library, built on Click, making writing Python scripts even easier. I didn’t think it was possible to be easier than Python Click until Typer came out and proved me wrong.

    The Python Click library

    A typical pattern I use with Click is creating a command that accepts an argument like a URL.

    import click
    
    
    @click.command()
    @click.argument("url", type=str)
    def command(url):
    	do_something(url=url)
    

    The Python django-click library

    This works well, but one improvement would be to accept as many url arguments as I can pass to it. Thankfully, Click solves this with the nargs argument. I always have to look this up, which is why we are here.

    import djclick as click
    
    
    @click.command()
    @click.argument("urls", nargs=-1, type=str)
    def command(urls):
        for url in urls:
            do_something(url=url)
    

    The Python Typer library

    Suppose we were writing this example with Typer. In that case, we could simplify it to using Python’s native data types, which would make it feel more like I’m writing native code and less like I’m using a library.

    import typer
    
    
    app = typer.Typer() 
    
    
    @app()
    def command(urls: list[str]): 
        for url in urls:
            do_something(url=url)
    

    Conclusion

    There is also a django-typer library, bringing the Typer library to Django. I suspect I’ll switch to django-typer the next time I start a new project to give it a good test drive. I can speculate on what that looks like, but I’ll leave that for another day.

    Wednesday January 22, 2025
  • Django

    ,

    Python

    ,

    Today I Learned

    📩 Email, Calendars, and the Chaos of Modern Workflows

    I was feeling overloaded with emails this week, and then I remembered that my out-of-office auto-responder told people they should contact me after the first of the year if they needed me to reply.

    Thankfully, I could select and archive all of my 2024 emails with this rule label:inbox after:2023/12/31 before:2025/01/01, which reconciled my old emails.

    Calendars and shared Documents

    With each Google organization, I’m a member with another Google Calendar, Google Drive, and Google Contacts to manage. That document someone wants feedback on sometimes feels like spinning a wheel, and I need to guess which inbox and account the message might land in.

    The best solution that I have found for juggling meeting invites is Reclaim, which is terrific for merging multiple calendars into one calendar so I can at least keep on top of meeting invites and scheduling. Dropbox recently bought them, but I’m hoping that Dropbox will leave them alone.

    Email and calendars have become more challenging since I switched to a Mac Studio at the office. While we were returning to work during a blizzard last week, I realized that my personal Mac Mini in my home office had no concept of my work calendar or the 4 or 7 Vivaldi profiles with syncing that I use to jump between orgs all day.

    With 1Password, this is a straightforward process to set up and authorize, but it still takes time.

    Tonight, I’m pretty sure I even locked myself out of one service because it’s probably not a typical usage pattern to jump between three Macs over two locations with a half dozen profiles to juggle.

    Calendar Agent

    Over the Thanksgiving break, I wrote my first Calendar Agent, who can read and write to my work calendar. It’s not fully baked yet, but it works well enough to tell me about my upcoming meetings and to create a meeting for me. Sometimes.

    The biggest downside to using my Calendar Agent is that I have to run it from my terminal, which isn’t always the most convenient place.

    Side note: I might rewrite my agent using PydanticAI as an excuse to learn about the Python agent framework, streamline tool-calling, and play with more local agents using Ollama.

    The better email solution

    The better email solution was a Django email app called Concorde, one of Adam Fast’s creations. It was Django querysets for managing email rules, which I modified and ran over the years. It quickly created better rules than Gmail supported, like deleting old messages in specific folders after x-days. When I kept my fork running and updated, the tool was invaluable. When I kept my Concorde up and running, my email life was healthier than when I was slower to fix it after an upgrade.

    Conclusion

    I’m annoyed that the best solutions for these problems are to either pay a company to make a Google Suite usable or you must be a developer to build tools to manage it all.

    This stuff sucks.

    Wednesday January 15, 2025
  • Django

    ,

    Python

    ,

    Today I Learned

    django-templated-email-md notes aka if you want to format emails with Markdown, use it

    I launched Django News Jobs two DjangoCon USs ago, and I somehow put off deploying emails until this week. So, every day or two, I check my Review Jobs queue to see if there’s anything new to approve or reject.

    ✅ Sending emails with Django is straightforward and not very painful.

    🤔 Working with most email providers and troubleshooting issues is painful.

    🤔 Starting with a blank page and styling emails is painful.

    I tried a few third-party apps that I fought with before landing on Jack Linke’s django-templated-email-md (DTEM), which just worked. DTEM doesn’t re-invent the wheel, but it does let me write my email messages with Markdown, which turns out to be all I need.

    To add email support, I followed the Usage Guide and then I added one send email function per email that I wanted to send. I’ll eventually refactor this, but it was good enough to get started.

    jobs/models.py

    For the curious, the code looks like:

    # jobs/models.py
    
    # a bunch of imports ... 
    
    from templated_email import send_templated_mail
    
    ...
    
    class Job(models.Model):	
        ...
    
        def send_job_new_email(self):
            send_templated_mail(
                template_name="job_new",
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=settings.ADMINS,
                context={
                    "job": self,
                },
            )
    

    send_templated_mail does the actual sending, and template_name will look for a template file called job_new.md, which will contain our Markdown message.

    You can put anything you want in context, but I will include the job so we can include as many details from our job submission as possible.

    To send a Job, I can call job.send_job_new_email() via a signal, cron job, or after someone submits a Job for approval.

    templates/emails/job_new.md

    My emails contain both a “subject” and “content” block, and DTEM figures out the rest for me.

    <!-- templates/emails/job_new.md -->
    
    {% block subject %}[{{ site_name }}] New Job Posting: {{ job.title }} at {{ job.employer_name }}{% endblock %}
    
    {% block content %}
    # New Job Listing Posted
    
    A new job has been posted on the website:
    
    - **Title:** {{ job.title }}
    - **Company:** {{ job.employer_name }}
    - **Location:** {{ job.location }}
    - **Remote:** {{ job.get_remote_display }}
    
    You can view the full job listing here: <a href="{{ job.get_absolute_url_with_domain }}">{{ job.get_absolute_url_with_domain }}</a>
    {% endblock content %}
    

    My get_absolute_url_with_domain calls in my templates are my workaround for Django’s existential crisis of not making it easy to include the domain name in my urls.

    Bonus: Django-Q2

    I paired DTEM with Django-Q2, my favorite Django task queue. It can work with just the Django ORM, which is good enough for projects like Django News Jobs, which are relatively low traffic but spiky traffic.

    If my email-sending provider times out or I have an issue, like my credit card expiring, I never want a user to see it. So, I use a task queue to handle all potentially blocking processes, like sending emails.

    Django-Q2 is painless to configure. Using it involves importing async_task and modifying our send_templated_mail method to be an argument to the async_task method.

    # jobs/models.py
    
    # a bunch of imports ... 
    
    from django_q.tasks import async_task
    from templated_email import send_templated_mail
    
    ...
    
    class Job(models.Model):	
        ...
    
        def send_job_new_email(self):
            async_task(
                send_templated_mail,
                template_name="job_new",
                from_email=settings.DEFAULT_FROM_EMAIL,
                recipient_list=settings.ADMINS,
                context={
                    "job": self,
                },
            )
    

    If a Job was sent successfully, I can now check the Django Admin to see if there were any failures.

    Forward Email

    Now that we have an email confirmed to send, my go-to provider for side projects is Forward Email. They allow outbound SMTP and even support webhooks, which I might write about some other time.

    I like them over Mailchimp, Sendmail, and Gmail because they are cheap ($3 a month) and let me have unlimited domains and aliases. I have used them for a dozen side projects for several years now, and they just work. I gave up on Sendmail because I spent more time fighting with them to not turn off my account because the volume was too low. It’s worth $36 a year to have to fight this fight again.

    Forward Email’s products and services are fully open-source if you care about such things.

    Saturday January 11, 2025
  • Weeknotes

    ,

    Django

    ,

    Python

    ,

    Today I Learned

    🎄 Weeknotes for Week 51: December 15 to December 21

    I mostly finished my holiday shopping this week. I picked up a few gifts for my family throughout the year and then randomly stressed out last week, thinking I didn’t have enough. I picked up a few last-minute items, and everyone was in good shape outside of a few gift cards. Now, to wrap everything…

    Meta

    Lately, my weekly notes have been more like monthly notes. I draft them, forget to finish/publish them, and then the following Sunday morning, I start another week. So here we are again.

    Family

    December is LEGO advent calendar month in my household. Our recently turned three-year-old participates for the first time with the LEGO® ǀ Disney Advent Calendar 2024, and our seven-year-old picked the LEGO® Star Wars™ Advent Calendar 2024. Even if you are a grownup, these are fun, and you get some cool LEGO mini-figures.

    House

    If you ask our three-year-old what she wants for Christmas, she defaults to “Rainbow lights” because I didn’t put any lights up outside. Between our Thanksgiving trip to Chicago and two weeks of everyone in the house cycling through the crud, here we are, but at least the Christmas tree got put up.

    Community Work

    My community cups ran over a bit this week. This is due to my being new to the DSF Board and various end-of-year and before-holidays “things” that pop up.

    Community work cut into some of my open-source projects, but I’d like to catch up over the holidays.

    I also ended my week with my last Office Hours of 2024. This week was a little too non-profit and community-heavy, so I will balance that better. With the DSF having its own office hours, I want to keep solving those problems in the proper space.

    Side projects

    Side Quests

    Writing

    2024-12-20🗓️ December 21, 2024, is Volunteer Responsibility Amnesty Day 

    2024-12-19Default Apps 2024 - Here are my Default Apps 2024, which builds from my Default Apps 2023 post.

    2024-12-17🤷 Why do the Django and Python communities use so many Google Forms? 

    2024-12-14New project to shorten django-admin to django because we are not monsters - I didn’t realize this idea would kick a hornet’s nest, and yet somehow it did.

    Entertainment

    We picked up Mighty Morphin Power Rangers: Rita’s Rewind and finished it last week. It was fun, and we like these retro games, but it’s not TMNT’s level of replayability. The game has three hours of gameplay. I’m hoping future updates address this.

    I read mixed reviews on G.I. Joe: Wrath of Cobra, but my son was pretty excited about the new Power Rangers game, so we picked it up as a beat ‘em up game we could play over the weekend. It’s buggy in ways that make me wonder how this game has been out for a few months and still has this level of bugs. The controls are bad, but we somehow played through to the last stage before we both shrugged and decided to call it a night. I would give it a two out of five stars type of game. If it weren’t for the nostalgia from my youth, I’d give it one or one and a half stars.

    New gear

    My NanoKVMs arrived a few weeks ago. I quickly ran out of ports, so I ordered some short HDMI cables, Cat6 cables, more UBC-C cables, and a cheap 8-port switch.

    I also lost my Home Assistant machine again, so I swapped out RPis and still ran into issues. As impressive as Home Assistant, running, maintaining, and keeping running is a pain. I have been debating switching to one of their yellow box hardware solutions to support them financially and hoping that I won’t lose my box once a year because it’s so hard to troubleshoot and fix.

    I also picked up a 50-foot sewer cam (for looking in walls and vents), an under-desk walking treadmill, and a smart garage door opener to replace our smart Chamberlain garage door opener because they dropped their API.

    Next week

    This week is Christmas, which means a little bit of travel. Both kids are out of school and preschool, and we both are juggling jobs and deadlines.

    Sunday December 22, 2024
  • Today I Learned

    Default Apps 2024

    Here are my Default Apps 2024, which builds from my Default Apps 2023 post.

    🌐 Browser: Vivaldi + Polypane
    🔍 Search: Kagi
    📝 Writing: Obsidian + Grammarly
    📁 Cloud File Storage: iCloud Drive + Syncthing
    💬 Chat: Apple Messages (family & friends), Discord (friends), Slack (work and community work), Telegram (weird bots) 📆 Calendar: Apple Calendar
    📆 Scheduling + Booking: Cal.com
    📹 Video Calls: Zoom + Cal.com + Meeter
    🎵 Music. : Apple Music + Spotify
    🎤 Podcasts: Overcast (boo, hiss) 🔐 Password Management: 1Password
    🧑‍💻 Code Editor: Sublime Text + Zed 🗃️ Version Control: Tower
    🐚 Terminal: iTerm2
    ✈️ VPN: Tailscale + Mullvad 🔖 Bookmarks: Raindrop.io
    📑 Read It Later: Raindrop.io
    📨 Mail Client: Mimestream (Gmail only and not very often)
    📮 Mail Server: Fastmail (I pay for it but still don’t use it enough) + Gmail
    🚀 Launcher: Alfred 5
    🖼️ Screenshots: Xnapper
    👔 Menu Bar: Ice
    🤖 Containers: OrbStack + Docker Compose 🎒 Backups: Backblaze ⚙️ Automation: Hammerspoon

    Commentary

    I spent most of 2024 Chrome-free, the new cage-free because I was so frustrated at Mozilla that I deleted Firefox from all my machines. I have doubled my Gmail Accounts and usage between non-profits and community projects.

    I’m a happy Vivaldi user and advocate. I only had a handful of issues, quickly solved by turning all or some level of trackers back on. The worst offenders were both my bank and my credit card company.

    I dropped Bartender and switched to Ice after the company started acting shady due to poor communication. I have no regrets.

    My beloved Overcast podcasting app has been complete garbage for more of 2024 than it was working. The author rewrote it, and it became an unusable disaster for months, one part of Marco’s fault and one part of Apple’s fault for making the iOS App Store such a pain to release products into. It still has Airplay issues and crashes on me, but I can once again play more than one podcast in a row without it crashing. I tried many other podcast apps, and I’m sad that this is the state of things.

    I have mostly switched from Docker to OrbStack, which I highly recommend for my development life. I keep Docker around for Compose, but I have several legacy projects I haven’t ported yet.

    Previously inspired by: “Apps I’ve been using regularly this year.” Heavily inspired by Matt Stein and cataloged by Robb Knight’s App Defaults

    Thursday December 19, 2024
  • Weeknotes

    ,

    Django

    ,

    Python

    ,

    Today I Learned

    📓 Weeknotes for Week 48: November 25 to December 1

    Family

    We drove up to Chicago to see family for five days. It’s a 7.25-hour drive if not stopping were an option, but we usually have to stop at least two or three times to re-fuel, eat, and have a few bathroom breaks. Both of my kids are pro-travelers. Give them an iPad with an offline cache of their favorite Disney and Netflix movies and series, plus some snacks, and they are good to go for 8 to 16 hours. On our last trip back from Omaha, they complained that it was too short because we didn’t stop on our 2.5-hour drive back.

    We take turns driving, and through the magic of tethering, I’m surprised that I can comfortably use my laptop from the car for half the trip.

    Seeing family was good. There are five kids, ranging from two to nine, but this year everyone is out of diapers, everyone can communicate their needs, and everyone plays together nicely.

    We decided to avoid dinner prep drama for Thanksgiving and go out for Mexican food and margaritas. This was an excellent idea. The staff was super friendly, the food was excellent, the margaritas were the right amount of salty and sweet, and everyone got to pick something they enjoyed. There was no food prep or cleanup stress. Overall, our bill for 10 people plus a very generous tip, even after a service fee, was much less than it costs to try to feed 10 people a traditional spread.

    Work

    It was a short, two-day workweek for me. I helped a client with an inventory project running a week or two behind on their side. The timing wasn’t great because it all landed on my lap the day before I was heading out of town, and it took a few days to run. I hate k8s even more than before.

    My main client is missing a tool or two to nail the pulse. I have thought about this because everything takes less time to complete but more time to work on the problem, so things are stretched out. The holiday break was the mental reset I needed to know how to manage this for the next month until our winter break.

    Community Work

    I skipped all Office Hours this week, but it was a busier few days for my DSF CoC WG card. We reset/rebooted the WG a month ago and have more members and communication. It’s taking more time to reset and settle into a healthy normal.

    Side projects

    Side Quests

    Calendar Agent

    I spent some free time over the break working on an app that lets ChatGPT control my work calendar. I got the basics up quickly, and my Calendar Agent (Bot?) can look at what’s scheduled on my calendar and create new events for me. The hardest part was navigating Google permissions (I hate them) so that I could access my calendar through Python. Once I got that setup, I used Claude Projects to help me write most of the code.

    I ran into an issue with tool calling because I wanted to let the Agent query the time for a day before creating the event. I shifted this logic into my create event function as a fix and realized I was way overthinking it. Now, I have an interesting agent. Still, I have yet to figure out how to run it outside my terminal, which isn’t helpful.

    Entertainment

    I mostly watched football and basketball this week, but I started a few shows from jefftriplett.com/now/

    New gear

    Since it was Black Friday, I did some shopping, but not as much as in previous years. I noticed Mastodon vibes are anti-Black Friday, whereas Blue Sky and X are more about saving money and getting a deal. More of my friends shared deals over Slack and Discord than in previous years.

    I picked up a five-pack of 32 GB microSD drives for my NanoKVMs, which I hope will be delivered next week. I also bought the family an Air Fryer convection oven combo unit, which may replace our old toaster and be helpful.

    Next week

    We get to start our LEGO Advent Calendars. I started this tradition with my son when he was two or three. My daughter will be three in a few weeks, so she is now old enough to join in the tradition, too. She requested the Disney Princess calendar, and my son has the Star Wars set. We skipped the traditional set because one can only have so many Santa Clauses.

    Sunday December 1, 2024
  • Python

    ,

    UV

    ,

    Today I Learned

    🤷 UV does everything or enough that I'm not sure what else it needs to do

    UV feels like one of those old infomercials where it solves everything, which is where we have landed in the Python world.

    I have had several discussions with friends about UV, and even when we talk about it during my weekly(ish) office hours, the list has grown to an ever-growing number of options.

    UV started as a quicker way of installing Python packages, and now it’s easier to tell people that UV does everything and to focus on what it doesn’t do.

    My favorite feature is that UV can now bootstrap a project to run on a machine that does not previously have Python installed, along with installing any packages your application might require.

    Here is my incomplete list of what UV does today:

    • uv pip install replaces pip install
    • uv venv replaces python -m venv
    • uv pip compile replaces pip-tools compile
    • uv pip sync replaces pip-tools sync
    • uv run replaces pipx
    • uv tool run replaces pipx
    • uv python replaces pyenv, asdf, mise, and several other like-minded tools
    • uv build - Build your Python package for pypi
    • uv publish - Upload your Python package to pypi
    • astral-sh/setup-uv brings UV to GitHub Actions
    • ghcr.io/astral-sh/uv:latest brings UV and Python to Docker

    I copied these four from uv --help, which feels like poetry features.

    • uv add - Add dependencies to the project
    • uv remove - Remove dependencies from the project
    • uv sync - Update the project’s environment
    • uv lock - Update the project’s lockfile

    So what doesn’t UV do?

    UV does a lot, but it still needs to do everything.

    • UV doesn’t run custom scripts defined in our pyproject.toml like npm-run-script allows. Thank you to @command_tab for jogging my memory.
    • UV doesn’t convert my non-UV-based projects to UV. Converting is more about prefixing and replacing my commands to switch over.
    • UV doesn’t manage, and bump version numbers like the BumpVer, and others do.
    • UV doesn’t manage pre-commit like hooks. This is a long shot, but I’d love to see support via pyproject.toml.
    • UV doesn’t replace Python, nor should it.
    Sunday November 3, 2024
  • Today I Learned

    Please publish and share more

    Friends, I encourage you to publish more, indirectly meaning you should write more and then share it.

    It’d be best to publish your work in some evergreen space where you control the domain and URL. Then publish on masto-sky-formerly-known-as-linked-don and any place you share and comment on.

    You don’t have to change the world with every post. You might publish a quick thought or two that helps encourage someone else to try something new, listen to a new song, or binge-watch a new series.

    This week, I have encouraged at least half a dozen people to blog something, and at least three of them were happily surprised to see their work re-posted by another friend or published in a newsletter.

    I have nothing against masto-whatever-you-use-this-week or blue-sky-levels-of-vc-money or formerly-called-no-one-cares, but those platforms are hard to share an article on.

    So, even if you re-publish to thread your post infinitely, please find a cheap or free publishing platform and own your work. GitHub Pages is a free way to publish your work via GitHub, and they will let you use your own domain name for free.

    You don’t need an editor

    I used to ask my friends to review my work, and I still sometimes do. Then I realized that 99% of the time, it doesn’t matter.

    I pay for Grammarly because I have Dyslexia, and it helps me communicate better. But you don’t have to.

    You can use a free tool like LanguageTool, which has an online version that will let you copy and paste your writing into a free, no-login-required Grammar checker. This is more than the average person will do, and it’s a quick and free gut check.

    Not every gift needs a bow

    Our posts are done when you say they are. You do not have to fret about sticking to landing and having a perfect conclusion. Your posts, like this post, are done after we stop writing.


    PS: Write and publish before you write your own static site generator or perfect blogging platform. We have lost billions of good writers to this side quest because they spend all their time working on the platform instead of writing.

    Saturday November 2, 2024
  • Justfiles

    ,

    Today I Learned

    TIL Justfiles can also be Just Scripts

    Please note: passing an argument like --justfile It only works on MacOS and on Linux.

    TIL that Justfiles can turn into Just Scripts by adding #!/usr/bin/env just --justfile to the top of the file and running chmod +x on the file.

    From the docs:

    By adding a shebang line to the top of a justfile and making it executable, just can be used as an interpreter for scripts: https://github.com/casey/just?tab=readme-ov-file#just-scripts

    just.sh

    #!/usr/bin/env just --justfile
    
    @_default:
    	just --justfile just.sh --list
    
    @lint *ARGS:
        uv --quiet run --with pre-commit-uv pre-commit run {{ ARGS }} --all-files
    

    After you run chmod +x just.sh, this file may be run using ./just.sh, and sub-commands everything just <subcommand> will just work.

    Please note that --justfile just.sh is needed if you want your Just Script to be able to introspect or call itself.

    Why?

    More and more of my clients are using Justfiles, and occasionally, I want some other recipes that may belong outside the default workflows. These can also be reusable between projects for some of my other internal tooling, so it’s an excellent resource to learn about.

    Wednesday October 23, 2024
  • Python

    ,

    Docker

    ,

    UV

    ,

    Today I Learned

    📓 My notes on publishing a Python package with UV and building a custom GitHub Action for files-to-claude-xml

    My new Python application files-to-claude-xml is now on PyPI, which means they are packaged and pip installable. My preferred way of running files-to-claude-xml is via UV’s tool run, which will install it if it still needs to be installed and then execute it.

    $ uv tool run files-to-claude-xml --version
    

    Publishing on PyPi with UV

    UV has both build and publish commands, so I took them for a spin today.

    uv build just worked, and a Python package was built.

    When I tried uv publish, it prompted me for some auth settings for which I had to log in to PyPI to create a token.

    I added those to my local ENV variables I manage with direnv.

    export UV_PUBLISH_PASSWORD=<your-PyPI-token-here>
    export UV_PUBLISH_USERNAME=__token__
    

    Once both were set and registered, uv publish published my files on PyPI.

    GitHub Action

    To make files-to-claude-xml easier to run on GitHub, I created a custom action to build a _claude.xml from the GitHub repository.

    To use this action, I wrote this example workflow, which runs from files-to-claude-xml-example

    name: Convert Files to Claude XML
    
    
    on:
      push
    
    
    jobs:
      convert-to-xml:
        runs-on: ubuntu-latest
        steps:
        - uses: actions/checkout@v4
        - name: Convert files to Claude XML
          uses: jefftriplett/files-to-claude-xml-action@main
          with:
            files: |
              README.md
              main.py          
            output: '_claude.xml'
            verbose: 'true'
        - name: Upload XML artifact
          uses: actions/upload-artifact@v4
          with:
            name: claude-xml
            path: _claude.xml
    

    My GitHub action is built with a Dockerfile, which installs files-to-claude-xml.

    # Dockerfile
    FROM ghcr.io/astral-sh/uv:bookworm-slim
    
    
    ENV UV_LINK_MODE=copy
    
    
    RUN --mount=type=cache,target=/root/.cache/uv \
        --mount=type=bind,source=uv.lock,target=uv.lock \
        --mount=type=bind,source=pyproject.toml,target=pyproject.toml \
        uv sync --frozen --no-install-project
    
    
    WORKDIR /app
    
    
    ENTRYPOINT ["uvx", "files-to-claude-xml"]
    

    To turn a GitHub repository into a runnable GitHub Action, an action.yml file needs to exist in the repository. This file describes the input arguments and which Dockerfile or command to run.

    # action.yml
    name: 'Files to Claude XML'
    description: 'Convert files to XML format for Claude'
    inputs:
      files:
        description: 'Input files to process'
        required: true
        type: list
      output:
        description: 'Output XML file path'
        required: false
        default: '_claude.xml'
      verbose:
        description: 'Enable verbose output'
        required: false
        default: 'false'
      version:
        description: 'Display the version number'
        required: false
        default: 'false'
    runs:
      using: 'docker'
      image: 'Dockerfile'
      args:
        - ${{ join(inputs.files, ' ') }}
        - --output
        - ${{ inputs.output }}
        - ${{ inputs.verbose == 'true' && '--verbose' || '' }}
        - ${{ inputs.version == 'true' && '--version' || '' }}
    

    Overall, this works. Claude’s prompting helped me figure it out, which felt fairly satisfying given the goal of files-to-claude-xml.

    Wednesday October 16, 2024
  • Django

    ,

    Python

    ,

    Today I Learned

    🧳 DjangoCon US, Black Python Devs Leadership Summit, and Django Girls Durham

    I’m heading to Durham, NC, for seven days of DjangoCon US this Friday. This is my 10th year volunteering and the 9th year that DEFNA, the non-profit I co-founded, has run a DjangoCon US event. Here is an overview of the week.

    Black Python Devs Leadership Summit (Saturday)

    I’m attending and speaking on a discussion panel on Saturday at the Black Python Devs Leadership Summit. Tickets are free, and they will be streaming online in the afternoon. Donations are accepted and appreciated.

    Django Girls Durham (Saturday)

    Django Girls are hosting a Django workshop and teaching beginners a crash course on building their first website using Django.

    DjangoCon US Tutorials (Sunday)

    On Sunday morning, I’ll be volunteering and helping out at the tutorials. In the afternoon, we have a tradition of stuffing swag bags, which takes a big group and is a fun way to kick off the conference. You do not need a tutorial ticket or an organizer to help out. Ask at the registration desk, and they can direct you to when and where we are doing this.

    Django Social meetup (Sunday)

    My company REVSYS is sponsoring a DjangoSocial Raleigh/Durham Pre-DjangoCon Special meetup on Sunday evening before the conference kicks off. The meetup will be great for meeting other attendees the night before the conference.

    DjangoCon US Talks (Monday through Wednesday)

    The talks are great, but the busiest three days of the conference are also the busiest. There is always a lot going on, from sun up to sun down.

    DjangoCon US Sprints (Thursday and Friday)

    The sprints are one of my favorite parts of the conference. In past years, I have been so exhausted by the sprints that it’s hard to sit down and focus. It’s one of the best times to discuss Django and the Django ecosystem. If you have a project or want to find a project to help with, the sprints are great for getting your feet wet.

    Outro

    Tickets are still available if you live near Durham and want to attend. Both events have online and in-person options, so there is no pressure to make last-minute travel plans.

    If you live around Durham and want to meet up, please reach out. Let’s see if we can meet for coffee.

    Friday September 20, 2024
  • Django

    ,

    Python

    ,

    UV

    ,

    Today I Learned

    🤠 UV Roundup: Five good articles and a pre-commit tip

    I have written quite a bit about UV on my micro blog, and I am happy to see more and more people adopt it. I have stumbled on so many good articles recently that I wanted to share them because every article points out something new or different about why UV works well for them.

    If you are new to UV, it’s a new tool written by Astral, the creators of Ruff.

    I like UV because it replaces, combines, or complements a bunch of Python tools into one tool and user developer experience without forcing a UV way of doing it. UV effectively solves the question, “Why do I need another Python tool?” to do everyday Python tasks.

    Some reason I like UV after using it for months:

    • It’s a faster pip and is really, really fast
    • It can install and manage Python versions
    • It can run and install Python scripts
    • It can run single-file Python scripts along with their dependencies
    • It can handle project lock files

    While some people don’t care about UV being fast, it’s shaved minutes off my CI builds and container rebuilds, which means it has also saved me money and energy resources.

    Overall thoughts on UV

    Oliver Andrich’s UV — I am (somewhat) sold takes the approach of only using UV to set up a new Python environment. Oliver uses UV to install Python, aliases to call Python, and UV tool install to set up a few global utilities.

    Using UV with Django

    Anže Pečar’s UV with Django shows how to use UV to set up a new project with Django.

    Switching from pyenv to UV

    Will Guaraldi Kahn-Greene’s Switching from pyenv to uv was relatable for me because I also use pyenv, but I plan to slowly migrate to using only UV. I’m already halfway there, but I will have pyenv for my legacy projects for years because many aren’t worth porting yet.

    Using UV and managing with Ansible

    Adam Johnson’s Python: my new uv setup for development taught me to use uv cache prune to clean up unused cache entries and shows how he manages his UV setup using Ansible.

    Some notes on UV

    Simon Willison’s Notes on UV is an excellent summary of Oliver’s notes.

    A parting UV tip

    If you are a pre-commit fan hoping for a version that supports UV, the pre-commit-uv project does just that. I started updating my justfile recipes to bake just lint to the following uv run command, which speeds up running and installing pre-commit significantly.

    $ uv run --with pre-commit-uv pre-commit run --all-files
    pre-commit-uv
    

    If you are attending DjangoCon US…

    If you are attending DjangoCon US and want to talk UV, Django, Django News, Django Packages, hit me up while you are there.

    I’ll be attending, volunteering, organizing, sponsoring, and sprinting around the venue in Durham, NC, for the next week starting this Friday.

    We still have online and in-person tickets, but not much longer!

    Thursday September 19, 2024