brettterpstra.com.web.brid.gy
brettterpstra.com.web.brid.gy
@brettterpstra.com.web.brid.gy
A few Black Friday deals
First off, Marked 2 is part of the Unclutter Black Friday bundle, along with 11 other _excellent_ Mac apps. You can get the full bundle for 77% off, or individual apps (like Marked 2) for 50% off. You should definitely check that out. I’ve gathered a few other deals you may or may not have seen already. I mostly just wanted to get some less-visible ones on your radar. * CleanMyMac is 30% off starting Friday. This link is having a little trouble on non-Chrome browser, and I’m asking for a new one. I’ll update if I get one. * [Elements](https://brettterpstra.com https://realmacsoftware.com/pricing/?coupon=BlackFridayWeb45&utm_source=website&utm_medium=web&utm_campaign=brett+terpstra) web site creator is 45% until December 1st * Notepad.exe is over 50% off * UGREEN stuff like UGREEN NASync DXP4800 is all marked down * Keyboard Maestro is 50% off * Flying Meat apps (Acorn, Retrobatch) are all $10 off * Tower Git client is 30% off * MacSparky field guides are 25% off with code `PIE25` * Auralog (migraine tracker) is $8.99/year * TextTastic is 50% the first year Feel free to add any other deals you find notable in the comments! Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
November 27, 2025 at 10:31 AM
A Jekyll plugin for viewing large SVG diagrams
I recently built a WordPress plugin specifically for my friend Allison Sheridan over at Podfeet Podcasts. She had a _huge_ mind map she wanted to display on her blog — HTML embeds were messy and broke the site, PNG exports were unsearchable and impossible to navigate, and she didn’t want to just offer an OPML download. So she output an SVG, and I built a viewer to embed it in a WordPress post with nice zooming and panning, text searchability, and a bunch of customization options. I’ve submitted that plugin to the WordPress registry, but it will take a couple of weeks before it’s available. In the meantime, if you want to check it out, you can learn more at the BT SVG Viewer GitHub repo, or just download the latest zip file and use Upload Plugin to install it. It has complete built-in help you can access from the Preset Editor page. But I’ll post more about that when it’s available for easy install. In the meantime, I thought I’d port it to Jekyll. I don’t have an immediate need for it, but being able to display mind maps such as those exported from MindNode Next seems like something I’ll definitely want to do sooner or later. I packaged the Jekyll plugin as a gem, so you can install it just by adding it to the `Gemfile` and your `_config.yml`. Once that’s done, when you build the site, you’ll get a `/svg-viewer/preset-builder/` that gives you an interface for building the Liquid tag used to embed the actual viewer. Copying the generated Liquid tag and pasting it into a post serves up your SVG like: You can define a whole set of default options for the tag in your `_config.yml` file. I don’t know how many people will need this one, but I think it came out really polished and will be a perfect solution for those who _do_ need it. Check out the project page for more details. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
November 11, 2025 at 10:04 AM
Web Excursions for November 6th, 2025
Web excursions brought to you in partnership with MindMeister, the best collaborative mind mapping software out there. manaflow-ai/cmux > cmux lets you run Claude Code, Codex CLI, Amp, Gemini CLI, Cursor CLI, Opencode, and other coding agent CLIs in parallel across multiple tasks. git-flow-next: Your Favorite Git Workflow. Reimagined. A ground-up rewrite of git-flow in Go. More flexible branching and merge strategies, better conflict resolution, and better branch dependency tracking. (The original git-flow project has even started redirecting users to git-flow-next.) The folks at Tower do amazing work. Zellij I’m probably not going to put the energy into porting all of my tmux knowledge and settings into a new tool, but Zellij looks like an awesome terminal multiplexer and has been extremely intuitive in the time I’ve put into playing with it. If you have the energy to switch, or especially if you’ve never gotten into multiplexing, definitely check it out. There’s even a web client mode with “multiplayer” capabilities in your browser. Effects Showroom - TerminalTextEffects Docs These are dumbfoundingly cool terminal text effects. Some useful for scripting, some just pretty to look at. Hat tip to cavalierex in the forum. Check out MindMeister and start brainstorming, collaborating, and boosting productivity. Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
November 7, 2025 at 10:04 AM
Announcing Bear & Glass, Apple workflow consulting
I’ve mentioned before that what I really wanted out of my career was to be a consultant, especially in automation and efficiency on Mac and iOS, for teams and small-to-medium size businesses looking to grow. Well, I’m excited to have teamed up with Christopher Gamblée-Wallendjack to create Bear & Glass. We want to take what we normally do for individuals one-on-one and bring it to teams. I’m excited to be launching this! Bear & Glass will make the work I do and the things I create accessible to people that might never have found me on their own — creative teams who need someone who speaks both human and machine. When one person decides what “clever” tool the whole company will use, it rarely works. If the needs of the people actually using the tools aren’t heard, they won’t use them — and everyone’s right back where they started. That’s why Bear & Glass will talk to everyone. We build systems and automations that make sense to the people who rely on them, and which connect perfectly to create the whole. The automation possibilities are vast. We can do everything from speeding up your email workflow to streamlining your publishing, onboarding, and day-to-day operations. We provide stable, future-proof automations that scale with your business. We specialize in Mac and iOS automation, but we also do Unix automations and server-side operations. Working with Christopher is the perfect pairing: I get to focus on the automation side, and Christopher, who specializes in customer service, documentation, and project management, gets to focus on the people. So if you’re someone who believes their team or business could be saving time, working more efficiently, or improving everything from onboarding to operations with our bespoke services, visit bearandglass.co and contact us today! You’ll get a personal response from Christopher, who can answer all of you questions! Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
November 1, 2025 at 9:57 AM
One more na update
This week I pushed a set of focused improvements to NA that make interactive workflows a lot smoother and the codebase a little more robust — including first‑class time tracking. If you run `na update` without arguments you’ll now get an interactive menu that helps you pick the file(s) and actions to operate on, and I’ve added a flexible plugin architecture and time tracking features. ## TL;DR * `na update` (no args) now launches an interactive, consistent selection flow for files and actions. * The update submenu supports multi-select, edit, move, and direct action modes more reliably. * Fixed many edge cases: nil-safe string helpers, clearer YARD docs, and better tests for helper code. * Time tracking * Plugin architecture * Under-the-hood improvements to file selection, action movement, and tagging logic. ### New: Time tracking * Add start/finish times right from the CLI: * `--started TIME` on `add`, `complete`/`finish`, and `update` * `--end TIME` (alias `--finished`) on `add`, `complete`/`finish`, and `update` * `--duration DURATION` to backfill a start time from the finish * Natural language and shorthand supported everywhere: `30m ago`, `-2h`, `2h30m`, `2:30 ago`, `yesterday 5pm` * Durations aren’t stored; they’re computed from `@started(...)` and `@done(...)` when displayed. * Display enhancements in `next`/`tagged`: * `--times` shows per‑action durations and a grand total (implies `--done`) * `--human` switches durations to friendly text * `--only_timed` filters to actions with both `@started` and `@done` (implies `--times --done`) * `--only_times` outputs only the totals (no action list; implies `--times --done`) * `--json_times` emits a JSON object with timed actions, per‑tag totals, and overall totals (implies `--times --done`) * Per‑tag totals are shown as a Markdown table with aligned columns and a footer row for the grand total * Duration color is theme‑configurable via a `duration` key (default `{y}`) Example: na add --started "30 minutes ago" "Investigate bug" na complete --finished now --duration 2h30m "Investigate bug" na next --times --human na next --only_times na tagged bug --json_times | jq ### New: Plugin architecture You can now extend NA with small scripts placed in `~/.local/share/na/plugins`. Each plugin is a script with a shebang; NA feeds it actions on STDIN and reads transformed actions from STDOUT. * Run: `na plugin run NAME` * Shortcut: `na plugin NAME` (defaults to run, but doesn’t accept run‑flags) * From update: `na update --plugin NAME` (also appears in the interactive menu) * From display: `na next --plugin NAME` (STDOUT‑only transform; no file writes) * IO formats: `json`, `yaml`, `csv`, `text` (`--input/--output/--divider` supported) * Plugins may modify `text`, `note`, `tags`, and `parents`; NA applies moves if parents change * Optional ACTIONs supported in plugin output: `UPDATE` (default), `DELETE`, `COMPLETE/FINISH`, `RESTORE/UNFINISH`, `ARCHIVE`, `ADD_TAG`, `DELETE_TAG/REMOVE_TAG`, `MOVE` #### Plugin subcommands * `na plugin new NAME` (alias `n`): Create a plugin in `~/.local/share/na/plugins`. If no extension is provided, NA prompts for a language (or pass `--language rb|py|/usr/bin/env bash`). Shebang and extension are inferred. * `na plugin edit NAME`: Open a plugin in your system editor. If no name is given, you’ll be prompted to pick from available plugins (enabled and disabled). * `na plugin run NAME` (alias `x`): Execute a plugin against selected actions. With no arguments, NA prompts for the plugin and actions. Supports `--input`, `--output`, `--divider`, plus usual filters like `--file`, `--depth`, `--search`, `--tagged`, `--done`. * `na plugin enable NAME` (alias `e`): Move a plugin from `plugins_disabled` into `plugins`. * `na plugin disable NAME` (alias `d`): Move a plugin from `plugins` into `plugins_disabled`. * `na plugin ls` (alias `list`): List plugins. Use `--type enabled|disabled` (or `e|d`) to output plain names only. Quick examples: # Transform a filtered list without writing files na tagged bug --plugin AddFoo --output text # Modify selected actions in place na update --plugin AddFoo --input json --output json On first run NA creates the plugins folder with a README and two sample plugins (`Add Foo.py`, `Add Bar.sh`) you can use as starting points. ## Interactive `na update` flow Run `na update` with no arguments and you’ll see a consistent selection flow: * Choose one or more todo files (fuzzy search / fzf / gum used when available). * Pick which actions to update (multi-selection supported). * Choose an operation: edit, move, add tag, remove tag, mark done, delete. Examples: $ na update Select files: (interactive list) Select actions (multi): [x] 23 % Inbox/Work : - Fix X [x] 45 % Inbox/Personal : - Call Y Choose operation: (edit / move / done / delete / tag) The menu now behaves consistently whether you pick a single file or multiple files; if you choose multi-select the update command applies as you’d expect to the set of chosen actions. There’s a direct action mode when you know the file and action: `na update PATH -l 23` still works as before. The interactive flow only kicks in when no explicit target is provided. ## Notable fixes A lot of the work was small but important: * Nil-safe string helpers: `trunc_middle`, `highlight_filename`, and friends were guarded against `nil` inputs so tests and UI code don’t explode when a file is missing or the database contains a stray blank line. * Action move/edit correctness: moving an action to a different project now updates parent indexes and project line numbers properly, avoiding off-by-one bugs that could leave the file in a strange state. * `select_file` and fuzzy matching: the fuzzy and database-driven file selection was made more robust — the code handles directories that have a `file.na` or `file/file.na` pattern and falls back to a clear error instead of failing silently. * YARD docs: cleaned up a number of `@!method` directives and added top-level `@example` blocks for the main classes and helpers so the docs are friendlier and generate without warnings. * Tests: added and fixed unit tests for `Array`, `Hash`, and `String` helpers. TTY screen and color-related test stubs were improved for reliability on CI. * New tests for time features: JSON output, totals‑only output, timed‑only filtering. ## Try it You can update to the latest version with: gem install na Run in iTerm That should give you v1.2.85 or higher. If you’re on a recent development build or want to try the updates locally: # From the gem checkout bundle exec bin/na update # Or after building the gem and installing na update If you run into anything odd, please open an issue with the command you ran and a short description of what you expected vs what happened. Small, reproducible steps are the fastest way to a fix. If you hit an error and want to include a backtrace, run the command with debug enabled and paste the output: GLI_DEBUG=true na [COMMAND] ## Other updates I last wrote about 1.2.80. Here are a changes since then: ### CHANGED * Default theme/templates include `%line`; users with custom theme may need to regenerate to see line numbers * Update command interactive menu lists available plugins * README: add detailed Plugins section with examples and schema * CHANGELOG: add 1.2.88 entry for plugins feature * Completed plugin subcommand sections with aliases and context * Plugins section updated with management workflow and notes * Sample plugins can be regenerated with –generate-examples when deleted * Shortcut `na plugin NAME` works without run command flags (use `na plugin run NAME` for flags) ### NEW * Display line numbers with actions across `na next` and selection * Support `PATH:LINE` targeting in `na update` and `na edit` * Multi-action editing in external editor using `# ------ PATH:LINE` markers; notes supported beneath each action * Add –started flag on add/complete(update) to set @started * Add –finished alias to –end on add/complete/update to set @done * Add –duration (add/complete/update); backfills @started from –end * Add –times (next/tagged) to show per-action durations and totals * Add –human (next/tagged) for human-friendly duration format * Add –only_timed (next/tagged) to show only items with @started/@done * Add –json_times to next/tagged (JSON of timed actions, tags, total) * Add –only_times to next/tagged (show only totals, no action list) * Plugin system with scripts in ~/.local/share/na/plugins * Na plugin command to run plugins on selected actions * –plugin/–input/–output/–divider flags on update/next/tagged/find * Plugin IO supports json, yaml, csv, and text formats * ACTION support in plugin IO (UPDATE/DELETE/COMPLETE/RESTORE/ARCHIVE/ADD_TAG/DELETE_TAG/MOVE) * Auto-create plugins dir with README and sample plugins (Add Foo.py, Add Bar.sh) * Display commands can pipe through plugins (STDOUT-only, no writes) * Add –json_times to next/tagged (JSON of timed actions, tags, total) * Add –only_times to next/tagged (show only totals, no action list) * Added plugin enable/disable and flow tests (update/move behaviors) * Document plugin subcommands (new/edit/run/enable/disable) * `--generate-examples` flag regenerates sample plugins and README * `na plugin NAME` shortcut defaults to `na plugin run NAME` ### IMPROVED * –times and –only_timed imply –done for next/tagged * Duration annotations render with theme colors in output * Natural-language dates for @started/@done normalized automatically * Support shorthand: 2h30m, 30m ago, -2:30, 2:05 ago * –only_timed, –times, and –json_times imply –done automatically * Per-tag duration totals rendered as aligned Markdown table with footer * Duration color configurable via theme key `duration` (default {y}) * Duration/time docs and output clarifications in README * Internal plugin README with metadata, IO, and examples * –only_timed, –times, and –json_times imply –done automatically * Per-tag duration totals rendered as aligned Markdown table with footer * Duration color configurable via theme key `duration` (default {y}) * `na plugin run` prompts for plugin and action selection when no filters * README: brief descriptions for `plugin` subcommands * README plugin command docs with brief descriptions * Sample plugins only generated once (tracked via .samples_generated flag) ### FIXED * Incorrect colorization (unexpected bright green) in action output * Search highlighting no longer corrupts ANSI color codes or numbers in escape sequences * Multi-action editor now only includes the specifically selected lines (no duplicates) * Delete in update menu removes the correct lines * Multi-select updates process bottom-to-top to avoid line shifts * String wrapping now wraps at requested widths (e.g., 60 cols) and indents * Plugin update path writing duplicate actions; now in-place by line * Plugin apply finds action via target_line, avoids Symbol->Integer error * String#wrap indentation and width handling for multi-line wrapping * Lint/DuplicateBranch in plugin parse_actions * Style/MapToHash in apply_plugin_result * String wrapping now wraps at requested widths (e.g., 60 cols) and indents * Plugin-applied updates now modify in place (no duplicate actions) * Suppressed Y/N prompts during plugin operations and non-interactive runs * `--only_timed` filtering (case-insensitive tag keys) in output * Stabilized wrapping by removing test override of String#wrap Thanks for playing with it and for the helpful feedback you’ve been sending. Check out the NA project page for more info. Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 30, 2025 at 9:55 AM
Optimizing na and doing
I’ve been using my na and doing command-line tools daily for years, but lately they’d started feeling sluggish. When `na next` was taking over a second to show me my next actions from a tiny 5KB file, I knew it was time to dig in and fix things. In case you’re unfamiliar, `na` is a command line tool for adding and listing per-project todos, and `doing` is a CLI for tracking what you were doing, with export features, time tracking, tagging, and other usefuly features. * na project page * doing projct page ## The Problem Both tools had developed performance issues that made them occasionally frustrating to use. The `na next` command was consistently running at 1.3+ seconds with wildly variable performance—sometimes 200ms, sometimes 500ms for identical inputs. Meanwhile, `doing recent` was sometimes taking over 3 seconds to display a simple list of recent entries. This wasn’t acceptable for tools meant for quick, frequent use. ## The Investigation I built custom benchmarking systems for both gems (`NA_BENCHMARK=1` and `DOING_BENCHMARK=1`) to see where the time was going. The results were eye-opening. For `na`, the culprits were: * Theme loading happening repeatedly for every action * Regex compilation on every color template use * Pager overhead adding 300-479ms to small outputs * Git integration running system calls by default * Heavy gems loading at startup For `doing`, the pager was the main bottleneck, consuming about 600ms of the 3+ second runtime. ## The Fixes Caching everything I pre-compiled regex patterns, cached theme data, and implemented template caching to eliminate repeated processing. Smart pagination Both tools now skip the pager entirely for small outputs. For `na`, that’s anything under 2000 characters or 50 lines. For `doing`, it’s anything under 150% of terminal height (letting you scroll up half a page for reasonable outputs). Lazy loading Made git integration opt-in with a `--repo-top` flag in `na`, and deferred heavy gem loading until actually needed. Batch processing Generate all strings first, then apply regex highlighting once instead of repeatedly. ## The Results The improvements were dramatic: * **na** : From 1300ms+ down to ~6ms (99.5% faster) * **doing** : From 3+ seconds down to 0.73 seconds (78% faster) Both tools now feel genuinely responsive again. The `na next` command is nearly instantaneous, and `doing recent` displays results in under a second. ## Bonus Fixes While I was at it, I fixed several test failures that had been nagging me: * Color module method definitions causing `NoMethodError` * ANSI escape sequence handling in exports * String extension methods for color attributes * Cleaned up export output (no more unwanted `\e[0m` characters) Both tools are back to being the responsive, daily-use utilities they were meant to be. You can find more details in the doing wiki and there’s full documentation for `na` on the na project page, if you’re curious. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 30, 2025 at 9:55 AM
YouTube to Markdown embed
This is just a little 30-minute project I made for my own use, but figured I’d share. It simply takes a YouTube URL and converts it to GitHub-friendly Markdown for use in READMEs, wikis, and PR comments. GitHub doesn’t allow for `<iframe>`s, so you can’t actually embed a YouTube video in a README. You can, however, easily embed a thumbnail and link it to the video. This little API does just that. Sure, it’s built with GitHub in mind, but it’s useful in any situation where you can’t use an actual embed but want to include a YouTube video. You can play around with the API at https://brettterpstra.com/yt-md, which has full documentation and live testing. The endpoint for the API is: https://brettterpstra.com/yt-md/api Include a `?url=` (full YouTube URL) or `?id=` (just a video ID) parameter, and it will generate the Markdown for you. You can also include `&output=json` or `&output=html` to get JSON or HTML output. Add `&caption=0` to avoid including a caption in the HTML. Here are a few quick examples you can run from the command line using curl. Replace the video ID in the examples with your own. Basic: request Markdown for a video ID: curl -s 'https://brettterpstra.com/yt-md/api?id=w1LuV82l0vs' Run in iTerm Using a full YouTube URL: curl -s 'https://brettterpstra.com/yt-md/api?url=https://www.youtube.com/watch?v=w1LuV82l0vs' Run in iTerm JSON output (pretty-print with jq): curl -s 'https://brettterpstra.com/yt-md/api?id=w1LuV82l0vs&output=json' | jq '.' Run in iTerm HTML output without a caption (show the first few lines): curl -s 'https://brettterpstra.com/yt-md/api?id=w1LuV82l0vs&output=html&caption=0' | sed -n '1,3p' Run in iTerm Request a specific thumbnail size (maxres): curl -s 'https://brettterpstra.com/yt-md/api?id=w1LuV82l0vs&thumb=maxres' | sed -n '1,1p' Run in iTerm 🔒 ### Premium Content Bonus CLI and Shortcut Subscribe Now I'm a Subscriber Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 30, 2025 at 9:55 AM
Ruby Progress Indicators for Modern CLI Tools
I’ve been playing around with progress bars in the terminal, and I think I’ve created something genuinely useful. Yes, I know there are multiple options already available for this, this is mostly just an experiment for me. I wanted to build a Ruby library that could be used for command-line scripting, with CLI executables that made it easy to use when scripting in _any_ language — not just Ruby, with a few options I didn’t see elsewhere. The result is `ruby-progress`, a gem that provides four different animated progress indicators, each with its own visual style and use cases. > If you’re scripting in Ruby, I do highly recommend the TTY tools from Piotr Murach, such as tty-progress. Piotr’s TTY tools have Ruby APIs. `ruby-progress` is designed more for use in other shell scripts than it is for detailed Ruby scripting. This tool owes a lot to Piotr’s work. ## The Four Progress Indicators Ripple Creates a wave-like effect across text, with the characters rippling through different visual states. It supports rainbow colors, inverse highlighting, and case transformations. Perfect for text-heavy operations where you want something more dynamic than a simple spinner. Worm Displays a moving dot pattern that crawls across the screen using Unicode characters. It includes multiple styles (circles, blocks, geometric shapes) and can move in any direction. Great for file operations, network transfers, or any continuous background task. Twirl Provides classic spinning indicators with over 35 different spinner styles — from simple dots to complex geometric patterns. It’s the most traditional of the four, but with way more visual variety than typical spinners. Fill The only determinate indicator in the bunch — an actual progress bar that shows completion percentage. It can be controlled via command execution or updated programmatically through daemon control messages. ## Installation and Quick Start Installation is straightforward: gem install ruby-progress All four indicators are available through a unified `prg` command: # Basic usage prg ripple "Processing files..." prg worm --message "Loading..." --style blocks prg twirl --message "Working..." --style dots --speed fast prg fill --total 100 --report # With command execution prg ripple "Building..." --command "make build" --success "Build complete!" --checkmark You can also use the individual commands directly: `ripple`, `worm`, `twirl`, and `fill`. ## The Killer Feature: Daemon Mode Here’s where things get interesting. All four indicators support daemon mode, which lets you run a progress indicator in the background while your scripts execute other commands. This is useful for complex workflows. # Start a background spinner prg worm --daemon-as deployment --message "Deploying application..." # Execute your actual deployment steps git push production main kubectl apply -f manifests/ ./run-migrations.sh # Stop the spinner with a success message prg job stop --daemon-name deployment --message "Deployment complete!" --checkmark The daemon mode uses `Process.fork` and `Process.detach`, so there are no shell job notifications cluttering your output. The progress indicator runs cleanly in the background, providing visual feedback without interfering with your script’s execution. ### Keeping Output Clean One of the challenges with daemon mode is preventing command output from disrupting the animation. By default, when you run a progress indicator in daemon mode, any STDOUT or STDERR output from commands executed in your script will appear on the terminal and potentially mess up the animation display. Ruby-progress handles this intelligently. When running in daemon mode, the animation continues on its dedicated line while your commands execute. However, if you want to see the output from those commands, you have options: # Run command with animation, show output after completion prg ripple "Building..." --command "make build" --stdout # Stream output live while animation runs (reserves terminal rows) prg worm "Installing..." --command "npm install" --stdout-live --output-lines 5 # Run daemon and manually capture output from your script commands prg worm --daemon-as build --message "Building..." make build > /tmp/build.log 2>&1 prg job stop --daemon-name build --message "Build complete!" --checkmark cat /tmp/build.log The `--stdout-live` flag is particularly useful — it reserves a section of the terminal to display live command output while the animation continues above or below it. You control how many lines to reserve with `--output-lines` and where they appear with `--output-position` (above or below the animation). ### Job Control Commands The `prg job` command provides subcommands for controlling background indicators: * `prg job stop` — Stop a running daemon with an optional message * `prg job status` — Check if a daemon is running and show its PID * `prg job advance` — Update a fill progress bar by a specific amount # Check if a daemon is running prg job status --daemon-name mytask # Advance a progress bar remotely prg fill --daemon-as progress --total 100 prg job advance --daemon-name progress --amount 10 # Stop with error state prg job stop --daemon-name mytask --message "Build failed!" --error 🔒 ### Premium Content Library and advanced usage Subscribe Now I'm a Subscriber ## Real-World Use Cases **Deployment Scripts** : Show continuous progress during multi-step deployments without blocking command output. **Data Processing** : Provide visual feedback during long-running data imports or transformations. **Build Systems** : Integrate into Rake or Make tasks to show progress during compilation. **CI/CD Pipelines** : Add visual indicators to pipeline scripts for better monitoring. **System Administration** : Show progress during backups, database dumps, or file synchronization. ## Try It Out The easiest way to get started: gem install ruby-progress Run in iTerm prg ripple 'Hello, World!' Run in iTerm Or check out the GitHub repository for full documentation, examples, and the complete API reference. The README includes detailed guides for each indicator type and advanced usage patterns. The gem is also available on RubyGems, and the latest release includes significant improvements to daemon mode handling and job control commands. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 29, 2025 at 9:55 AM
iTerm Jekyll plugin — Run commands from your blog
I’ve published a tiny Jekyll plugin that makes terminal commands in posts runnable with a click (in iTerm, at least). The iTerm tag plugin renders a fenced code block showing a command, and a “Run in iTerm” link that uses iTerm 2’s new(ish) command url scheme (`iterm2:/command?c=COMMAND`) to open the command in a confirmation box in iTerm, ready for immediate execution. The plugin outputs fenced code blocks with language definitions, so whatever syntax highlighter your Jekyll blog uses can still appropriately highlight the code. The “Run in iTerm” link is given a little extra markup to make it easy to style. ### Examples If you’re running iTerm, click the link below the code blocks to open the command in a confirmation dialog. Short form: {% iterm "gem install ruby-progress" %} Renders: gem install ruby-progress Run in iTerm Key/value form (custom text, directory, language): {% iterm command:"python -m http.server" directory:"~/Sites/project" text:"iTerm: Start dev server" language:"console" %} That outputs a fenced code block with the command and a small “Run in iTerm” link/button that uses iTerm2’s URL scheme: python -m http.server iTerm: Start dev server Get the plugin and examples on GitHub. 🔒 ### Premium Content Don't forget about the rest! Subscribe Now I'm a Subscriber Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 24, 2025 at 9:53 AM
Spend some, get some
I’ve built a paywall system for this blog. It was quite an endeavor to make a home-grown solution that works with my static Jekyll site, but I think I got it. From now on, I’ll be adding bonus content to posts that will only available to subscribers. You can subscribe at any level — the benefits are the same whether you pay $3, $6, or $12 per month. You can subscribe using PayPal, with or without a PayPal account. I’m going to be phasing out Memberful as I’m able to switch over the subscribers there. So become a subscriber today and don’t miss out on anything. > All of my current supporters should automatically have been ported to the new system, and should be able to log in just by providing the email address with which they used to subscribe. (If you’re currently using Memberful, please consider cancelling there and re-subscribing via the links on the support page.) As a benefit that I hope will make subscribing worthwhile, I’ve negotiated hundreds of dollars in savings from my favorite software developers, with 10-50% discounts about 40 apps and services (and growing), including discounts on books and educational screencasts. I’m also going to implement a paid tier on the forum, but I’m not planning to _not_ converse with non-paying subscribers, so I’ll have to figure out what the benefit there will be. I have no intention of paywalling my best content, or limit access to any of my projects. Paywalled content will be bonus content within publicly-available posts. It’s really still just a way to support my work, so if you appreciate what I do and want to see more of it, please do consider subscribing. As a demonstration, I’m going to detail how I built this system as bonus content. So if you want to see that, subscribe. Be sure to check out the Member Discounts page to see all of the bonuses you can get. 🔒 ### Premium Content My first paywalled content — subscribe to see how it all works! Subscribe Now I'm a Subscriber Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 22, 2025 at 9:51 AM
Web Excursions for October 6th, 2025
Web excursions brought to you in partnership with Fabric, the best way to organize your notes, tasks, and projects in one place. Sleeve 2 — Now playing on your Desktop Sleeve is my current top choice for displaying “now playing” information on my desktop, with a very customizable display, theme sharing, and last.fm scrobbling. Side note: I found QuietScrob to be pretty good for scrobbling Apple Music plays on the iPhone. I recently cancelled Spotify and am trying to make my way with Music now… MeSoul - Smart Journaling & Self-Discovery App There are some benefits to this being web-based, and I like that the AI integration doesn’t try to write for you, it just answers questions and improves long-term memory. > Transform your thoughts into insights with MeSoul’s intelligent journaling platform. Track emotions, discover patterns, and grow through AI-powered reflection. Start your 14-day free trial. Quip – AI Clipboard Manager and Text Expander for macOS & iOS I’ve tested this out and it’s pretty great. As a side note, there’s a brand new app called Stash that functions more as an AI-powered Yoink that looks worth checking out (as is Yoink). > Quip is the first clipboard manager powered by AI, built for Mac, iPhone, and iPad. Automatically save everything you copy, organize it with Smart Collections, and paste anywhere using Super Shortcuts. Enjoy powerful search, full editing, and seamless iCloud sync—designed with privacy in mind. Vibe Backup - Code Backup Tool for Vibe Coding Developers I’m not a “vibe coder,” but I _have_ been using AI to add some features to my apps. And given how quickly AI can make a mess and totally break things, constantly committing to your VCS is a must. This little app seeks to automate and make that process simpler. Features Shortcuts integration, Timeline management, and quick Rollback capabilities for secure code backup. $4.99 for the Pro version with no limits. Let Fabric be your second brain, with an all-in-one AI workspace and smart organizer for all your projects, ideas, notes & links. Check it out today. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 20, 2025 at 9:48 AM
Markdown Lipsum API v4 and the random-words gem
I’ve released v4 of md-lipsum, my API for generating Markdown Lorem Ipsum. This update introduces expanded source options, new query parameters for enhanced functionality, and is powered by the new ‘random-words’ Ruby gem. Try the new API: https://brettterpstra.com/md-lipsum ### Share Your Shortcuts! If you use the API to create any Apple Shortcuts, System Services, command line scripts, or other automations, please share in the comments/forum! I’d love to highlight any use cases out there. ### What’s New in API v4 #### Expanded Source Options The biggest change in v4 is the expansion of available text sources. While previous versions were limited to basic Lorem Ipsum, v4 now offers 14 different source languages and themes: * **lipsum** - Lorem Ipsum (default) * **1984** - George Orwell’s 1984 * **alice** - Alice in Wonderland * **bacon** - Bacon ipsum * **corporate** - Corporate Buzzwords * **doctor** - Doctor Who * **english** - English text * **foulmouth** - Foulmouth ipsum * **hipster** - Hipster ipsum * **latin** - Latin text * **spanish** - Spanish text * **veggie** - Vegetarian ipsum * **walken** - Christopher Walken * **random** - Random source You can use any of these sources by appending `?source=SOURCE_NAME` to your API calls: https://brettterpstra.com/md-lipsum/api/4/6/short/all/?source=1984 #### New Query Parameters API v4 introduces several new query parameters for enhanced functionality: ##### `?preview=true` This parameter enables preview mode, which renders the HTML output in a styled format instead of returning raw HTML. Perfect for seeing how your generated content will look when styled. ##### `?style=PATH` Allows you to specify a custom stylesheet for the HTML output. The path should be URL-encoded: ?style=%2Fmd-lipsum%2Fstylesheets%2Fpreview.css You can also use full URLs to external stylesheets: ?style=https://example.com/styles.css ##### `?html=true` This option returns raw HTML instead of Markdown. ##### `?complete=true` When combined with `html=true`, this parameter returns a complete HTML document with proper `<html>`, `<head>`, and `<body>` tags, making it ready for direct use. #### Example API Calls Here are some examples of the new v4 API in action: # Generate HTML with 1984 curl 'https://brettterpstra.com/md-lipsum/api/4/6/short/all/?html=true&source=1984' # Preview mode with corporate buzzwords curl 'https://brettterpstra.com/md-lipsum/api/4/5/medium/header/link/bq/?preview=true&source=corporate' # Raw Markdown with long sentencs and Doctor Who source curl 'https://brettterpstra.com/md-lipsum/api/4/3/long/all/?source=doctor' ### The random-words gem The Markdown Lipsum API v4 is now powered by the new `random-words` Ruby gem, which provides a comprehensive solution for generating random text content. This gem is available for both command-line use and as a Ruby library. Full documentation can be found on RubyDocs and the source is on GitHub. #### Installation gem install random-words Depending on your setup, you may need: gem install --user-install random-words Or in worst case scenario: sudo gem install random-words You can also use `brew gem install random-words` if you use Homebrew. #### Using as a Ruby Library The `random-words` gem provides a clean Ruby API for generating random content: require 'random-words' # Create a generator with a specific source rw = RandomWords::Generator.new(:corporate) # List all available dictionaries puts rw.sources # Change source dictionary rw.source = :bacon # Configure paragraph and sentence lengths rw.paragraph_length = 5 # Number of sentences in a paragraph (default 3) rw.sentence_length = :short # :short, :medium, :long, :verylong (default :medium) # Generate different types of content puts rw.sentence # Single sentence puts rw.sentences(3) # Array of 3 sentences puts rw.paragraph # Single paragraph puts rw.paragraph(2) # Paragraph with 2 sentences puts rw.characters(100) # Text limited to 100 characters puts rw.characters(100, whole_words: false) # Allow truncation # Access parts of speech directly puts rw.adjectives.sample puts rw.nouns.sample puts rw.verbs.sample #### Available Sources The gem comes with the same 14 sources available in the API: rw = RandomWords::Generator.new(:lipsum) # Lorem Ipsum rw = RandomWords::Generator.new(:'1984') # George Orwell's 1984 rw = RandomWords::Generator.new(:alice) # Alice in Wonderland rw = RandomWords::Generator.new(:bacon) # Bacon ipsum rw = RandomWords::Generator.new(:corporate) # Corporate Buzzwords rw = RandomWords::Generator.new(:doctor) # Doctor Who rw = RandomWords::Generator.new(:english) # English text rw = RandomWords::Generator.new(:foulmouth) # Foulmouth ipsum rw = RandomWords::Generator.new(:hipster) # Hipster ipsum rw = RandomWords::Generator.new(:latin) # Latin text rw = RandomWords::Generator.new(:spanish) # Spanish text rw = RandomWords::Generator.new(:veggie) # Vegetarian ipsum rw = RandomWords::Generator.new(:walken) # Christopher Walken ### The randw Binary Installing the gem also provides a powerful command-line tool called `randw`. Here’s what you can do with it: #### Available Commands $ randw -h Usage: randw [options] OPTIONS: -S, --source SOURCE Specify the source language (default: latin) -l, --length LENGTH Specify the length of the sentence [short|medium|long|very_long] --graf-length NUMBER Specify the number of sentences in a paragraph --[no-]extended Specify whether to use extended punctuation in generated text GENERATORS: -s, --sentences [NUMBER] Generate NUMBER of random sentences (default: 3) -p, --paragraphs [NUMBER] Generate NUMBER of random paragraphs (default: 3) -w, --words [NUMBER] Generate NUMBER of random words (default: 10) -c, --characters [NUMBER] Generate random characters of length (default: 100) -m, --markdown [SETTINGS] Generate random markdown text -H, --html [SETTINGS] Generate random html text --password [LENGTH] Generate a random password of LENGTH (default: 20) DICTIONARIES: --list-dictionaries List available dictionaries --create-dictionary [NAME] Create a new dictionary OTHER OPTIONS: -d, --debug Enable debug mode -h, --help Display help message -v, --version Display version -t, --test Run debug test #### Command Examples # Generate sentences with different sources randw -s 5 -S corporate randw -s 3 -S bacon # Generate markdown with specific elements randw -m "latin/1/short/ol" # Latin, 1 paragraph, short length, ordered lists randw -m "english,5,all" # English, 5 paragraphs, all elements # Generate HTML content randw -H "corporate/medium/3/header/link" # Generate a random password randw --password 25 # List all available dictionaries randw --list-dictionaries # Create a new dictionary randw --create-dictionary medical #### Markdown Generation The `--markdown` flag is particularly powerful, allowing you to specify exactly which elements to include: trigger | element ---|--- dec | add em and strong link | add links ul | add unordered lists ol | add ordered lists dl | add definition lists bq | add block quotes code | add code spans and blocks mark | add ==highlights== headers | add headlines image | add images table | add tables x | add extended punctuation Example: $ randw -m "latin/1/short/ol" Illa brevis muros potior arcesso, apud multae octo centum nonaginta octo nodum! 1. Hoc cognatus opus facile complor latus discendo 2. Aliqua apparens census quod nego 3. Nullus salvus dux apud habeo spectabilis ### Creating Custom Dictionaries One of the most exciting features of the `random-words` gem is the ability to create your own custom dictionaries. This allows you to generate content in specialized domains like medical terminology, legal jargon, or any other field-specific language. #### Getting Started To create a new dictionary, use the CLI command: randw --create-dictionary [NAME] This creates a new directory in `~/.config/random-words/words/[NAME]` with all the necessary files populated with English defaults. #### Required Files Each dictionary needs these files: * `adjectives.txt` * `adverbs.txt` * `articles-plural.txt` * `articles-singular.txt` * `clauses.txt` * `config.yml` * `conjunctions-coordinating.txt` * `conjunctions-subordinate.txt` * `names.txt` * `nouns-plural.txt` * `nouns-singular.txt` * `numbers.txt` * `phrases.txt` * `prepositions.txt` * `terminators.txt` * `verbs-passive.txt` * `verbs-plural.txt` * `verbs-singular.txt` #### Configuration The `config.yml` file controls how your dictionary works: name: medical description: Medical terminology triggers: [medical, medicine, health] extended_punctuation: false * `name`: The name of the dictionary * `description`: Display description * `triggers`: Alternative names that can be used to access the dictionary * `extended_punctuation`: Whether to include extended punctuation For detailed information about creating custom dictionaries, see the GitHub documentation. ### Wrapping up Markdown Lipsum API v4 represents a significant step forward in random text generation, offering expanded source options, new query parameters, and the flexibility of the `random-words` gem. Whether you’re using the API for quick placeholder content or the gem for programmatic text generation, you now have access to a much richer set of tools for creating random content. The combination of the web API and the Ruby gem provides flexibility for both quick one-off uses and more complex applications requiring custom dictionaries and programmatic control. _Try the new API athttps://brettterpstra.com/md-lipsum_ Enjoy, and please let me know in the comments if you have any questions or suggestions, or just want to let me know how you’ve implemented it. I’d love to share any Shortcuts or Services (or command line scripts) you create, so don’t be shy. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 18, 2025 at 9:46 AM
RNKD --- free ranked-choice polls
A few times over the last couple of years I’ve wanted to do an open poll to get a feel for how users felt about things, particularly icons during the design process. I found there aren’t a lot of affordable ways to do this. What I wanted was a way to upload multiple images and get ranked-choice voting on them. If you’re not familiar, ranked voting is a system where voters rank their choices in order of preference, and then an algorithm is used to determine the winner. So your second choice vote doesn’t get lost if your first choice doesn’t win — it becomes part of the aggregate. It’s the way US elections _should_ be run1. So I built RNKD. I took a morning away from working on Marked 3 to build it, but that’s all it took. It’s not a complicated beast, just does what it says on the tin. Sign in with an email address, create a poll by uploading images and adding optional captions, then share the link to the poll. Voters don’t need to log in, and anyone with the link can vote. Share it with your team, your friends, or your whole social network. Voting is anonymous. There’s currently no mechanism to prevent the same person from voting multiple times, which I should probably build in sooner than later if this is going to be used seriously. But assuming everyone voting isn’t intentionally trying to rig the game, it works great as is. Here’s a demo poll: RNKD Logo Options You can sign in and create your own polls at https://rnkd.xyz. I’m making it free (and accepting donations). If you use it for anything profitable, please consider clicking the “Buy me a coffee” link. And let me know what thoughts you have on it any time! 1. And adding more than two parties. That and doing away with the Electoral College would be a good start. But I digress. ↩ Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
October 3, 2025 at 9:37 AM
A quick look at Marked 3
Since leaving Oracle I’ve been hard at work on Marked 3, and holy cow it’s a _huge_ update. I expect to have the beta out in the next week or two. When it’s ready it will be available as a subscription directly via Paddle or the Mac App Store, and all Setapp users will get it as part of their subscription. There’s a new website up at markedapp.com, though none of download links will work. Soon, though. I decided to go with subscription pricing because I want to support continuous development. I haven’t charged an upgrade price for almost a decade, so people who paid $10 for the app 10 years ago have gotten all of the improvements for free for years. That’s not sustainable for me, and subscriptions mean I don’t have to release a new app every year, but rather just constantly improve on it. Here are some of the highlights that I think make Marked 3 worth paying for. (Marked 2 will continue to function but won’t get any more updates.) ### Custom Rules The new Custom Rules setup replaces the old Custom Preprocessor and Custom Processor settings. The old way only allowed one of each, and your only option was to manually enable it or disable it. Now you can have multiple processors that activate automatically based on criterium like file extension, metadata type, or even content detected in the document (regex available!). * Drag and drop a file onto the rules editor to see what rules it triggers * Select a built-in processor for each rule match * Many built-in transforms and actions, including search and replace, fix header heirarchy, etc. * Run multiple scripts/commands (custom pre/processors) in sequence * Inject JavaScript, text, or HTML * Set a style for a particular file type * and more! ### New Processors I’ve added CommonMark (with GFM extensions) as a default processor, which brings full compatibility with other apps that use CommonMark, such as Bear. I also added Kramdown, my personal favorite processor. It’s 99% compatible with MultiMarkdown syntax, and its major draw for me is IALs, a special syntax that can apply CSS classes to any element. ### DOCX madness Marked 2’s DOCX export was terrible. It basically just output rich text content into a DOCX container, with no structural information. I’ve completely rewritten the DOCX export from the ground up. Not only can Marked 3 output perfect DOCX files with a variety of styling options, it can also open most DOCX files and export them to other formats. * Export clean DOCX with correct structure and built-in styles * Bi-directional MathML to OMML (Word) * Convert comments and change tracking to CriticMarkup (also bi-directional) * Add your own export styles * Handle DOCX highlights, using a `<mark>` tag with appropriate background color * Syntax highlighting in code blocks is preserved when exporting to Word * Open DOCX directly in Marked * In DOCX import, allow highlights to be suppressed when comments are disabled ### Export Profiles Because the export settings are so vast, I’ve added “Export Profiles.” These allow you to save current settings as a profile, making it easy to switch between settings at export time. Every type of export has a dropdown in the save dialog for selecting a profile. You can use it for something as simple as setting a different header logo for different contexts, or for completely different export settings like page margins, markdown link styles, and anything else that affects any of the export formats. As part of setting this up, I refactored all of the export code to allow for future automation (AppleScript, Shortcuts). Previously Marked couldn’t bypass the Save dialog, and all of the export functionality was tied to callbacks. Now it’s separate and can be called externally. I’m partially finished with AppleScript support for things like `tell app "Marked" to export markdown to PATH with profile "Work"`. Still working out the syntax and I’ve run into some showstoppers on that, but once I get this release out I’ll get back to working on that. All of this will also be applied to Shortcuts actions and the URL handler. ### Paginated PDF magic Marked used to rely on the macOS print system to generate PDFs, which is basically the same as opening an HTML file in Safari and printing to PDF (with a few niceties like customizable headers and footers). I completely rewrote this in Marked 3, which builds the PDF manually, allowing for much more control. * Logos/images in headers and footers * Footnotes can appear on the page on which they’re referenced * Current heading/section in headers and footers * Intra-document links work (navigation anchors, Table of Contents) ### New export formats * EPUB with full styling, cover image, and metadata * TextBundle/TextPack with embedded assets, fully compatible with apps like Ulysses * Improved RTF export. If you’re trying to open in Pages, it’s better to export a DOCX and open that, but if you’re trying to get rich text into another app, the improved RTF handling will be useful. ### Markdown Dingus Experiment with Marked’s different built-in processors and see live rendering of both source code and web preview. ### Style Stealer Load any web page you like the look of and replicate its styling as a Marked Custom Style. The Style Stealer window accepts a URL, preferably an article/post page with plenty of content, and allows you to hover and click on the main content area of the page. It will grab the styles for existing elements and inject and read any that are missing, getting their computed style (the result of all CSS applied to the page). It even does a decent job of grabbing custom fonts, including Google Web Fonts. ### And more… * Allow user to specify minimum level to include in table of contents * Display comments (CriticMarkup, Scrivener, Word) in a sidebar instead of inline * [[Wiki Link]] navigation * Fully functional back/forward navigation when following links to other Markdown files * Make missing file links clickable to create a new file * Show wiki backlinks to current document * Autoscroll now works with a WPM (words per minute) setting, and calculates how many words would fit on the screen with the current style, setting the scroll speed accordingly * Remove RTFD export option. RTFD will be automatically selected when the export includes images * Custom Processor Log changed to Custom Rules Log, showing what rules run and any errors * Change reading speed right from the detailed stats window ### The rest I might as well stick the rest of the changelog in here… there will almost definitely be more to add by the time the final release is ready, but I’m one step away from beta, so this is a pretty complete list. #### NEW * Syntax highlighting for CSS and JSON fields * Custom Styles can be dragged and dropped on the Settings window to add them * Display a message when an empty file is opened * Import/Export all preferences as a JSON file #### IMPROVED * Reorderable custom Styles * Button visibility in Settings * Double click to rename Custom Styles in menu * Performance improvements for lined text views * Hide advanced config options when MathJax is disabled * Ignore non-numeric values in reading speed text field * Suppress open dialog when opening a file * Scrivener import will convert single-cell tables (used for asides) into block quotes * When exporting, convert non-built-in fonts to equivalent system fonts * Better regex for detecting highlights and deletions in text * Completely rewritten Scrivener import, faster and with no reliance on Ruby or Python * Rewritten CriticMarkup handling, all native, no reliance on external scripts or tools * Detailed stats view UI adjustments * Native mmd_merge handler * Hold option to set debug logging level from the Help menu * Additional documentation about debugging and support requests * Store current autoscroll speed as a global preference instead of preview-specific value, ensuring consistent behavior across previews and maintaining preferences across launches. * Markdown export can optionally wrap text at a specified width * Markdown export can optionally convert inline links to reference links * Change default file extension for markdown export to “.md” * Clean up Markdown export, especially for Word files * Better handling when custom processors are disabled or not available * Save assets to subfolder when exporting HTML, OPML, or Markdown * Substitute Goldilocks (Bear style) for Multi-Column * Handle proper conversion of Pandoc % metadata * Make document metadata available as MD_keyname environment variables to custom Run Command and Run Embedded Script actions * Improved conversion of existing custom processor settings to the new Custom Rules format #### FIXED * Help document list item link styling * Tab order in Proofreading Settings * Paragraphs generated by DOCX converter that start with `<mark>` not being properly handled by MultiMarkdown * Don’t strip query parameters when copying URL with right click or popup menu * Thread safe accessors for document stats * Status bar and drawer not changing to dark mode * Recognize more ways Word inconsistently marks paragraphs as headings * Handle `metadata` document in Scrivener binder as document metadata * Race condition on filesize * Don’t show open dialog if Welcome screen is opening * Handle MarsEdit 5 previews * Handle emphasis with internal spaces when converting HTML to Markdown * Raw file includes not working Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
September 15, 2025 at 9:24 AM
Web Excursions for August 18th, 2025
Web excursions brought to you in partnership with Setapp. Get access to hundreds of Mac and iOS apps for one low monthly subscription fee. A Mise guide for Swift developers > In this blog post we share how to use Mise to install, activate, and share tools to enhance Swift development. I switched to Mise (en place) a while ago and have loved it as a replacement for asdf. But I haven’t made good use of its task runners and other features, so this blog post was a good reminder of some of its advanced capabilities. TextIndex — Matt Gemmell > A lightweight syntax for creating indexes in Markdown. This is pretty cool, and represents some good syntax choices for extending Markdown. MindMap AI: Create AI-Powered Mind Maps Instantly This looks pretty great. I haven’t tried it yet, but with a pro plan under $10/mo and a basic plan under $5, if it works as well as I’ve read, I could become a subscriber. > Create instant AI-powered mind maps with MindMap AI. Featuring Copilot Chat, co-creation, multi-format input, and easy sharing. Revolutionize your brainstorming process today! Read a good review here. Notepad.exe - Your Developer’s Playground This looks really cool for experimenting with Swift and Python. More languages to come. Similar to CodeRunner but with additional features (like AI). Check out Setapp today and get access to the best Mac and iOS apps out there. Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
August 26, 2025 at 9:15 AM
lsgrep - rapid recursive file search
`lsgrep` is just a simple tool I use to quickly find files matching a search pattern in the current directory. I just figured I’d share it because I end up using it a lot. My primary shell is Fish, so I’ll share that one first. This version uses silver searcher (ag) to search for matches. The search uses wildcards (`*` for multiple characters and `?` for a single character). Spaces get turned into `.*?` and extensions (e.g. `.sh`) get escaped so the period matches. # Defined in /Users/ttscoff/.config/fish/functions/lsgrep.fish @ line 1 function lsgrep --description 'Wildcard folder/file search' # Convert search to regex: change .X to \.X, ? to ., and space or * to .*? set -l needle (echo $argv|sed -E 's/\.([a-z0-9]+)$/\\\.\1/'|sed -E 's/\?/./'| sed -E 's/[ *]/.*?/g') command ag --hidden --depth 3 -SUg "$needle" 2>/dev/null end Now I can run `lsgrep` in Fish like this: $ lsgrep gist test This will find the file `gist_test.sh` in any subdirectory up to 3 levels deep from the current directory. Here’s a version in Bash (or Zsh) that uses `find` instead, requiring no extra tools: #!/bin/bash NEEDLE=".*$(echo $@|sed -E 's/\.([a-z0-9]+)$/\\\.\1/'|sed -E 's/\?/./'| sed -E 's/[ *]/.*/g').*" find -E . -maxdepth 3 -regex $NEEDLE 2>/dev/null Because `find` doesn’t allow for `.*?` operators in the regex, it’s a little bit broader of a search, but in 99% of cases the results are the same as the `ag` version. You can obviously change the max depth in either function to a higher number if you want to search deeper in the directory tree. Hope that’s useful! Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
August 24, 2025 at 9:17 AM
Web Excursions for July 24th, 2025
Web excursions brought to you in partnership with CleanShot X, the absolute, hands-down best app for Mac screenshots. Get one of my all-time favorite apps here. Be My AI for Creating Photo Captions and Alt Text Allison Sheridan shares an awesome tool that, among it’s amazing capabilities, can create perfect ALT text for images you share on social media. It’s surprisingly accurate when describing images, and that kind of description is very important for disabled users. You should check it out. Apple’s macOS Apps Are the Weak Link Luc Beaudoin on the need for user-accessible ubiquitous linking in Apple’s own apps. ripple_progress_bar.py Forum user Cavalierex ported my Riplle progress indicator to Python as a context manager. Looks great! knadh/listmonk High performance, self-hosted, newsletter and mailing list manager with a modern dashboard. Single binary app. If I hadn’t already bought multiple licenses for Sendy I would 100% be hosting this. I do all of my screenshots and screen recordings with CleanShot X. I love it to pieces. You should get it. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
August 7, 2025 at 9:09 AM
47 years in this (scarred) skin
My 47th birthday was a few days ago. 11 years ago, I wrote a post on my birthday, detailing my addiction and recovery, and while I won’t go into the same points again, I thought I’d write something in the same vein. Shortly after my last (third) stint in rehab in my early 20s, I had moved back in with my parents while I found my footing. In the depths of bipolar depression (undiagnosed at the time), I went into the basement, coiled a coat hanger into a spiral, heated it to white hot with a blow torch, and pressed it into my forearm. I almost passed out from the pain, but it was, in lieu of proper treatment, what I needed at the time. I’ve always been extremely sensitive to sensation, both pleasurable and painful. It’s part of why drugs always sat well with me — they were fun, but also an escape from physical and emotional pain. And I’ve had lots of both my whole life. I didn’t shy away from pain — I got tattoos, got in fights, slammed around in mosh pits, and all of that, but I always had drugs to keep me from feeling the brunt of it. But here I was, stone cold sober, seeking out the pain I’d always sought to numb. It was clarifying, cathartic, and I felt peace like I hadn’t in a long time. When I was a kid, we used to have those Just Say No films shown to us. They made smoking look cool enough that I started when I was 12. But what really got me was one on heroin and cocaine. The “disastrous” results they portrayed looked appealing to me at 8 or 9 years old. I decided early on that when I could get drugs, I would. It would narrow my world down to worrying about just one thing, and that was appealing. Nancy Reagan made sure I knew it would be painful, but I wanted it. It was, in essence, about focus. I had so many internal voices and contradictions that I just sought simplicity. And addiction offered that. Intense pain offers that as well. It’s clarifying. Especially pain you can control when you can’t control anything else. But I’ve been clean for over 20 years, I’ve been through years of therapy, and I have diagnoses and treatments for my various disorders. I don’t seek out pain anymore. I don’t need it the way I used to. Now I avoid it when I can, and have healthier ways of dealing with what I can’t. I still have that brand on my arm, though. I look at it as a symbol of a painful time in my life, but the scar also represents healing to me. Of dealing with pain — depression, uncertainty, loss — and finding my way through it. Pain changes you, and it leaves a mark, but it doesn’t have to control you. Both pain and pleasure are temporary. I do a pretty good job of remembering pleasure, but I’m wont to forget pain. And that’s a good way to keep hurting yourself. This brand is something I can’t forget, and is a good reminder about recovery the temporary nature of pain. When my partner, Elle, knit a raccoon (my favorite animal) for me, they knit a matching brand into its right foreleg. It meant so much to me, representing the same pain and healing that my own brand does, and it reminds me that I’m not alone. I love it (and them) more than I can say. Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
August 1, 2025 at 9:06 AM
Liiift giveaway!
I’m excited to offer the next giveaway, 5 licenses ($19.99 value each) for Liiift. I don’t lift much these days, but this app looks good enough to get me back into it. Liiift is a beautiful iOS app designed to help you get the most out of your weightlifting workouts. This giveaway is for lifetime licenses! From the developer: > Liiift is an iOS workout tracker and planner for weightlifting routines. It helps you stay focused in the gym with rest timer, and pushes you further with a power bar that compares your current progress to your typical lifts. Liiift packs power in a simple, offline-first, iOS-native design that feels right at home. Check out the Liiift site for more info. Sign up below to enter. Winners will be randomly drawn on Friday, July 18, at 12pm Central. The drawing is for 5 licenses ($19.99 value each) for Liiift, one per winner. Note that if you’re reading this via RSS, you’ll need to visit this post on brettterpstra.com to enter! New rule: All signups must have a **first and last name** in order to be eligible. Entries with only a first name will be skipped by the giveaway robot. A lot of the vendors in this series require first and last names for generating license codes, and your cooperation is appreciated! _Sorry, this giveaway has ended._ If you have an app you’d love to see featured in this series of giveaways, let me know. Also be sure to sign up for the mailing list or follow me on Mastodon so you can be (among) the first to know about these! Like or share this post on Mastodon, Bluesky, or Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
July 28, 2025 at 9:11 AM
Ripple: an indeterminate progress indicator
I made a thing I think is pretty cool. It’s an indeterminate progress indicator for use in your scripts, inspired by the way Cursor indicates it’s working. In Cursor, when it’s generating or thinking, it shows some text that looks like a spotlight is panning across it. I figured it would be easy enough to replicate, so I did. Here’s what it looks like: To use it in a script, just save the code in the gist to a file called `ripple` in your $PATH (or right next to your script). Then call `ripple "TEXT TO RIPPLE" -c "COMMAND TO RUN"`. Ripple will run the command, and while it’s waiting it will “ripple” the text provided. It has some options: $ ripple -h Usage: ripple [options] STRING -s, --speed SPEED Set animation speed ((f)ast/(m)edium/(s)low) -r, --rainbow Enable rainbow mode -d, --direction DIRECTION Set animation format ((f)orward/(b)ack-and-forth) -i, --inverse Enable inverse mode -c, --command COMMAND Run a command during the animation --stdout Output captured command result to STDOUT --quiet Suppress all output -v, --version Display the version -h, --help Display this help message It also works as a Ruby library: require 'ripple' rippler = Ripple.new(ARGV.join(" "), { speed: :medium, format: :bidirectional }) Ripple.hide_cursor while true do rippler.advance end Ripple.show_cursor # OR Ripple.progress("IN PROGRESS", { speed: :fast }) { sleep 5 } Pass a block to `Ripple.progress` with a string argument to run some code while the ripple is running. Just a little fun thing, let me know if you find it useful! Here’s the gist again. Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
July 9, 2025 at 8:58 AM
The Mac Users's Guide to Saner Email in 2025 [Sponsor]
Thanks to SaneBox for sponsoring BrettTerpstra.com this week! Rather than the typical sponsored post, I’ve opened up the blog to a guest post from SaneBox’s Thomas Yuan. Not a typical thing for me, but I’m a huge fan of SaneBox and Thomas’ tools are a perfect fit for my readers (and my own preferences). Please enjoy! Picture this: There’s a constant stream of messages filling your inbox, from notifications to urgent memos to mailing lists you joined 10 years ago. But instead of getting distracted, feeling overwhelmed, or spending hours sifting through them, every email is filed right where you need it. This is the beauty of implementing email management tools. With the right email stack, you can put the most labor-intensive tasks on autopilot and waste much less time in your inbox. Here’s how I built my own low-maintenance workflow — and the tools that help me do it. ### The Joy of an Email Stack That Doesn’t Suck Without filters and workflows, your inbox is a messy stream of information and requests, all with different levels of urgency. If you try to organize your inbox manually, you’ll waste hours every week on a never-ending battle… as soon as you deal with one email, three more pop up in its place! That’s why an email stack that leverages AI is so powerful. It can take the hard work out of managing your email, but you still control the rules. Here are the essential ingredients for my own email stack: * Powerful filtering: I need to see exactly what’s urgent without being distracted by newsletters, calendar invites, notifications, and other messages that can wait. * Features to help me prioritize: I like to queue up each day’s high-priority tasks and take distractions out of sight. This prevents me from getting dragged into the guilty mindset of “I’ll just tackle one more message…” * Learning support: I subscribe to a ton of newsletters, from product releases to tech blogs, but they’re useless if they simply drift down my Inbox. ### My 2025 Email Productivity Stack #### 1. MailMate + SaneBox = efficient, customizable inbox management MailMate is my favorite email client on Mac and if you live by keyboard shortcuts it might be perfect for you, too. I use it to set up a tagging system to help organize my emails. SaneBox, which works across different email clients, is the perfect complement to MailMate. It adds a trainable intelligence layer that automatically organizes my email into categories. Here’s how I get the most out of SaneBox: * **Trainable AI:** To train SaneBox, I drag an email to one of my folders. The next email from that sender will show up in that folder. Easy. * **BlackHole:** I want to unsubscribe in one click, but I don’t want to click on risky unsubscribe links. BlackHole does the dirty work for me, I just drag an email to the BlackHole and I’ll never hear from that sender again. * **Reminders and Snooze:** These SaneBox features are distraction-busters. They allow me to set custom reminders, or simply snooze an email until I’m ready to deal with it. #### 2. Streamlining my stack (another win for SaneBox) Simplicity is the key. Your stack should reduce the number of tools you need, and work well with your existing email client and workflows. By adding SaneBox to my toolkit, I no longer needed to use inbox cleaning services like Clean Email or attachment cleaner tools to clear space. The Deep Clean feature is already built into SaneBox and I periodically use it to clear out old emails and free up storage space. SaneBox also fits in with the rest of my stack, with no duplication of effort. There’s no need to rebuild filters in MailMate or any other client, my email behaves consistently everywhere. #### 3. Emailing faster and more decisively with snippet tools Whether it’s an introduction or a polite “no thanks,” no one likes typing the same replies over and over again. Tools like TextExpander and Text Blaze are great additions to any email strategy. I use them to create pre-defined snippets for quick replies, and I can activate them with just a few keystrokes. #### Bonus email hack: Turning my inbox into a personal news reader! My favorite hack is filtering newsletters into a SaneReading folder. With MailMate’s ability to display one inbox per sender, I can create a “news reader” experience right within my inbox. ### My Daily Email Routine You need a solid routine to maximize the benefits of your email stack, otherwise you might still dip in and out of your inbox and waste time throughout the day. Here’s the email routine that works for me: Morning Review In the morning, I perform a quick “Inbox triage,” following the Eisenhower Matrix: I reply to anything time-sensitive, hit snooze on emails that can wait, and set reminders for emails that I need to handle within a specific time frame. It doesn’t take long because the only things waiting for me in my Inbox are important: VIP senders, my family, and anything else that I’ve trained SaneBox’s AI to recognize as urgent. Midday Check-in I usually check my inbox again around lunch to make sure nothing time-sensitive has come in. If I see anything new in my inbox that isn’t important, I drag it to my SaneLater folder. This trains SaneBox’s AI to file future messages from that sender in my SaneLater folder, so they don’t clog up my inbox. Evening Email Blast I’m most decisive in the evenings, so that’s when I blast through most of my emails. I start with emails that I’ve snoozed earlier in the day, and I fire off some replies. Then I review my Digest; this is a daily update from SaneBox, like an executive summary of all the emails SaneBox considers non-urgent. I save time by bulk-archiving or mass-deleting messages, and Digest gives me confidence that nothing important is slipping through the cracks. Then comes the fun part: I can look at my SaneReading folder and catch up on my newsletters! This way, I keep on top of my email but it’s working for me, not the other way around. My inbox doesn’t dominate my day. * * * Thanks again to SaneBox, both for existing and for sponsoring. Check it out today and save $25 off! Like or share this post Twitter. * * * BrettTerpstra.com is supported by readers like you. Click here if you'd like to help out. Find Brett on Mastodon, Bluesky, GitHub, and everywhere else.
brett.trpstra.net
June 30, 2025 at 8:55 AM